@a-t-h-i/bot-lobby 0.6.13 → 0.6.15
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 +70 -79
- package/package.json +4 -2
- package/prompts/master.md +3 -2
- package/prompts/qa.md +33 -11
- package/prompts/reviewer.md +5 -2
- package/prompts/worker.md +5 -2
- package/src/ask/relay.ts +1 -2
- package/src/ask/tool.ts +4 -6
- package/src/ask/types.ts +16 -0
- package/src/ask/web.ts +63 -0
- package/src/index.ts +7 -1
- package/src/lobby/ask.ts +4 -5
- package/src/lobby/blocks.ts +57 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +178 -0
- package/src/lobby/keys.ts +10 -41
- package/src/lobby/prompt-hub.ts +30 -77
- package/src/lobby/prompts.ts +8 -12
- package/src/lobby/runtime.ts +49 -310
- package/src/lobby/service.ts +54 -134
- package/src/lobby/task-rows.ts +28 -0
- package/src/pi/commands.ts +14 -20
- package/src/pi/model-settings.ts +112 -0
- package/src/pi/start-task.ts +1 -1
- package/src/pi/tools.ts +6 -7
- package/src/pi/ui.ts +0 -9
- package/src/schemas/configuration.ts +9 -33
- package/src/state/attachments.ts +82 -0
- package/src/webui/api/index.ts +17 -15
- package/src/webui/api/lobby.ts +4 -3
- package/src/webui/api/planner.ts +6 -4
- package/src/webui/api/quickfix.ts +4 -3
- package/src/webui/api/sessions.ts +9 -7
- package/src/webui/api/settings.ts +20 -8
- package/src/webui/api/status.ts +4 -15
- package/src/webui/api/tasks.ts +11 -15
- package/src/webui/command.ts +35 -19
- package/src/webui/dev/fake-service.ts +1 -2
- package/src/webui/dev/fixtures.ts +2 -5
- package/src/webui/events.ts +1 -1
- package/src/webui/protocol.ts +22 -11
- package/src/webui/server.ts +42 -1
- package/src/webui/uploads.ts +68 -0
- package/webui/dist/assets/index-B1PnCyl2.css +2 -0
- package/webui/dist/assets/index-DI9Yq9IC.js +90 -0
- package/webui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/webui/dist/build.json +142 -110
- package/webui/dist/index.html +4 -3
- package/src/ask/dialog.ts +0 -193
- package/src/ask/image.ts +0 -202
- package/src/ask/png.ts +0 -179
- package/src/ask/state.ts +0 -175
- package/src/ask/view.ts +0 -163
- package/src/lobby/layout.ts +0 -496
- package/src/lobby/markdown.ts +0 -121
- package/src/lobby/mini.ts +0 -167
- package/src/lobby/tabs/excalidraw.ts +0 -95
- package/src/lobby/tabs/git.ts +0 -162
- package/src/lobby/tabs/home.ts +0 -544
- package/src/lobby/tabs/issues.ts +0 -73
- package/src/lobby/tabs/knowledge.ts +0 -135
- package/src/lobby/tabs/metrics.ts +0 -300
- package/src/lobby/tabs/plan.ts +0 -262
- package/src/lobby/tabs/quickfix.ts +0 -136
- package/src/lobby/tabs/tasks.ts +0 -412
- package/src/lobby/theme.ts +0 -30
- package/src/lobby/view.ts +0 -3018
- package/src/pi/settings-ui.ts +0 -708
- package/src/width.ts +0 -102
- package/webui/dist/assets/index-Bk-lpAGm.css +0 -2
- package/webui/dist/assets/index-E6MPcbdC.js +0 -80
package/src/lobby/service.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The lobby's backend
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* drive the lobby through this module without a TUI. Putting a question on
|
|
6
|
-
* screen stays with the terminal; the dialog data and its answer live here.
|
|
2
|
+
* The lobby's backend: `createLobbyService(state)` builds the service the web
|
|
3
|
+
* server drives from the runtime's state. Questions go to the page through
|
|
4
|
+
* the prompt hub (see ../ask/web.ts).
|
|
7
5
|
*/
|
|
8
6
|
import { SessionManager, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
7
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
10
8
|
import { loadTask, peekTasks } from "../state/persistence.ts";
|
|
9
|
+
import { releaseAttachments } from "../state/attachments.ts";
|
|
11
10
|
import { loadConfig, saveConfig as writeConfig } from "../state/project.ts";
|
|
12
11
|
import { addPlanComment, readPlanComments } from "../state/comments.ts";
|
|
13
12
|
import { isAutoMode } from "../state/auto.ts";
|
|
@@ -16,7 +15,7 @@ import { livePresence } from "../state/presence.ts";
|
|
|
16
15
|
import { archiveTask as archiveTaskOnDisk, deleteTask as deleteTaskOnDisk, listArchivedTasks, restoreTask as restoreTaskOnDisk } from "../state/archive.ts";
|
|
17
16
|
import { discardPlannedTask, listPlannedTasks, type PlannedTask } from "../state/backlog.ts";
|
|
18
17
|
import { readClassifierMetrics, readMetrics } from "../state/metrics.ts";
|
|
19
|
-
import { currentZenTask,
|
|
18
|
+
import { currentZenTask, taskSnapshot } from "../pi/ui.ts";
|
|
20
19
|
import { taskName } from "../text.ts";
|
|
21
20
|
import { describeWorkspace } from "../execution/workspace.ts";
|
|
22
21
|
import { stripStartFlags } from "../pi/start-flags.ts";
|
|
@@ -29,21 +28,18 @@ import { chatFromEntries, lobbyFeed, type ChatEntry } from "./feed.ts";
|
|
|
29
28
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
30
29
|
import { launchPi, SessionRegistry, type BackgroundSession, type DialogAnswer, type SessionLauncher } from "./sessions.ts";
|
|
31
30
|
import { SessionChats } from "./session-files.ts";
|
|
32
|
-
import { answerMessage,
|
|
31
|
+
import { answerMessage, questionnaires, settledQuestions } from "./ask.ts";
|
|
32
|
+
import { askUser } from "../ask/web.ts";
|
|
33
|
+
import type { Asker } from "../ask/types.ts";
|
|
33
34
|
import { PlanningSession, type PlannerSeed } from "./planner.ts";
|
|
35
|
+
import { applyMasterModel } from "../pi/model-settings.ts";
|
|
34
36
|
import { lobbyTopics } from "./topics.ts";
|
|
35
|
-
import { promptHub } from "./prompt-hub.ts";
|
|
36
37
|
import { checkSession } from "../excalidraw/check.ts";
|
|
37
38
|
import type { LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
|
|
38
|
-
import type { LiveSession,
|
|
39
|
+
import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
|
|
40
|
+
import { pushNotice, type NoticeLevel } from "../webui/notices.ts";
|
|
39
41
|
import type { Runtime } from "./runtime.ts";
|
|
40
42
|
|
|
41
|
-
/**
|
|
42
|
-
* The lobby without its terminal-only members (rows, theme, hide,
|
|
43
|
-
* requestRender, now, keys, panels, savePanels, editText, openSettings).
|
|
44
|
-
*/
|
|
45
|
-
export type LobbyService = Omit<LobbyHost, "rows" | "theme" | "hide" | "requestRender" | "now" | "keys" | "panels" | "savePanels" | "editText" | "openSettings">;
|
|
46
|
-
|
|
47
43
|
/** The state background callbacks report to; set while the lobby service runs. */
|
|
48
44
|
let serviceState: Runtime | undefined;
|
|
49
45
|
|
|
@@ -52,36 +48,14 @@ export function setServiceState(state: Runtime | undefined): void {
|
|
|
52
48
|
serviceState = state;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
/** Repaints asked for by what happens in the background come at most this often. */
|
|
56
|
-
export const FRAME_MS = 40;
|
|
57
|
-
let lastFrame = 0;
|
|
58
|
-
let frameTimer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
-
|
|
60
51
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `FRAME_MS`, instead of each asking for its own. Keys repaint at once
|
|
64
|
-
* through pi.
|
|
52
|
+
* Tell the user: a toast in the page, and in pi's own terminal too when it is
|
|
53
|
+
* a warning or an error (the page may not be open).
|
|
65
54
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
const wait = lastFrame + FRAME_MS - Date.now();
|
|
71
|
-
const paint = () => {
|
|
72
|
-
frameTimer = undefined;
|
|
73
|
-
lastFrame = Date.now();
|
|
74
|
-
const current = serviceState;
|
|
75
|
-
if (current?.visible || (current && miniShown(current))) current.tui?.requestRender();
|
|
76
|
-
};
|
|
77
|
-
if (wait <= 0) return paint();
|
|
78
|
-
frameTimer = setTimeout(paint, wait);
|
|
79
|
-
frameTimer.unref?.();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The status line shows under the editor while the lobby is hidden (not while bot-lobby is minimized), unless settings turn it off. */
|
|
83
|
-
export function miniShown(state: Runtime): boolean {
|
|
84
|
-
return !state.visible && !isMinimized() && loadConfig().lobby.miniLine;
|
|
55
|
+
export function lobbyNotify(state: Runtime, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
56
|
+
lobbyTopics.bump("notices");
|
|
57
|
+
pushNotice(message, level as NoticeLevel);
|
|
58
|
+
if (level !== "info") state.ctx.ui.notify(message, level);
|
|
85
59
|
}
|
|
86
60
|
|
|
87
61
|
function sessionModel(ctx: ExtensionContext): string | undefined {
|
|
@@ -92,7 +66,7 @@ export function lobbyProfile(state: Runtime, kind: LobbyAgentKind) {
|
|
|
92
66
|
return resolveLobbyProfile(loadConfig(), kind, {
|
|
93
67
|
lookup: modelLookup(state.ctx),
|
|
94
68
|
sessionModel: sessionModel(state.ctx),
|
|
95
|
-
warn: (message) => state
|
|
69
|
+
warn: (message) => lobbyNotify(state, message, "warning"),
|
|
96
70
|
});
|
|
97
71
|
}
|
|
98
72
|
|
|
@@ -118,15 +92,14 @@ function addComment(state: Runtime, taskId: string, text: string): string {
|
|
|
118
92
|
|
|
119
93
|
function failed(state: Runtime, what: string, error: Error): void {
|
|
120
94
|
lobbyFeed.log("LOBBY", `${what} — ${error.message}`, "error");
|
|
121
|
-
|
|
122
|
-
state.ctx.ui.notify(`bot-lobby: ${what} — ${error.message}`, "error");
|
|
95
|
+
lobbyNotify(state, `bot-lobby: ${what} — ${error.message}`, "error");
|
|
123
96
|
}
|
|
124
97
|
|
|
125
98
|
function toOracle(state: Runtime, text: string): string | undefined {
|
|
126
99
|
const busy = !state.ctx.isIdle();
|
|
127
100
|
if (text.startsWith("/")) {
|
|
128
101
|
state.pi.sendUserMessage(text, { expandPromptTemplates: true, ...(busy ? { deliverAs: "followUp" as const } : {}) });
|
|
129
|
-
return `sent ${text.split(/\s+/)[0]} to pi
|
|
102
|
+
return `sent ${text.split(/\s+/)[0]} to pi`;
|
|
130
103
|
}
|
|
131
104
|
// While the oracle decides where a request goes, what the user types joins that conversation.
|
|
132
105
|
if (!currentZenTask() && !pendingRequest()) {
|
|
@@ -143,9 +116,7 @@ function startPlanned(state: Runtime, plan: PlannedTask): string {
|
|
|
143
116
|
startPlannedTask(state.pi, state.ctx, state.configDir, plan.id)
|
|
144
117
|
.then((task) => {
|
|
145
118
|
if (typeof task === "string") return failed(state, `could not start ${plan.id}`, new Error(task));
|
|
146
|
-
state.view?.setTab("lobby");
|
|
147
119
|
lobbyTopics.bump("tasks");
|
|
148
|
-
repaintLobby();
|
|
149
120
|
})
|
|
150
121
|
.catch((error: Error) => failed(state, `could not start ${plan.id}`, error));
|
|
151
122
|
return `starting ${plan.id} here — its agreed plan needs no approval…`;
|
|
@@ -167,10 +138,7 @@ const chats = new SessionChats(async () => {
|
|
|
167
138
|
if (!dirs.has(state.root)) dirs.set(state.root, undefined);
|
|
168
139
|
const lists = await Promise.all([...dirs].map(([cwd, dir]) => SessionManager.list(cwd, dir).catch(() => [])));
|
|
169
140
|
return lists.flat();
|
|
170
|
-
}, () =>
|
|
171
|
-
lobbyTopics.bump("sessions");
|
|
172
|
-
repaintLobby();
|
|
173
|
-
});
|
|
141
|
+
}, () => lobbyTopics.bump("sessions"));
|
|
174
142
|
|
|
175
143
|
/** Launch background sessions with something else (tests); resets the registry. */
|
|
176
144
|
export function setSessionLauncher(next: SessionLauncher | undefined): void {
|
|
@@ -199,7 +167,7 @@ export function backgroundSessions(): readonly BackgroundSession[] {
|
|
|
199
167
|
return registry?.sessions ?? [];
|
|
200
168
|
}
|
|
201
169
|
|
|
202
|
-
/** A background session changed:
|
|
170
|
+
/** A background session changed: say so when one asks something or fails. */
|
|
203
171
|
function sessionsChanged(): void {
|
|
204
172
|
lobbyTopics.bump("sessions");
|
|
205
173
|
const state = serviceState;
|
|
@@ -207,20 +175,13 @@ function sessionsChanged(): void {
|
|
|
207
175
|
if (session.sessionId && session.sessionFile) chats.remember(session.sessionId, session.sessionFile);
|
|
208
176
|
const seen = announced.get(session.key) ?? 0;
|
|
209
177
|
announced.set(session.key, session.dialogs.length);
|
|
210
|
-
if (!state
|
|
211
|
-
if (session.dialogs.length > seen) {
|
|
212
|
-
lobbyTopics.bump("notices");
|
|
213
|
-
state.ctx.ui.notify(`bot-lobby: ${session.name} is waiting for you — alt+l opens the lobby`, "info");
|
|
214
|
-
}
|
|
178
|
+
if (!state) continue;
|
|
179
|
+
if (session.dialogs.length > seen) lobbyNotify(state, `bot-lobby: ${session.name} is waiting for you — see the Sessions page`, "info");
|
|
215
180
|
if (!session.alive && !exitsSaid.has(session.key)) {
|
|
216
181
|
exitsSaid.add(session.key);
|
|
217
|
-
if (session.exitCode) {
|
|
218
|
-
lobbyTopics.bump("notices");
|
|
219
|
-
state.ctx.ui.notify(`bot-lobby: ${session.name} exited (${session.exitCode})${session.lastError() ? `: ${session.lastError()}` : ""}`, "warning");
|
|
220
|
-
}
|
|
182
|
+
if (session.exitCode) lobbyNotify(state, `bot-lobby: ${session.name} exited (${session.exitCode})${session.lastError() ? `: ${session.lastError()}` : ""}`, "warning");
|
|
221
183
|
}
|
|
222
184
|
}
|
|
223
|
-
repaintLobby();
|
|
224
185
|
}
|
|
225
186
|
|
|
226
187
|
/** Start a task in its own new pi session, named after the task; the session, or why not. */
|
|
@@ -245,39 +206,6 @@ function startSession(state: Runtime, start: { request?: string; plan?: PlannedT
|
|
|
245
206
|
}
|
|
246
207
|
}
|
|
247
208
|
|
|
248
|
-
/** The oldest question a background session waits on, if any (data only; the terminal draws it). */
|
|
249
|
-
export function peekSessionDialog(session: BackgroundSession) {
|
|
250
|
-
return session.dialogs[0];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Answer a background session's dialog (data only; the terminal draws it). */
|
|
254
|
-
export function answerSessionDialog(session: BackgroundSession, id: string, answer: DialogAnswer): void {
|
|
255
|
-
session.answer(id, answer);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Put the oldest question a background session waits on to the user, with pi's own dialogs; escape cancels it as it would there. */
|
|
259
|
-
async function answerDialog(state: Runtime, session: BackgroundSession): Promise<void> {
|
|
260
|
-
const dialog = peekSessionDialog(session);
|
|
261
|
-
if (!dialog) return;
|
|
262
|
-
const title = `${session.name} — ${dialog.title}`;
|
|
263
|
-
const ui = state.ctx.ui;
|
|
264
|
-
try {
|
|
265
|
-
await promptHub.run("sessionDialog", session.name, { dialog }, async () => {
|
|
266
|
-
if (dialog.method === "select") {
|
|
267
|
-
const value = await ui.select(title, dialog.options ?? []);
|
|
268
|
-
answerSessionDialog(session, dialog.id, value === undefined ? { cancelled: true } : { value });
|
|
269
|
-
} else if (dialog.method === "confirm") {
|
|
270
|
-
answerSessionDialog(session, dialog.id, { confirmed: await ui.confirm(title, dialog.message ?? "") });
|
|
271
|
-
} else {
|
|
272
|
-
const value = dialog.method === "input" ? await ui.input(title, dialog.placeholder) : await ui.editor(title, dialog.prefill);
|
|
273
|
-
answerSessionDialog(session, dialog.id, value === undefined ? { cancelled: true } : { value });
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
} catch (error) {
|
|
277
|
-
failed(state, `could not answer ${session.name}`, error as Error);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
209
|
/** Leave a message for a task another terminal's session drives; its owner passes it to the oracle. */
|
|
282
210
|
function sendToTask(state: Runtime, taskId: string, text: string): string {
|
|
283
211
|
const task = loadTask(state.root, state.configDir, taskId);
|
|
@@ -350,16 +278,6 @@ function sendToSession(state: Runtime, sessionId: string, text: string): string
|
|
|
350
278
|
return "sent — that session passes it to its oracle within a few seconds";
|
|
351
279
|
}
|
|
352
280
|
|
|
353
|
-
/** The lobby reopens on the session this window switches to (set just before asking pi to switch). */
|
|
354
|
-
let reopenAfterSwitch = false;
|
|
355
|
-
|
|
356
|
-
/** Whether a switch asked for from the lobby is still owed its reopen; clears the flag. */
|
|
357
|
-
export function takeReopenAfterSwitch(): boolean {
|
|
358
|
-
const value = reopenAfterSwitch;
|
|
359
|
-
reopenAfterSwitch = false;
|
|
360
|
-
return value;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
281
|
/**
|
|
364
282
|
* Run another session in this window through `/bot-lobby switch`, which pi
|
|
365
283
|
* runs with a command context (the only one that may replace the session).
|
|
@@ -367,9 +285,8 @@ export function takeReopenAfterSwitch(): boolean {
|
|
|
367
285
|
* task no session owns is claimed instead.
|
|
368
286
|
*/
|
|
369
287
|
async function switchTo(state: Runtime, target: SwitchTarget): Promise<string> {
|
|
370
|
-
if (!state.ctx.isIdle()) return "this window's oracle is working —
|
|
288
|
+
if (!state.ctx.isIdle()) return "this window's oracle is working — stop it, then switch";
|
|
371
289
|
if (target.claimTaskId) {
|
|
372
|
-
reopenAfterSwitch = false;
|
|
373
290
|
state.pi.sendUserMessage(`/bot-lobby claim ${target.claimTaskId}`, { expandPromptTemplates: true });
|
|
374
291
|
return `taking ${target.claimTaskId} over in this window`;
|
|
375
292
|
}
|
|
@@ -380,7 +297,6 @@ async function switchTo(state: Runtime, target: SwitchTarget): Promise<string> {
|
|
|
380
297
|
}
|
|
381
298
|
file ??= target.sessionId ? await chats.locate(target.sessionId) : undefined;
|
|
382
299
|
if (!file) return `could not find ${target.name}'s session file — /resume lists every saved session`;
|
|
383
|
-
reopenAfterSwitch = true;
|
|
384
300
|
state.pi.sendUserMessage(`/bot-lobby switch ${file}`, { expandPromptTemplates: true });
|
|
385
301
|
return `switching this window to ${target.name}…`;
|
|
386
302
|
}
|
|
@@ -390,7 +306,7 @@ function drivenElsewhere(state: Runtime, task: Task): string | undefined {
|
|
|
390
306
|
if (TERMINAL_STATES.includes(task.state) || !task.ownerSessionId) return undefined;
|
|
391
307
|
if (task.ownerSessionId === state.ctx.sessionManager.getSessionId()) return `this window drives ${task.id} — cancel it first (/bot-lobby cancel ${task.id})`;
|
|
392
308
|
const background = backgroundSessions().find((session) => session.alive && session.sessionId === task.ownerSessionId);
|
|
393
|
-
if (background) return `${background.name} is driving ${task.id} in the background — stop it first
|
|
309
|
+
if (background) return `${background.name} is driving ${task.id} in the background — stop it first`;
|
|
394
310
|
if (liveSessions(state).some((session) => session.sessionId === task.ownerSessionId)) return `a session in another terminal is driving ${task.id} — finish or cancel it there first`;
|
|
395
311
|
return undefined;
|
|
396
312
|
}
|
|
@@ -402,12 +318,13 @@ function archiveTask(state: Runtime, taskId: string): string {
|
|
|
402
318
|
if (busy) return busy;
|
|
403
319
|
try {
|
|
404
320
|
archiveTaskOnDisk(state.root, state.configDir, taskId);
|
|
321
|
+
releaseAttachments(taskId);
|
|
405
322
|
} catch (error) {
|
|
406
323
|
return (error as Error).message;
|
|
407
324
|
}
|
|
408
325
|
lobbyFeed.log("LOBBY", `archived ${taskId}`, "info");
|
|
409
326
|
lobbyTopics.bump("tasks");
|
|
410
|
-
return `archived ${taskId}${TERMINAL_STATES.includes(task.state) ? "" : " (abandoned first)"} —
|
|
327
|
+
return `archived ${taskId}${TERMINAL_STATES.includes(task.state) ? "" : " (abandoned first)"} — show archived tasks to restore it`;
|
|
411
328
|
}
|
|
412
329
|
|
|
413
330
|
function restoreTask(state: Runtime, taskId: string): string {
|
|
@@ -430,6 +347,7 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
430
347
|
}
|
|
431
348
|
try {
|
|
432
349
|
deleteTaskOnDisk(state.root, state.configDir, taskId, where);
|
|
350
|
+
releaseAttachments(taskId);
|
|
433
351
|
} catch (error) {
|
|
434
352
|
return (error as Error).message;
|
|
435
353
|
}
|
|
@@ -443,7 +361,7 @@ export function reviewProfile(state: Runtime) {
|
|
|
443
361
|
return resolveReviewProfile(loadConfig(), {
|
|
444
362
|
lookup: modelLookup(state.ctx),
|
|
445
363
|
sessionModel: sessionModel(state.ctx),
|
|
446
|
-
warn: (message) => state
|
|
364
|
+
warn: (message) => lobbyNotify(state, message, "warning"),
|
|
447
365
|
});
|
|
448
366
|
}
|
|
449
367
|
|
|
@@ -451,7 +369,7 @@ export function seatProfile(state: Runtime, member: PanelMember) {
|
|
|
451
369
|
return resolvePanelProfile(loadConfig(), member, {
|
|
452
370
|
lookup: modelLookup(state.ctx),
|
|
453
371
|
sessionModel: sessionModel(state.ctx),
|
|
454
|
-
warn: (message) => state
|
|
372
|
+
warn: (message) => lobbyNotify(state, message, "warning"),
|
|
455
373
|
});
|
|
456
374
|
}
|
|
457
375
|
|
|
@@ -473,20 +391,13 @@ function newPlanner(state: Runtime, seed?: PlannerSeed, seats?: readonly PanelMe
|
|
|
473
391
|
stallTimeoutMs: workflow.stallTimeoutMs,
|
|
474
392
|
toolStallTimeoutMs: workflow.toolStallTimeoutMs,
|
|
475
393
|
feed: lobbyFeed,
|
|
476
|
-
onChange: () =>
|
|
477
|
-
|
|
478
|
-
repaintLobby();
|
|
479
|
-
},
|
|
480
|
-
onRound: (session) => {
|
|
481
|
-
lobbyTopics.bump("planner");
|
|
482
|
-
// Put the questions to the user at once when they are looking at the Plan tab.
|
|
483
|
-
if (loadConfig().lobby.autoAsk && state.visible && state.view?.tab === "plan" && session.awaitingAnswers) void answerPanel(state);
|
|
484
|
-
},
|
|
394
|
+
onChange: () => lobbyTopics.bump("planner"),
|
|
395
|
+
onRound: () => lobbyTopics.bump("planner"),
|
|
485
396
|
}, seed);
|
|
486
397
|
return state.planner;
|
|
487
398
|
}
|
|
488
399
|
|
|
489
|
-
/** The questionnaire
|
|
400
|
+
/** The questionnaire in the page; tests swap it. */
|
|
490
401
|
function panelAsker(state: Runtime): Asker {
|
|
491
402
|
return state.asker ?? askUser;
|
|
492
403
|
}
|
|
@@ -504,10 +415,10 @@ export async function savePlan(state: Runtime | undefined): Promise<string> {
|
|
|
504
415
|
state.asking = true;
|
|
505
416
|
try {
|
|
506
417
|
const asker = panelAsker(state);
|
|
507
|
-
return await session.saveWithSplit((questions, signal) =>
|
|
418
|
+
return await session.saveWithSplit((questions, signal) => asker(questions, state.ctx, signal, "planner"), { splitAbove: loadConfig().lobby.splitPlanAbove });
|
|
508
419
|
} finally {
|
|
509
420
|
state.asking = false;
|
|
510
|
-
|
|
421
|
+
lobbyTopics.bump("planner");
|
|
511
422
|
}
|
|
512
423
|
}
|
|
513
424
|
|
|
@@ -529,10 +440,9 @@ export async function answerPanel(state: Runtime | undefined): Promise<string> {
|
|
|
529
440
|
const chunks = questionnaires(session.questions);
|
|
530
441
|
for (let index = session.answered.length; index < chunks.length; index++) {
|
|
531
442
|
const questions = chunks[index]!;
|
|
532
|
-
const result = await
|
|
443
|
+
const result = await asker(questions, state.ctx, undefined, "planner");
|
|
533
444
|
if (result.cancelled) {
|
|
534
|
-
|
|
535
|
-
return session.answered.length > 0 ? `answers kept — ${chunks.length - session.answered.length} questionnaire${chunks.length - session.answered.length === 1 ? "" : "s"} left; enter resumes` : "questions put away — enter brings them back";
|
|
445
|
+
return session.answered.length > 0 ? `answers kept — ${chunks.length - session.answered.length} questionnaire${chunks.length - session.answered.length === 1 ? "" : "s"} left; answer again to resume` : "questions put away — answer again to bring them back";
|
|
536
446
|
}
|
|
537
447
|
session.answered = [...session.answered, result];
|
|
538
448
|
}
|
|
@@ -547,7 +457,7 @@ export async function answerPanel(state: Runtime | undefined): Promise<string> {
|
|
|
547
457
|
return `could not put the questions: ${(error as Error).message}`;
|
|
548
458
|
} finally {
|
|
549
459
|
state.asking = false;
|
|
550
|
-
|
|
460
|
+
lobbyTopics.bump("planner");
|
|
551
461
|
}
|
|
552
462
|
}
|
|
553
463
|
|
|
@@ -560,7 +470,6 @@ function refreshWorkspace(state: Runtime): void {
|
|
|
560
470
|
if (serviceState !== state || (info.name === state.workspace.name && info.branch === state.workspace.branch)) return;
|
|
561
471
|
state.workspace = info;
|
|
562
472
|
lobbyTopics.bump("status");
|
|
563
|
-
repaintLobby();
|
|
564
473
|
})
|
|
565
474
|
.catch(() => {})
|
|
566
475
|
.finally(() => {
|
|
@@ -570,6 +479,11 @@ function refreshWorkspace(state: Runtime): void {
|
|
|
570
479
|
|
|
571
480
|
/** The lobby's backend for `state`: every host member that draws nothing. */
|
|
572
481
|
export function createLobbyService(state: Runtime): LobbyService {
|
|
482
|
+
const masterKey = () => {
|
|
483
|
+
const { master } = loadConfig();
|
|
484
|
+
return `${master.model}|${master.thinking}`;
|
|
485
|
+
};
|
|
486
|
+
let appliedMaster = masterKey();
|
|
573
487
|
return {
|
|
574
488
|
sessionId: () => state.ctx.sessionManager.getSessionId(),
|
|
575
489
|
zen: () => {
|
|
@@ -600,8 +514,15 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
600
514
|
planningRounds: () => loadConfig().lobby.maxPlanningRounds,
|
|
601
515
|
config: () => loadConfig(),
|
|
602
516
|
saveConfig: (next) => writeConfig(next),
|
|
603
|
-
configChanged: () =>
|
|
604
|
-
|
|
517
|
+
configChanged: () => {
|
|
518
|
+
lobbyTopics.bump("status");
|
|
519
|
+
// A new master model or effort takes hold in this session at once.
|
|
520
|
+
const next = masterKey();
|
|
521
|
+
if (next === appliedMaster) return;
|
|
522
|
+
appliedMaster = next;
|
|
523
|
+
void applyMasterModel(state.pi, state.ctx, loadConfig()).catch((error: Error) => lobbyNotify(state, `bot-lobby: could not apply the master model — ${error.message}`, "warning"));
|
|
524
|
+
},
|
|
525
|
+
sessionModel: () => sessionModel(state.ctx), models: () => {
|
|
605
526
|
try {
|
|
606
527
|
const scoped = state.ctx.scopedModels;
|
|
607
528
|
const usable = scoped.length > 0 ? scoped.map((entry) => entry.model) : state.ctx.modelRegistry.getAvailable();
|
|
@@ -637,7 +558,6 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
637
558
|
sessionName: () => state.pi.getSessionName(),
|
|
638
559
|
sessions: () => backgroundSessions(),
|
|
639
560
|
startSession: (start) => startSession(state, start),
|
|
640
|
-
answerDialog: (session) => answerDialog(state, session),
|
|
641
561
|
isAuto: (taskId) => autoFor(state, taskId),
|
|
642
562
|
setAuto: (taskId, on) => switchAuto(state, taskId, on),
|
|
643
563
|
sendToTask: (taskId, text) => sendToTask(state, taskId, text),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The shapes of the Tasks tab's rows, shared by the web API and the page's words. */
|
|
2
|
+
|
|
3
|
+
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
4
|
+
|
|
5
|
+
/** How a row is ticked off: still to do (a plan, or a task under way), completed, or abandoned. */
|
|
6
|
+
export type CheckState = "open" | "done" | "dropped";
|
|
7
|
+
|
|
8
|
+
export interface TaskRow {
|
|
9
|
+
/** A task on the list, a saved plan, or a task in the archive. */
|
|
10
|
+
kind: "task" | "plan" | "archived";
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
section: TaskSection;
|
|
14
|
+
/** Task state, or `pending` for a saved plan. */
|
|
15
|
+
status: string;
|
|
16
|
+
paused?: boolean;
|
|
17
|
+
check: CheckState;
|
|
18
|
+
/** Plan steps done, once the task has a plan. */
|
|
19
|
+
progress?: { done: number; total: number };
|
|
20
|
+
/** Who drives it, for tasks other sessions own. */
|
|
21
|
+
owner?: string;
|
|
22
|
+
/** How long ago it finished (finished tasks), was saved (plans) or was archived, as `3h`. */
|
|
23
|
+
age?: string;
|
|
24
|
+
/** The GitHub issue a plan came from. */
|
|
25
|
+
issue?: number;
|
|
26
|
+
/** Auto mode is on: the oracle drives it without asking. */
|
|
27
|
+
auto?: boolean;
|
|
28
|
+
}
|
package/src/pi/commands.ts
CHANGED
|
@@ -19,12 +19,11 @@ import { readFirstExisting } from "../knowledge/store.ts";
|
|
|
19
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
20
20
|
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAsks, waiveQa, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
21
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
22
|
-
import { classifierSummary
|
|
22
|
+
import { classifierSummary } from "./model-settings.ts";
|
|
23
23
|
import { keyStatus } from "../classifier/instance.ts";
|
|
24
24
|
import { kickoff, startPlannedTask } from "./start-task.ts";
|
|
25
25
|
import { startRequest } from "./route.ts";
|
|
26
26
|
import { setAuto, toggleOwnAuto } from "./owner.ts";
|
|
27
|
-
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
28
27
|
import { registerWebServer, webCommand } from "../webui/command.ts";
|
|
29
28
|
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
30
29
|
import { describeRun, runFromLog } from "./run-summary.ts";
|
|
@@ -35,7 +34,7 @@ import type { GitIsolation } from "../schemas/configuration.ts";
|
|
|
35
34
|
import { START_FLAG } from "./start-flags.ts";
|
|
36
35
|
|
|
37
36
|
const HELP = [
|
|
38
|
-
"/bot-lobby Open the lobby: tasks, plan, quick fix, metrics
|
|
37
|
+
"/bot-lobby Open the web lobby in your browser: tasks, plan, quick fix, metrics",
|
|
39
38
|
"/bot-lobby <request> Start a request: a quick fix when one agent can do it alone (the oracle confirms), else a task",
|
|
40
39
|
"/bot-lobby --task [--auto] <request> Always a task (also when the request begins with a subcommand word)",
|
|
41
40
|
"/bot-lobby --budget 90m <request> Start a task with a time budget the oracle divides between its agents",
|
|
@@ -50,14 +49,14 @@ const HELP = [
|
|
|
50
49
|
"/bot-lobby accept [taskId] Accept a task's work as it is, without a QA pass; the oracle then completes it",
|
|
51
50
|
"/bot-lobby knowledge Show persistent knowledge files",
|
|
52
51
|
"/bot-lobby runs [taskId] Recent subagent runs: time, turns, tokens, cost, model",
|
|
53
|
-
"/bot-lobby settings
|
|
52
|
+
"/bot-lobby settings Open the web lobby's Settings: per-agent model, effort and instructions",
|
|
54
53
|
"/bot-lobby config Show effective configuration",
|
|
55
54
|
"/bot-lobby minimize|restore Hide or restore bot-lobby for this session (ctrl+shift+m)",
|
|
56
55
|
"/bot-lobby claim <taskId> Take ownership of an orphaned task",
|
|
57
56
|
"/bot-lobby auto [on|off] Auto mode: the oracle drives this session's task without asking (alt+g)",
|
|
58
57
|
"/bot-lobby start-plan PLAN-… [auto] Start a planned task here; its agreed plan needs no approval",
|
|
59
|
-
"/bot-lobby switch <session.jsonl> Run a saved session in this window (the lobby's session browser uses it
|
|
60
|
-
"/bot-lobby web [stop|link|reset]
|
|
58
|
+
"/bot-lobby switch <session.jsonl> Run a saved session in this window (the web lobby's session browser uses it)",
|
|
59
|
+
"/bot-lobby web [stop|link|reset] Open the browser UI (it starts with pi, on this machine only), or stop it, print its link, reset its link",
|
|
61
60
|
"/bot-lobby help This help",
|
|
62
61
|
].join("\n");
|
|
63
62
|
|
|
@@ -306,7 +305,7 @@ function showConfig(ctx: ExtensionCommandContext): void {
|
|
|
306
305
|
async function switchCommand(ctx: ExtensionCommandContext, path: string): Promise<void> {
|
|
307
306
|
if (!existsSync(path)) return ctx.ui.notify(`bot-lobby: no session file at ${path}`, "warning");
|
|
308
307
|
if (ctx.sessionManager.getSessionFile() === path) return ctx.ui.notify("bot-lobby: this window already runs that session", "info");
|
|
309
|
-
if (!ctx.isIdle()) return ctx.ui.notify("bot-lobby: the oracle is working —
|
|
308
|
+
if (!ctx.isIdle()) return ctx.ui.notify("bot-lobby: the oracle is working — stop it, then switch", "warning");
|
|
310
309
|
const result = await ctx.switchSession(path);
|
|
311
310
|
if (result.cancelled) ctx.ui.notify("bot-lobby: the switch was cancelled", "warning");
|
|
312
311
|
}
|
|
@@ -336,19 +335,14 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
336
335
|
const { sub, rest, restText, auto, budget, budgetError, track, task, isolation } = parseCommand(args ?? "");
|
|
337
336
|
if (budgetError) return ctx.ui.notify(`bot-lobby: ${budgetError}`, "warning");
|
|
338
337
|
if (!sub) {
|
|
339
|
-
if (!restText)
|
|
340
|
-
if (!showLobby()) ctx.ui.notify(HELP, "info");
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
338
|
+
if (!restText) return webCommand(ctx, undefined);
|
|
343
339
|
// A request one agent can do alone may go to the quick-fix agent once the oracle confirms; the rest start as tasks.
|
|
344
|
-
|
|
345
|
-
if (started === "task") autoOpenLobby();
|
|
340
|
+
await startRequest(pi, ctx, configDir, restText, { ...(auto ? { auto } : {}), ...(budget ? { budget } : {}), ...(track ? { track } : {}), ...(task ? { task } : {}), ...(isolation ? { isolation } : {}) });
|
|
346
341
|
return;
|
|
347
342
|
}
|
|
348
343
|
switch (sub) {
|
|
349
344
|
case "lobby":
|
|
350
|
-
|
|
351
|
-
return;
|
|
345
|
+
return webCommand(ctx, undefined);
|
|
352
346
|
case "help":
|
|
353
347
|
return ctx.ui.notify(HELP, "info");
|
|
354
348
|
case "status":
|
|
@@ -376,7 +370,7 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
376
370
|
case "knowledge":
|
|
377
371
|
return showKnowledge(ctx, configDir);
|
|
378
372
|
case "settings":
|
|
379
|
-
return
|
|
373
|
+
return webCommand(ctx, undefined);
|
|
380
374
|
case "minimize":
|
|
381
375
|
setMinimized(true);
|
|
382
376
|
return applyStatus(ctx, detectProjectRoot(ctx.cwd, configDir), configDir);
|
|
@@ -393,8 +387,8 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
393
387
|
return webCommand(ctx, rest[0]);
|
|
394
388
|
case "start-plan": {
|
|
395
389
|
const started = await startPlannedTask(pi, ctx, configDir, rest[0]!, { auto: rest[1] === "auto" });
|
|
396
|
-
if (typeof started === "string")
|
|
397
|
-
return
|
|
390
|
+
if (typeof started === "string") ctx.ui.notify(`bot-lobby: ${started}`, "warning");
|
|
391
|
+
return;
|
|
398
392
|
}
|
|
399
393
|
default:
|
|
400
394
|
return showConfig(ctx);
|
|
@@ -403,7 +397,7 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
403
397
|
});
|
|
404
398
|
|
|
405
399
|
pi.registerCommand("bot-lobby-settings", {
|
|
406
|
-
description: "
|
|
407
|
-
handler: async (_args, ctx) =>
|
|
400
|
+
description: "Open the web lobby's settings: per-agent model, effort, and instructions",
|
|
401
|
+
handler: async (_args, ctx) => webCommand(ctx, undefined),
|
|
408
402
|
});
|
|
409
403
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the settings page writes to the config, in one place: a profile entry
|
|
3
|
+
* patched into a config copy, the master's model and thinking applied to the
|
|
4
|
+
* live session, the subagents pinned to the session model, and the
|
|
5
|
+
* classifier's one-line summary.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
INHERIT_MODEL,
|
|
10
|
+
isThinkingLevel,
|
|
11
|
+
SUBAGENT_KINDS,
|
|
12
|
+
type AgentModelConfig,
|
|
13
|
+
type BotLobbyConfig,
|
|
14
|
+
type SubagentKind,
|
|
15
|
+
} from "../schemas/configuration.ts";
|
|
16
|
+
import { modelLookup } from "./tools.ts";
|
|
17
|
+
import { describeKey, jevEndpoint, type StatusSource } from "../classifier/hosts.ts";
|
|
18
|
+
|
|
19
|
+
/** One settings entry: the master or a subagent profile. */
|
|
20
|
+
export type SettingsKind = "master" | SubagentKind;
|
|
21
|
+
export const SETTINGS_KINDS: readonly SettingsKind[] = ["master", ...SUBAGENT_KINDS];
|
|
22
|
+
|
|
23
|
+
/** The editable fields of one entry; scouts have no thinking and no instructions of their own. */
|
|
24
|
+
export interface EntryView {
|
|
25
|
+
model: string;
|
|
26
|
+
thinking?: string;
|
|
27
|
+
instructions?: string;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
/** The model this agent switches to when its own runs out of usage; unset = none. */
|
|
30
|
+
fallbackModel?: string;
|
|
31
|
+
fallbackThinking?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
35
|
+
if (kind === "master") return config.master;
|
|
36
|
+
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs, ...(config.scout.fallbackModel ? { fallbackModel: config.scout.fallbackModel } : {}) };
|
|
37
|
+
if (kind === "researcher") return config.researcher;
|
|
38
|
+
if (kind === "quickfix") return config.quickFix;
|
|
39
|
+
if (kind === "planner") return config.planner;
|
|
40
|
+
return config.agents[kind];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface EntryPatch {
|
|
44
|
+
model?: string;
|
|
45
|
+
thinking?: string;
|
|
46
|
+
instructions?: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/** `inherit` clears the fallback (its thinking level with it). */
|
|
49
|
+
fallbackModel?: string;
|
|
50
|
+
fallbackThinking?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A settings entry with the fallback patch applied: `inherit` removes the fallback model and its thinking. */
|
|
54
|
+
function withFallbackPatch<T extends { fallbackModel?: string; fallbackThinking?: string }>(entry: T): T {
|
|
55
|
+
if (entry.fallbackModel !== INHERIT_MODEL) return entry;
|
|
56
|
+
const { fallbackModel: _model, fallbackThinking: _thinking, ...rest } = entry;
|
|
57
|
+
return rest as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Apply a patch to one entry in a config copy; scouts ignore thinking and instructions. */
|
|
61
|
+
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
62
|
+
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
63
|
+
if (kind === "master") next.master = withFallbackPatch({ ...config.master, ...patch } as AgentModelConfig);
|
|
64
|
+
else if (kind === "scout") {
|
|
65
|
+
const fallbackModel = patch.fallbackModel ?? config.scout.fallbackModel;
|
|
66
|
+
next.scout = withFallbackPatch({
|
|
67
|
+
model: patch.model ?? config.scout.model,
|
|
68
|
+
timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs,
|
|
69
|
+
...(fallbackModel ? { fallbackModel } : {}),
|
|
70
|
+
});
|
|
71
|
+
} else if (kind === "researcher") next.researcher = withFallbackPatch({ ...config.researcher, ...patch } as AgentModelConfig);
|
|
72
|
+
else if (kind === "quickfix") next.quickFix = withFallbackPatch({ ...config.quickFix, ...patch } as AgentModelConfig);
|
|
73
|
+
else if (kind === "planner") next.planner = withFallbackPatch({ ...config.planner, ...patch } as AgentModelConfig);
|
|
74
|
+
else next.agents[kind] = withFallbackPatch({ ...config.agents[kind], ...patch } as AgentModelConfig);
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Apply the master model/thinking to the live session; "inherit" leaves it alone. */
|
|
79
|
+
export async function applyMasterModel(pi: ExtensionAPI, ctx: ExtensionContext, config: BotLobbyConfig): Promise<void> {
|
|
80
|
+
const { model, thinking } = config.master;
|
|
81
|
+
if (model !== INHERIT_MODEL) {
|
|
82
|
+
const found = modelLookup(ctx)(model);
|
|
83
|
+
if (!found) ctx.ui.notify(`bot-lobby: unknown master model "${model}".`, "warning");
|
|
84
|
+
else if (!(await pi.setModel(found))) ctx.ui.notify(`bot-lobby: no auth for ${model}.`, "warning");
|
|
85
|
+
}
|
|
86
|
+
if (isThinkingLevel(thinking)) pi.setThinkingLevel(thinking);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subagents never inherit silently: any subagent whose model is still unset is
|
|
91
|
+
* pinned to the session's current model and saved, so settings always show
|
|
92
|
+
* what each agent runs on.
|
|
93
|
+
*/
|
|
94
|
+
export function prefillModels(config: BotLobbyConfig, sessionModel: string | undefined): { config: BotLobbyConfig; filled: SubagentKind[] } {
|
|
95
|
+
if (!sessionModel) return { config, filled: [] };
|
|
96
|
+
let next = config;
|
|
97
|
+
const filled: SubagentKind[] = [];
|
|
98
|
+
for (const kind of SUBAGENT_KINDS) {
|
|
99
|
+
if (entryView(next, kind).model !== INHERIT_MODEL) continue;
|
|
100
|
+
next = patchEntry(next, kind, { model: sessionModel });
|
|
101
|
+
filled.push(kind);
|
|
102
|
+
}
|
|
103
|
+
return { config: next, filled };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `on · OpenCode Zen · jev-1.13-free · key stored in pi`, for the settings menu and `/bot-lobby config`. */
|
|
107
|
+
export function classifierSummary(config: BotLobbyConfig, status: StatusSource | undefined): string {
|
|
108
|
+
const { host, model } = jevEndpoint(config.classifier, status);
|
|
109
|
+
const via = config.classifier.provider === "auto" ? `${host.label} (auto)` : host.label;
|
|
110
|
+
return [config.classifier.enabled ? "on" : "off", via, model, `key ${describeKey(host, status)}`].join(" · ");
|
|
111
|
+
}
|
|
112
|
+
|
package/src/pi/start-task.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
|
14
14
|
import { transition } from "../state/task-state.ts";
|
|
15
15
|
import { shortTitle } from "../text.ts";
|
|
16
16
|
import { applyStatus } from "./ui.ts";
|
|
17
|
-
import { applyMasterModel } from "./settings
|
|
17
|
+
import { applyMasterModel } from "./model-settings.ts";
|
|
18
18
|
import { setAutoMode } from "../state/auto.ts";
|
|
19
19
|
import { listPlannedTasks, loadPlannedTask, markPlannedTaskStarted, plannedTaskRequest, type PlannedTask } from "../state/backlog.ts";
|
|
20
20
|
import { triageFor } from "../classifier/instance.ts";
|