@a-t-h-i/bot-lobby 0.6.3 → 0.6.5
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 +180 -17
- package/package.json +12 -6
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/master.md +98 -19
- package/prompts/quickfix.md +16 -3
- package/prompts/researcher.md +8 -2
- package/prompts/reviewer.md +42 -0
- package/prompts/worker.md +7 -0
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- package/src/ask/dialog.ts +167 -0
- package/src/ask/image.ts +202 -0
- package/src/ask/png.ts +179 -0
- package/src/ask/relay.ts +89 -0
- package/src/ask/state.ts +160 -0
- package/src/ask/tool.ts +126 -0
- package/src/ask/types.ts +55 -0
- package/src/ask/view.ts +159 -0
- package/src/classifier/triage.ts +24 -7
- package/src/execution/agent-runner.ts +103 -11
- package/src/execution/git.ts +111 -14
- package/src/execution/pi-runner.ts +164 -11
- package/src/index.ts +9 -0
- package/src/lobby/ask.ts +10 -120
- package/src/lobby/feed.ts +6 -1
- package/src/lobby/layout.ts +27 -8
- package/src/lobby/markdown.ts +36 -6
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +50 -3
- package/src/lobby/runtime.ts +41 -40
- package/src/lobby/tabs/home.ts +112 -48
- package/src/lobby/tabs/issues.ts +4 -3
- package/src/lobby/tabs/plan.ts +4 -4
- package/src/lobby/tabs/quickfix.ts +8 -2
- package/src/lobby/tabs/tasks.ts +16 -5
- package/src/lobby/theme.ts +30 -0
- package/src/lobby/view.ts +36 -5
- package/src/master/decisions.ts +10 -2
- package/src/master/master.ts +41 -4
- package/src/master/research.ts +5 -2
- package/src/pi/commands.ts +108 -13
- package/src/pi/events.ts +48 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/route.ts +180 -0
- package/src/pi/start-task.ts +56 -4
- package/src/pi/tools.ts +32 -9
- package/src/pi/ui.ts +6 -1
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +16 -9
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +28 -2
- package/src/schemas/configuration.ts +15 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +52 -0
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/text.ts +28 -2
- package/src/web/extract.ts +332 -0
- package/src/web/fetch.ts +232 -0
- package/src/web/html.ts +183 -0
- package/src/web/read.ts +113 -0
- package/src/web/search.ts +202 -0
- package/src/web/tools.ts +279 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +734 -46
package/src/lobby/markdown.ts
CHANGED
|
@@ -5,10 +5,37 @@
|
|
|
5
5
|
* text, since the lobby repaints many times a second.
|
|
6
6
|
*/
|
|
7
7
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { Markdown, stripTerminalSequences, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
8
|
+
import { Markdown, stripTerminalSequences, type DefaultTextStyle, type MarkdownOptions, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whose words a piece of Markdown is, which sets how its plain text reads, as
|
|
12
|
+
* pi draws the same things: an agent's reply in the theme's text colour, your
|
|
13
|
+
* own words in yours (list markers and backslash escapes kept as typed), a
|
|
14
|
+
* thought dimmed and in italics.
|
|
15
|
+
*/
|
|
16
|
+
export type MarkdownStyle = "reply" | "you" | "thought";
|
|
17
|
+
|
|
18
|
+
/** The plain-text look of each style but a reply; the lobby takes them from the session's theme. */
|
|
19
|
+
export type MarkdownStyles = Partial<Record<Exclude<MarkdownStyle, "reply">, DefaultTextStyle>>;
|
|
9
20
|
|
|
10
21
|
/** Markdown rendered to lines at a width; `keep: false` for text seen once (a reply still streaming), so it does not crowd the cache. */
|
|
11
|
-
export type MarkdownRenderer = (text: string, width: number, keep?: boolean) => string[];
|
|
22
|
+
export type MarkdownRenderer = (text: string, width: number, keep?: boolean, style?: MarkdownStyle) => string[];
|
|
23
|
+
|
|
24
|
+
/** How pi renders what you write: your ordered-list markers and backslash escapes stay as you typed them. */
|
|
25
|
+
const YOUR_WORDS: MarkdownOptions = { preserveOrderedListMarkers: true, preserveBackslashEscapes: true };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Line breaks a model wrote out as `\n` (text escaped twice on its way here,
|
|
29
|
+
* so a whole plan arrives as one line): made real when the text has no real
|
|
30
|
+
* line break at all, outside inline code, where `\n` is usually meant.
|
|
31
|
+
*/
|
|
32
|
+
export function unescapeBreaks(text: string): string {
|
|
33
|
+
if (text.includes("\n") || !text.includes("\\n")) return text;
|
|
34
|
+
return text
|
|
35
|
+
.split(/(`[^`\n]*`)/)
|
|
36
|
+
.map((part, index) => (index % 2 === 1 ? part : part.replace(/(?:\\r)?\\n/g, "\n")))
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
12
39
|
|
|
13
40
|
/** A heading line as pi renders it: optional styling, then `#`…`######` and a space. */
|
|
14
41
|
const HEADING_HASHES = /^((?:\x1b\[[0-9;]*m)*)#{1,6} ((?:\x1b\[[0-9;]*m)*)/;
|
|
@@ -29,10 +56,10 @@ export const CACHE_LIMIT = 512;
|
|
|
29
56
|
* theme object on every call, so the theme is taken once, here: a theme
|
|
30
57
|
* switch makes a new renderer (the lobby keeps one per pi theme).
|
|
31
58
|
*/
|
|
32
|
-
export function createMarkdownRenderer(theme: MarkdownTheme = getMarkdownTheme()): MarkdownRenderer {
|
|
59
|
+
export function createMarkdownRenderer(theme: MarkdownTheme = getMarkdownTheme(), styles: MarkdownStyles = {}): MarkdownRenderer {
|
|
33
60
|
const cache = new Map<string, string[]>();
|
|
34
|
-
return (text, width, keep = true) => {
|
|
35
|
-
const key = `${width}\0${text}`;
|
|
61
|
+
return (text, width, keep = true, style = "reply") => {
|
|
62
|
+
const key = `${style}\0${width}\0${text}`;
|
|
36
63
|
const hit = cache.get(key);
|
|
37
64
|
if (hit) {
|
|
38
65
|
// Most recently used last, so the oldest is the one dropped.
|
|
@@ -40,7 +67,10 @@ export function createMarkdownRenderer(theme: MarkdownTheme = getMarkdownTheme()
|
|
|
40
67
|
cache.set(key, hit);
|
|
41
68
|
return hit;
|
|
42
69
|
}
|
|
43
|
-
const
|
|
70
|
+
const look = style === "reply" ? undefined : styles[style];
|
|
71
|
+
const lines = new Markdown(unescapeBreaks(text), 0, 0, theme, look, style === "you" ? YOUR_WORDS : undefined)
|
|
72
|
+
.render(Math.max(1, width))
|
|
73
|
+
.map((line) => tidyHeading(line.trimEnd(), theme.bold));
|
|
44
74
|
// Drop the blank lines pi pads the render with at either end.
|
|
45
75
|
while (lines.length > 0 && !lines[0]!.trim()) lines.shift();
|
|
46
76
|
while (lines.length > 0 && !lines.at(-1)!.trim()) lines.pop();
|
package/src/lobby/planner.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { fellShort, profileLabel, type EffortRouter, type EffortScore } from "..
|
|
|
37
37
|
|
|
38
38
|
/** Seats and the oracle read the repository to ask informed questions; they never edit. */
|
|
39
39
|
export const PLANNER_TOOLS: readonly string[] = ["read", "grep", "find", "ls"];
|
|
40
|
-
/** RESEARCH also gets
|
|
40
|
+
/** RESEARCH also gets bot-lobby's web tools (src/web/tools.ts). */
|
|
41
41
|
export const RESEARCH_PANEL_TOOLS: readonly string[] = [...PLANNER_TOOLS, "web_search", "fetch_content", "source_check", "get_search_content"];
|
|
42
42
|
|
|
43
43
|
/** The chair of the panel, as the Plan tab, the feed and the metrics name it. */
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { loadPrompt } from "../prompts/loader.ts";
|
|
|
9
9
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
10
10
|
import { describeToolCall } from "../pi/activity.ts";
|
|
11
11
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
12
|
+
import { appendChange, EditLog } from "../state/changes.ts";
|
|
12
13
|
import type { LobbyFeed } from "./feed.ts";
|
|
13
14
|
import type { FileHinter } from "../classifier/files.ts";
|
|
14
15
|
import type { Classifier } from "../classifier/classifier.ts";
|
|
@@ -51,6 +52,21 @@ export interface QuickFixJob {
|
|
|
51
52
|
/** The classifier routed it down (`trivial 0.88: p/big · low → p/cheap · low`); cleared when it re-ran on the configured profile. */
|
|
52
53
|
route?: string;
|
|
53
54
|
routedFrom?: string;
|
|
55
|
+
/** Files it changed with `edit`/`write`, as shown (relative to the project); the change ledger lets a running task's QA gate tell them from its own. */
|
|
56
|
+
files?: string[];
|
|
57
|
+
/** Runs on this profile instead of the quick-fix settings: a quick feature the oracle routed here gets its builder's model, thinking and time. */
|
|
58
|
+
profile?: QuickFixProfile;
|
|
59
|
+
/** The oracle sent it here from a request typed to the lobby. */
|
|
60
|
+
routed?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** How a job enters the queue besides its prompt. */
|
|
64
|
+
export interface SubmitOptions {
|
|
65
|
+
/** Skip the classifier's "looks like a task" hold (the oracle already routed it here). */
|
|
66
|
+
force?: boolean;
|
|
67
|
+
profile?: QuickFixProfile;
|
|
68
|
+
/** Shown with the job: where it came from. */
|
|
69
|
+
note?: string;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
export interface QuickFixProfile {
|
|
@@ -103,6 +119,7 @@ export class QuickFixQueue {
|
|
|
103
119
|
jobs: QuickFixJob[] = [];
|
|
104
120
|
private readonly deps: QuickFixDeps;
|
|
105
121
|
private readonly controllers = new Map<string, AbortController>();
|
|
122
|
+
private readonly edits = new Map<string, EditLog>();
|
|
106
123
|
private counter = 0;
|
|
107
124
|
|
|
108
125
|
constructor(deps: QuickFixDeps) {
|
|
@@ -114,10 +131,21 @@ export class QuickFixQueue {
|
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
/** Queue a quick fix; it starts at once when nothing else is running. */
|
|
117
|
-
submit(prompt: string, now = Date.now()): QuickFixJob {
|
|
134
|
+
submit(prompt: string, now = Date.now(), options: SubmitOptions = {}): QuickFixJob {
|
|
118
135
|
const text = prompt.trim();
|
|
119
136
|
if (!text) throw new Error("a quick fix needs a prompt");
|
|
120
|
-
const job: QuickFixJob = {
|
|
137
|
+
const job: QuickFixJob = {
|
|
138
|
+
id: `QF-${++this.counter}`,
|
|
139
|
+
prompt: text,
|
|
140
|
+
status: "queued",
|
|
141
|
+
createdAt: now,
|
|
142
|
+
steps: [],
|
|
143
|
+
tools: 0,
|
|
144
|
+
turns: 0,
|
|
145
|
+
...(options.force ? { force: true, routed: true } : {}),
|
|
146
|
+
...(options.profile ? { profile: options.profile } : {}),
|
|
147
|
+
...(options.note ? { note: options.note } : {}),
|
|
148
|
+
};
|
|
121
149
|
this.jobs = [...this.jobs, job].filter((entry, index, all) => isActive(entry) || index >= all.length - MAX_JOBS);
|
|
122
150
|
this.changed();
|
|
123
151
|
this.pump();
|
|
@@ -188,6 +216,8 @@ export class QuickFixQueue {
|
|
|
188
216
|
const now = Date.now();
|
|
189
217
|
if (event.type === "tool_execution_start") {
|
|
190
218
|
job.tools += 1;
|
|
219
|
+
const edits = this.edits.get(job.id);
|
|
220
|
+
if (edits?.note(event.toolName, event.args)) job.files = edits.shown();
|
|
191
221
|
this.addStep(job, describeToolCall(event.toolName, event.args), now);
|
|
192
222
|
} else if (event.type === "turn_start") job.turns += 1;
|
|
193
223
|
else if (event.type === "thought") this.deps.feed?.thought(QUICK_FIX_SOURCE, event.text, now);
|
|
@@ -197,7 +227,7 @@ export class QuickFixQueue {
|
|
|
197
227
|
}
|
|
198
228
|
|
|
199
229
|
private async run(job: QuickFixJob): Promise<void> {
|
|
200
|
-
const profile = this.deps.profile();
|
|
230
|
+
const profile = job.profile ?? this.deps.profile();
|
|
201
231
|
const controller = new AbortController();
|
|
202
232
|
this.controllers.set(job.id, controller);
|
|
203
233
|
job.status = "running";
|
|
@@ -211,6 +241,7 @@ export class QuickFixQueue {
|
|
|
211
241
|
return this.hold(job, held);
|
|
212
242
|
}
|
|
213
243
|
this.deps.feed?.log(QUICK_FIX_SOURCE, `started: ${jobTitle(job)}`, "info", job.startedAt);
|
|
244
|
+
this.edits.set(job.id, new EditLog(this.deps.cwd));
|
|
214
245
|
try {
|
|
215
246
|
const [likely, route] = await Promise.all([this.likely(job.prompt, controller.signal), this.route(job.prompt, profile, controller.signal)]);
|
|
216
247
|
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
@@ -280,6 +311,8 @@ export class QuickFixQueue {
|
|
|
280
311
|
try {
|
|
281
312
|
const sized = await quickFixSize(jev, job.prompt, signal);
|
|
282
313
|
if (!sized || sized.size !== "large" || sized.confidence < jev.config.thresholds.quickFixLargeAt) return undefined;
|
|
314
|
+
// Large but still one engineer's work (a rich page in one file): it runs.
|
|
315
|
+
if ((sized.solo ?? 0) >= jev.config.thresholds.quickFixAt) return undefined;
|
|
283
316
|
return `looks like a task (large, ${sized.confidence.toFixed(2)})`;
|
|
284
317
|
} catch {
|
|
285
318
|
return undefined;
|
|
@@ -309,6 +342,20 @@ export class QuickFixQueue {
|
|
|
309
342
|
private finish(job: QuickFixJob): void {
|
|
310
343
|
job.finishedAt = Date.now();
|
|
311
344
|
for (const step of job.steps) step.pending = false;
|
|
345
|
+
const edited = this.edits.get(job.id)?.list() ?? [];
|
|
346
|
+
this.edits.delete(job.id);
|
|
347
|
+
// Whatever it edited, even when it failed, is on record for a task's QA gate as the user's own request.
|
|
348
|
+
if (edited.length > 0) {
|
|
349
|
+
appendChange(this.deps.root, this.deps.configDir, {
|
|
350
|
+
source: "quickfix",
|
|
351
|
+
id: job.id,
|
|
352
|
+
what: jobTitle(job),
|
|
353
|
+
files: edited,
|
|
354
|
+
startedAt: new Date(job.startedAt ?? job.createdAt).toISOString(),
|
|
355
|
+
finishedAt: new Date(job.finishedAt).toISOString(),
|
|
356
|
+
status: job.status,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
312
359
|
this.deps.feed?.end(job.id, job.status !== "success");
|
|
313
360
|
const ok = job.status === "success";
|
|
314
361
|
const outcome = ok ? "done" : `${job.status}${job.error ? ` — ${job.error.split("\n")[0]}` : ""}`;
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* turn into the feed, passes plan comments to the Master that owns a task and
|
|
7
7
|
* records each Master turn in the metrics log.
|
|
8
8
|
*/
|
|
9
|
-
import type { ExtensionAPI, ExtensionContext
|
|
10
|
-
import {
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { getSelectListTheme, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Key, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
|
|
12
12
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
13
13
|
import { loadTask, peekTasks } from "../state/persistence.ts";
|
|
@@ -26,22 +26,24 @@ import { shortTitle } from "../text.ts";
|
|
|
26
26
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
27
27
|
import { modelRef, resolveLobbyProfile, resolvePanelProfile } from "../pi/model-support.ts";
|
|
28
28
|
import { modelLookup } from "../pi/tools.ts";
|
|
29
|
-
import { startPlannedTask
|
|
29
|
+
import { startPlannedTask } from "../pi/start-task.ts";
|
|
30
|
+
import { pendingRequest, setQuickFixHandoff, startRequest } from "../pi/route.ts";
|
|
31
|
+
import type { Domain } from "../schemas/agent.ts";
|
|
30
32
|
import type { LobbyAgentKind, LobbyPanel, PanelMember } from "../schemas/configuration.ts";
|
|
31
33
|
import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike, type ChatEntry } from "./feed.ts";
|
|
32
34
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
33
35
|
import { checkThinking } from "../pi/model-support.ts";
|
|
34
36
|
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
35
37
|
import { SessionChats } from "./session-files.ts";
|
|
36
|
-
import { answerMessage,
|
|
38
|
+
import { answerMessage, askUser, questionnaires, type Asker } from "./ask.ts";
|
|
37
39
|
import { QuickFixQueue } from "./quickfix.ts";
|
|
38
40
|
import { PlanningSession, type PlannerSeed } from "./planner.ts";
|
|
39
41
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
40
42
|
import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type TabId } from "./view.ts";
|
|
41
|
-
import
|
|
42
|
-
import { createMarkdownRenderer } from "./markdown.ts";
|
|
43
|
+
import { lobbyTheme } from "./theme.ts";
|
|
43
44
|
import { deliverComments, onOwnerEvent, setAuto } from "../pi/owner.ts";
|
|
44
45
|
import { openEntrySettings, openSettings } from "../pi/settings-ui.ts";
|
|
46
|
+
import { budgetClock } from "../state/budget.ts";
|
|
45
47
|
|
|
46
48
|
export const ANCHOR_KEY = "bot-lobby-anchor";
|
|
47
49
|
export { deliverComments };
|
|
@@ -62,8 +64,8 @@ interface Runtime {
|
|
|
62
64
|
planner?: PlanningSession;
|
|
63
65
|
issues: IssuesState;
|
|
64
66
|
unsubscribeFeed?: () => void;
|
|
65
|
-
/**
|
|
66
|
-
asker?:
|
|
67
|
+
/** Puts the panel's questions to the user: the questionnaire unless a test sets another. */
|
|
68
|
+
asker?: Asker;
|
|
67
69
|
/** A questionnaire is on screen. */
|
|
68
70
|
asking: boolean;
|
|
69
71
|
/** The lobby turned the terminal's mouse reporting on (pi's regular screen only). */
|
|
@@ -149,9 +151,10 @@ function toOracle(state: Runtime, text: string): string | undefined {
|
|
|
149
151
|
state.pi.sendUserMessage(text, { expandPromptTemplates: true, ...(busy ? { deliverAs: "followUp" as const } : {}) });
|
|
150
152
|
return `sent ${text.split(/\s+/)[0]} to pi (built-in commands need the lobby hidden: alt+l)`;
|
|
151
153
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
// While the oracle decides where a request goes, what the user types joins that conversation.
|
|
155
|
+
if (!currentZenTask() && !pendingRequest()) {
|
|
156
|
+
startRequest(state.pi, state.ctx, state.configDir, text).catch((error: Error) => failed(state, "could not start the task", error));
|
|
157
|
+
return "reading your request — a quick fix or a task, the oracle takes it from here";
|
|
155
158
|
}
|
|
156
159
|
state.pi.sendUserMessage(text, busy ? { deliverAs: "steer" } : undefined);
|
|
157
160
|
return undefined;
|
|
@@ -425,7 +428,9 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
425
428
|
|
|
426
429
|
/** A task's status box without animations, for a session other than this window's. */
|
|
427
430
|
function taskScene(state: Runtime, task: Task, width: number, height: number): string[] {
|
|
428
|
-
|
|
431
|
+
const now = Date.now();
|
|
432
|
+
const time = budgetClock(state.root, state.configDir, task.id, now);
|
|
433
|
+
return panelLines(task, persistedRuns(task), now, false, { width, rows: Math.floor(height / 0.75), still: true, theme: state.ctx.ui.theme, ...(time ? { time } : {}) });
|
|
429
434
|
}
|
|
430
435
|
|
|
431
436
|
function seatProfile(state: Runtime, member: PanelMember) {
|
|
@@ -463,10 +468,9 @@ function newPlanner(state: Runtime, seed?: PlannerSeed, seats?: readonly PanelMe
|
|
|
463
468
|
return state.planner;
|
|
464
469
|
}
|
|
465
470
|
|
|
466
|
-
/** The
|
|
467
|
-
function panelAsker(state: Runtime):
|
|
468
|
-
state.asker
|
|
469
|
-
return state.asker;
|
|
471
|
+
/** The questionnaire (pi's own dialogs where it cannot be drawn); tests swap it. */
|
|
472
|
+
function panelAsker(state: Runtime): Asker {
|
|
473
|
+
return state.asker ?? askUser;
|
|
470
474
|
}
|
|
471
475
|
|
|
472
476
|
/**
|
|
@@ -483,7 +487,7 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
483
487
|
if (state.asking) return "the questions are already open";
|
|
484
488
|
state.asking = true;
|
|
485
489
|
try {
|
|
486
|
-
const asker =
|
|
490
|
+
const asker = panelAsker(state);
|
|
487
491
|
const chunks = questionnaires(session.questions);
|
|
488
492
|
for (let index = session.answered.length; index < chunks.length; index++) {
|
|
489
493
|
const result = await asker(chunks[index]!, state.ctx);
|
|
@@ -508,29 +512,6 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
|
|
|
508
512
|
}
|
|
509
513
|
}
|
|
510
514
|
|
|
511
|
-
const lobbyThemes = new WeakMap<Theme, LobbyTheme>();
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* pi's theme as the lobby draws with it, plus Markdown rendering; one wrapper
|
|
515
|
-
* (and one Markdown renderer with its cache) per theme, so caches stay warm
|
|
516
|
-
* and a theme switch starts fresh.
|
|
517
|
-
*/
|
|
518
|
-
function lobbyTheme(theme: Theme): LobbyTheme {
|
|
519
|
-
let wrapped = lobbyThemes.get(theme);
|
|
520
|
-
if (!wrapped) {
|
|
521
|
-
wrapped = {
|
|
522
|
-
fg: (color, text) => theme.fg(color, text),
|
|
523
|
-
bold: (text) => theme.bold(text),
|
|
524
|
-
italic: (text) => theme.italic(text),
|
|
525
|
-
bg: (color, text) => theme.bg(color, text),
|
|
526
|
-
markdown: createMarkdownRenderer(getMarkdownTheme()),
|
|
527
|
-
strike: (text) => theme.strikethrough(text),
|
|
528
|
-
};
|
|
529
|
-
lobbyThemes.set(theme, wrapped);
|
|
530
|
-
}
|
|
531
|
-
return wrapped;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
515
|
function host(state: Runtime, tui: TUI): LobbyHost {
|
|
535
516
|
return {
|
|
536
517
|
rows: () => tui.terminal.rows,
|
|
@@ -728,6 +709,7 @@ function shutdown(): void {
|
|
|
728
709
|
const state = runtime;
|
|
729
710
|
if (!state) return;
|
|
730
711
|
runtime = undefined;
|
|
712
|
+
setQuickFixHandoff(undefined);
|
|
731
713
|
setMouse(state, false);
|
|
732
714
|
state.unsubscribeFeed?.();
|
|
733
715
|
state.quickfix.cancelAll();
|
|
@@ -739,6 +721,24 @@ function shutdown(): void {
|
|
|
739
721
|
onMinimizeChange(undefined);
|
|
740
722
|
}
|
|
741
723
|
|
|
724
|
+
/**
|
|
725
|
+
* The oracle routed a request to the quick-fix agent: queue it (no "looks like
|
|
726
|
+
* a task" hold, the oracle already decided), on its builder's settings when it
|
|
727
|
+
* is a quick feature, and show it on the Quick fix tab.
|
|
728
|
+
*/
|
|
729
|
+
function handToQuickFix(state: Runtime, request: string, builder: Domain | undefined, reason: string): string {
|
|
730
|
+
const quick = lobbyProfile(state, "quickfix");
|
|
731
|
+
const profile = builder ? { ...seatProfile(state, builder), instructions: quick.instructions } : undefined;
|
|
732
|
+
const job = state.quickfix.submit(request, Date.now(), {
|
|
733
|
+
force: true,
|
|
734
|
+
...(profile ? { profile } : {}),
|
|
735
|
+
note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and time limit.` : ""}`,
|
|
736
|
+
});
|
|
737
|
+
showLobby("quickfix");
|
|
738
|
+
state.view?.showQuickFix(job.id);
|
|
739
|
+
return job.id;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
742
|
/** Create the session's lobby runtime (interactive master sessions only). */
|
|
743
743
|
export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string): void {
|
|
744
744
|
shutdown();
|
|
@@ -776,6 +776,7 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
776
776
|
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
777
777
|
});
|
|
778
778
|
runtime = state;
|
|
779
|
+
setQuickFixHandoff((request, builder, reason) => (runtime === state ? handToQuickFix(state, request, builder, reason) : undefined));
|
|
779
780
|
lobbyFeed.clear();
|
|
780
781
|
// Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
|
|
781
782
|
lobbyFeed.seedChat(chatFromEntries(ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY));
|