@a-t-h-i/bot-lobby 0.6.5 → 0.6.7
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 +127 -6
- package/package.json +1 -1
- package/prompts/global.md +18 -0
- package/prompts/master.md +77 -2
- package/prompts/worker.md +7 -2
- package/src/ask/dialog.ts +15 -3
- package/src/ask/state.ts +10 -1
- package/src/ask/tool.ts +6 -1
- package/src/ask/view.ts +2 -1
- package/src/execution/agent-runner.ts +23 -1
- package/src/execution/fallback.ts +75 -0
- package/src/index.ts +1 -1
- package/src/lobby/feed.ts +8 -1
- package/src/lobby/keys.ts +0 -1
- package/src/lobby/layout.ts +22 -1
- package/src/lobby/mini.ts +159 -0
- package/src/lobby/planner.ts +10 -3
- package/src/lobby/quickfix.ts +17 -3
- package/src/lobby/runtime.ts +29 -26
- package/src/lobby/tabs/home.ts +6 -42
- package/src/lobby/tabs/tasks.ts +1 -1
- package/src/lobby/view.ts +31 -30
- package/src/master/master.ts +1 -1
- package/src/master/research.ts +1 -0
- package/src/pi/activity.ts +1 -33
- package/src/pi/events.ts +3 -16
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +18 -3
- package/src/pi/plan-checklist.ts +305 -0
- package/src/pi/run-summary.ts +12 -3
- package/src/pi/settings-ui.ts +65 -12
- package/src/pi/tools.ts +5 -4
- package/src/pi/ui.ts +18 -284
- package/src/roles/worker.ts +1 -1
- package/src/schemas/configuration.ts +51 -15
- package/src/schemas/findings.ts +2 -0
- package/src/workflow/brief.ts +59 -0
- package/src/workflow/workflow.ts +15 -2
- package/src/pi/expressions.ts +0 -169
- package/src/pi/kaomoji.ts +0 -227
- package/src/pi/mascot-art.ts +0 -359
- package/src/pi/zen-large.ts +0 -699
- package/src/pi/zen-metrics.ts +0 -130
- package/src/pi/zen.ts +0 -659
package/src/lobby/feed.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* What the lobby narrates: an activity log of plain-words tool steps from the
|
|
3
3
|
* Master and every subagent, a single place for thoughts, and the conversation
|
|
4
4
|
* with the Master stripped of tool rows and thinking. Bounded ring buffers in
|
|
5
|
-
* one module-level store (like the
|
|
5
|
+
* one module-level store (like the task state in ui.ts), so every event source can
|
|
6
6
|
* write without a reference to the lobby, and the lobby repaints on `version`.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
@@ -70,6 +70,8 @@ export class LobbyFeed {
|
|
|
70
70
|
private readonly runSteps = new Map<string, string>();
|
|
71
71
|
private readonly runThoughts = new Map<string, string>();
|
|
72
72
|
private readonly runStatus = new Map<string, AgentRun["status"]>();
|
|
73
|
+
/** Runs already reported as switched to their fallback model. */
|
|
74
|
+
private readonly fellBack = new Set<string>();
|
|
73
75
|
|
|
74
76
|
onChange(listener: () => void): () => void {
|
|
75
77
|
this.listeners.add(listener);
|
|
@@ -200,6 +202,10 @@ export class LobbyFeed {
|
|
|
200
202
|
const before = this.runStatus.get(key);
|
|
201
203
|
this.runStatus.set(key, run.status);
|
|
202
204
|
if (before === undefined) this.log(source, `started as ${run.role}`, "info", at);
|
|
205
|
+
if (run.fellBackFrom && !this.fellBack.has(key)) {
|
|
206
|
+
this.fellBack.add(key);
|
|
207
|
+
this.log(source, `${run.fellBackFrom} is out of usage or unavailable: running on the fallback model`, "warning", at);
|
|
208
|
+
}
|
|
203
209
|
if (run.step && this.runSteps.get(key) !== run.step) {
|
|
204
210
|
this.runSteps.set(key, run.step);
|
|
205
211
|
this.step(source, run.step, key, at);
|
|
@@ -225,6 +231,7 @@ export class LobbyFeed {
|
|
|
225
231
|
this.runSteps.clear();
|
|
226
232
|
this.runThoughts.clear();
|
|
227
233
|
this.runStatus.clear();
|
|
234
|
+
this.fellBack.clear();
|
|
228
235
|
this.touch();
|
|
229
236
|
}
|
|
230
237
|
}
|
package/src/lobby/keys.ts
CHANGED
|
@@ -17,7 +17,6 @@ export const LOBBY_ACTIONS = {
|
|
|
17
17
|
toggleAuto: { key: "alt+g", help: "auto mode: the oracle drives the task to completion without asking" },
|
|
18
18
|
nextTab: { key: "tab", help: "next tab" },
|
|
19
19
|
prevTab: { key: "shift+tab", help: "previous tab" },
|
|
20
|
-
toggleScene: { key: "alt+z", help: "show or hide the oracle and agent animations" },
|
|
21
20
|
toggleConversation: { key: "alt+c", help: "show or hide the conversation" },
|
|
22
21
|
toggleActivity: { key: "alt+a", help: "show or hide the activity log" },
|
|
23
22
|
toggleThinking: { key: "alt+k", help: "show or hide thinking" },
|
package/src/lobby/layout.ts
CHANGED
|
@@ -268,6 +268,24 @@ export function scrollThumb(total: number, rows: number, start: number): { from:
|
|
|
268
268
|
return { from, to: from + size };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/** Page buttons in the bottom border of a pane that scrolls: two pages up, one up, one down, two down. */
|
|
272
|
+
export const PAGER = "▲▲ ▲ ▼ ▼▼";
|
|
273
|
+
/** Columns the pager takes with a space on each side. */
|
|
274
|
+
export const PAGER_WIDTH = PAGER.length + 2;
|
|
275
|
+
/** Narrowest box that has room for the pager beside its corners. */
|
|
276
|
+
const PAGER_MIN_WIDTH = PAGER_WIDTH + 6;
|
|
277
|
+
|
|
278
|
+
/** Which page button a column of a scrollable pane's bottom border is: -2, -1, 1 or 2 pages (negative up), or undefined. */
|
|
279
|
+
export function pagerButton(width: number, column: number): -2 | -1 | 1 | 2 | undefined {
|
|
280
|
+
if (width < PAGER_MIN_WIDTH) return undefined;
|
|
281
|
+
const from = column - width;
|
|
282
|
+
if (from >= -12 && from <= -10) return -2;
|
|
283
|
+
if (from >= -9 && from <= -7) return -1;
|
|
284
|
+
if (from >= -6 && from <= -5) return 1;
|
|
285
|
+
if (from >= -4 && from <= -2) return 2;
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
271
289
|
/**
|
|
272
290
|
* A rounded panel exactly `width` × `height`: the title set into the top
|
|
273
291
|
* border, `content` inside with one column of padding, extra lines cut and
|
|
@@ -291,7 +309,10 @@ export function box(width: number, height: number, content: readonly string[], o
|
|
|
291
309
|
const thumb = options.scroll ? scrollThumb(options.scroll.total, height - 2, options.scroll.start) : undefined;
|
|
292
310
|
const rightEdge = (row: number) => (thumb && row >= thumb.from && row < thumb.to ? paint(theme, focused ? "accent" : "muted", "┃") : edge("│"));
|
|
293
311
|
const rows = fill(content, height - 2).map((line, row) => `${edge("│")} ${fit(line, inner)} ${rightEdge(row)}`);
|
|
294
|
-
|
|
312
|
+
// A pane with more lines than rows carries page buttons in its bottom border; `pagerButton` finds them again for clicks.
|
|
313
|
+
const pager = thumb && width >= PAGER_MIN_WIDTH ? ` ${PAGER} ` : "";
|
|
314
|
+
const bottom = `${edge("╰")}${edge("─".repeat(width - 2 - textWidth(pager)))}${pager ? paint(theme, focused ? "accent" : "muted", pager) : ""}${edge("╯")}`;
|
|
315
|
+
return [top, ...rows, bottom];
|
|
295
316
|
}
|
|
296
317
|
|
|
297
318
|
/** Lay boxes out side by side, each already exactly its width and the same height. */
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line status shown under pi's editor while the lobby is hidden: a
|
|
3
|
+
* small graphic of where the work stands — the task's plan steps (or its stage
|
|
4
|
+
* before there is a plan), the planning rounds, the quick fix in hand — or a
|
|
5
|
+
* quiet "idle". Pure: everything it shows arrives as input.
|
|
6
|
+
*/
|
|
7
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { AgentRun } from "../schemas/findings.ts";
|
|
9
|
+
import type { Task, TaskState } from "../schemas/task.ts";
|
|
10
|
+
import { planChecklist } from "../pi/plan-checklist.ts";
|
|
11
|
+
import { agentName } from "../pi/run-summary.ts";
|
|
12
|
+
import { paint, type LobbyTheme } from "./layout.ts";
|
|
13
|
+
|
|
14
|
+
export interface MiniPlanning {
|
|
15
|
+
/** A round is running. */
|
|
16
|
+
busy: boolean;
|
|
17
|
+
/** The round in hand, and the limit (0 = unlimited). */
|
|
18
|
+
round: number;
|
|
19
|
+
limit: number;
|
|
20
|
+
/** Questions waiting for the user. */
|
|
21
|
+
questions: number;
|
|
22
|
+
/** The oracle's draft is ready to save. */
|
|
23
|
+
ready: boolean;
|
|
24
|
+
/** The plan was saved as a pending task. */
|
|
25
|
+
saved: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MiniQuickFix {
|
|
29
|
+
title: string;
|
|
30
|
+
running: boolean;
|
|
31
|
+
queued: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MiniInput {
|
|
35
|
+
task?: Task;
|
|
36
|
+
runs: readonly AgentRun[];
|
|
37
|
+
planning?: MiniPlanning;
|
|
38
|
+
quickfix?: MiniQuickFix;
|
|
39
|
+
/** The key that opens the lobby, as shown. */
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A full-workflow task's stages, in order, for the strip shown before its plan has steps. */
|
|
44
|
+
const STAGES: readonly TaskState[] = ["clarifying", "scouting", "synthesizing", "awaiting_approval", "planning", "implementing", "reviewing"];
|
|
45
|
+
|
|
46
|
+
/** Widest step bar; longer plans share its cells. */
|
|
47
|
+
const MAX_CELLS = 12;
|
|
48
|
+
|
|
49
|
+
/** `■■◐□□`: the plan's steps done, current and to do, squeezed into at most `MAX_CELLS` cells. */
|
|
50
|
+
export function stepBar(statuses: readonly ("done" | "current" | "pending")[]): string {
|
|
51
|
+
if (statuses.length === 0) return "";
|
|
52
|
+
const cells = Math.min(statuses.length, MAX_CELLS);
|
|
53
|
+
const out: string[] = [];
|
|
54
|
+
for (let cell = 0; cell < cells; cell += 1) {
|
|
55
|
+
const from = Math.floor((cell * statuses.length) / cells);
|
|
56
|
+
const to = Math.max(from + 1, Math.floor(((cell + 1) * statuses.length) / cells));
|
|
57
|
+
const slice = statuses.slice(from, to);
|
|
58
|
+
out.push(slice.every((status) => status === "done") ? "■" : slice.some((status) => status === "current") ? "◐" : slice.some((status) => status === "done") ? "◐" : "□");
|
|
59
|
+
}
|
|
60
|
+
return out.join("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `●●◐○○○○`: how far along the workflow a task without a plan yet is. */
|
|
64
|
+
export function stageBar(state: TaskState): string {
|
|
65
|
+
const at = STAGES.indexOf(state);
|
|
66
|
+
if (at < 0) return "";
|
|
67
|
+
return STAGES.map((_stage, index) => (index < at ? "●" : index === at ? "◐" : "○")).join("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `■■□□□` for `round` of `limit`, or empty without a limit. */
|
|
71
|
+
function roundBar(round: number, limit: number): string {
|
|
72
|
+
if (limit <= 0) return "";
|
|
73
|
+
const done = Math.min(limit, Math.max(0, round));
|
|
74
|
+
return "■".repeat(done) + "□".repeat(limit - done);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const STATE_WORDS: Partial<Record<TaskState, string>> = {
|
|
78
|
+
awaiting_approval: "awaiting your approval",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function taskSegment(input: MiniInput, theme?: LobbyTheme): string | undefined {
|
|
82
|
+
const task = input.task;
|
|
83
|
+
if (!task || task.state === "completed" || task.state === "abandoned") return undefined;
|
|
84
|
+
const steps = task.plan ? planChecklist(task.plan, input.runs) : [];
|
|
85
|
+
const done = steps.filter((step) => step.status === "done").length;
|
|
86
|
+
const bar = steps.length > 0 ? `${stepBar(steps.map((step) => step.status))} ${done}/${steps.length}` : stageBar(task.state);
|
|
87
|
+
const state = task.paused ? `${task.state} (paused)` : STATE_WORDS[task.state] ?? task.state;
|
|
88
|
+
const running = [...input.runs].reverse().find((run) => run.status === "running");
|
|
89
|
+
const doing = running ? ` · ${agentName(running)}${running.activity ? ` ${running.activity}` : ""}` : "";
|
|
90
|
+
const alert = task.state === "awaiting_approval" || task.paused;
|
|
91
|
+
return `${paint(theme, "accent", task.id)} ${paint(theme, "success", bar)} ${paint(theme, alert ? "warning" : "muted", state)}${paint(theme, "dim", doing)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function planningSegment(planning: MiniPlanning | undefined, theme?: LobbyTheme): string | undefined {
|
|
95
|
+
if (!planning || planning.saved) return undefined;
|
|
96
|
+
const rounds = roundBar(planning.round, planning.limit);
|
|
97
|
+
const head = `${paint(theme, "accent", "planning")} ${paint(theme, "success", rounds || `round ${planning.round}`)}${rounds ? paint(theme, "muted", ` ${planning.round}/${planning.limit}`) : ""}`;
|
|
98
|
+
if (planning.questions > 0) return `${head} ${paint(theme, "warning", `● ${planning.questions} question${planning.questions === 1 ? "" : "s"} for you`)}`;
|
|
99
|
+
if (planning.busy) return `${head} ${paint(theme, "muted", "the panel is thinking")}`;
|
|
100
|
+
if (planning.ready) return `${head} ${paint(theme, "success", "plan ready to save")}`;
|
|
101
|
+
return head;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function quickFixSegment(quickfix: MiniQuickFix | undefined, theme?: LobbyTheme): string | undefined {
|
|
105
|
+
if (!quickfix) return undefined;
|
|
106
|
+
const queued = quickfix.queued > 0 ? ` +${quickfix.queued} queued` : "";
|
|
107
|
+
return `${paint(theme, "accent", "quick fix")} ${paint(theme, "success", quickfix.running ? "◐" : "○")} ${paint(theme, "muted", quickfix.title)}${paint(theme, "dim", queued)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The status line, at most `width` columns. */
|
|
111
|
+
export function miniLine(input: MiniInput, width: number, theme?: LobbyTheme): string {
|
|
112
|
+
if (width < 12) return "";
|
|
113
|
+
const segments = [taskSegment(input, theme), planningSegment(input.planning, theme), quickFixSegment(input.quickfix, theme)].filter((segment): segment is string => Boolean(segment));
|
|
114
|
+
const body = segments.length > 0 ? segments.join(paint(theme, "dim", " │ ")) : paint(theme, "dim", "idle");
|
|
115
|
+
const hint = paint(theme, "dim", ` ${input.key} opens`);
|
|
116
|
+
const line = ` ${paint(theme, "accent", "◆ bot-lobby")} ${body}`;
|
|
117
|
+
return truncateToWidth(`${line}${hint}`, width, "…");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** An agent at work right now, for the lobby's bottom-line indicator. */
|
|
121
|
+
export interface WorkingAgent {
|
|
122
|
+
name: string;
|
|
123
|
+
activity?: string;
|
|
124
|
+
/** Epoch ms it started, for its elapsed time. */
|
|
125
|
+
since?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The subagents running now: task runs still going, and the quick fix in hand. */
|
|
129
|
+
export function workingAgents(runs: readonly AgentRun[], quickFix?: { startedAt?: number }): WorkingAgent[] {
|
|
130
|
+
const agents: WorkingAgent[] = [];
|
|
131
|
+
for (const run of runs) {
|
|
132
|
+
if (run.status !== "running") continue;
|
|
133
|
+
const since = Date.parse(run.startedAt);
|
|
134
|
+
agents.push({ name: agentName(run), ...(run.activity ? { activity: run.activity } : {}), ...(Number.isFinite(since) ? { since } : {}) });
|
|
135
|
+
}
|
|
136
|
+
if (quickFix) agents.push({ name: "QUICK FIX", ...(quickFix.startedAt ? { since: quickFix.startedAt } : {}) });
|
|
137
|
+
return agents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function elapsed(ms: number): string {
|
|
141
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
142
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `◐ DESIGN editing 2m · DEV running 40s`, at most `width` columns: it gives up
|
|
147
|
+
* the activity and time, then the names, then shows only a count, and nothing
|
|
148
|
+
* when even that does not fit or nothing is working. `spin` is the spinner glyph.
|
|
149
|
+
*/
|
|
150
|
+
export function agentsIndicator(agents: readonly WorkingAgent[], now: number, spin: string, width: number, theme?: LobbyTheme): string {
|
|
151
|
+
if (agents.length === 0) return "";
|
|
152
|
+
const full = agents.map((agent) => `${agent.name}${agent.activity ? ` ${agent.activity}` : ""}${agent.since ? ` ${elapsed(now - agent.since)}` : ""}`).join(" · ");
|
|
153
|
+
const names = [...new Set(agents.map((agent) => agent.name))].join(" · ");
|
|
154
|
+
const count = `${agents.length} agent${agents.length === 1 ? "" : "s"} working`;
|
|
155
|
+
for (const text of [full, names, count]) {
|
|
156
|
+
if (text.length + 2 <= width) return `${paint(theme, "accent", spin)} ${paint(theme, "muted", text)}`;
|
|
157
|
+
}
|
|
158
|
+
return "";
|
|
159
|
+
}
|
package/src/lobby/planner.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
22
22
|
import { compilePrompt } from "../prompts/compiler.ts";
|
|
23
|
+
import { withFallback } from "../execution/fallback.ts";
|
|
23
24
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
24
25
|
import { describeToolCall } from "../pi/activity.ts";
|
|
25
26
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
@@ -593,12 +594,12 @@ export class PlanningSession {
|
|
|
593
594
|
const startedAt = Date.now();
|
|
594
595
|
let outcome: RunOutcome;
|
|
595
596
|
try {
|
|
596
|
-
const
|
|
597
|
+
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
597
598
|
{
|
|
598
599
|
cwd: this.deps.cwd,
|
|
599
600
|
...request,
|
|
600
|
-
model
|
|
601
|
-
thinking
|
|
601
|
+
model,
|
|
602
|
+
thinking,
|
|
602
603
|
timeoutMs: profile.timeoutMs,
|
|
603
604
|
signal,
|
|
604
605
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
|
@@ -607,6 +608,12 @@ export class PlanningSession {
|
|
|
607
608
|
},
|
|
608
609
|
this.deps.runProcess ?? spawnPiProcess,
|
|
609
610
|
);
|
|
611
|
+
// A model that is out of usage hands the turn to the fallback the settings name.
|
|
612
|
+
const { result, switchedFrom } = await withFallback(profile.model, profile.thinking, profile.fallback, attempt);
|
|
613
|
+
if (switchedFrom && profile.fallback) {
|
|
614
|
+
this.deps.feed?.log(label, `${switchedFrom} is out of usage or unavailable; ran on ${profile.fallback.model}`, "warning");
|
|
615
|
+
profile = { ...profile, model: profile.fallback.model, thinking: profile.fallback.thinking };
|
|
616
|
+
}
|
|
610
617
|
outcome = { status: result.status, output: result.output, ...(result.error ? { error: result.error } : {}), ...(result.model ? { model: result.model } : {}), usage: result.usage };
|
|
611
618
|
} catch (error) {
|
|
612
619
|
outcome = { status: "failed", output: "", error: (error as Error).message, usage: { input: 0, output: 0, cost: 0, turns: 0 } };
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and the lobby feed, and every finished job lands in the metrics log.
|
|
7
7
|
*/
|
|
8
8
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
9
|
+
import { withFallback } from "../execution/fallback.ts";
|
|
9
10
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
10
11
|
import { describeToolCall } from "../pi/activity.ts";
|
|
11
12
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
@@ -74,6 +75,8 @@ export interface QuickFixProfile {
|
|
|
74
75
|
thinking: string;
|
|
75
76
|
timeoutMs: number;
|
|
76
77
|
instructions?: string;
|
|
78
|
+
/** Where the job goes when its model runs out of usage or is unavailable. */
|
|
79
|
+
fallback?: { model: string; thinking: string };
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export interface QuickFixDeps {
|
|
@@ -260,6 +263,17 @@ export class QuickFixQueue {
|
|
|
260
263
|
},
|
|
261
264
|
this.deps.runProcess ?? spawnPiProcess,
|
|
262
265
|
);
|
|
266
|
+
// A model that is out of usage hands the job to the fallback the settings name.
|
|
267
|
+
const attemptOrFallback = async (model: string | undefined, thinking: string) => {
|
|
268
|
+
const { result: outcome, switchedFrom } = await withFallback(model, thinking, profile.fallback, attempt);
|
|
269
|
+
if (switchedFrom && profile.fallback) {
|
|
270
|
+
job.model = profile.fallback.model;
|
|
271
|
+
job.thinking = profile.fallback.thinking;
|
|
272
|
+
this.addStep(job, `${switchedFrom} is out of usage or unavailable; ran on ${profile.fallback.model} (${profile.fallback.thinking})`, Date.now());
|
|
273
|
+
this.changed();
|
|
274
|
+
}
|
|
275
|
+
return outcome;
|
|
276
|
+
};
|
|
263
277
|
let result;
|
|
264
278
|
if (route) {
|
|
265
279
|
job.route = routeLabel(route);
|
|
@@ -267,7 +281,7 @@ export class QuickFixQueue {
|
|
|
267
281
|
job.thinking = route.thinking;
|
|
268
282
|
if (route.model) job.model = route.model;
|
|
269
283
|
this.changed();
|
|
270
|
-
result = await
|
|
284
|
+
result = await attemptOrFallback(route.model, route.thinking);
|
|
271
285
|
if (fellShort(result) && !controller.signal.aborted) {
|
|
272
286
|
// The routed attempt counts in the metrics on its own; the job re-runs on the configured profile.
|
|
273
287
|
appendMetrics(this.deps.root, this.deps.configDir, [{ ...quickFixMetric({ ...job, status: result.status, finishedAt: Date.now(), usage: result.usage, ...(result.model ? { model: result.model } : {}) }), id: `${job.id}-routed-${job.startedAt}` }]);
|
|
@@ -276,10 +290,10 @@ export class QuickFixQueue {
|
|
|
276
290
|
delete job.routedFrom;
|
|
277
291
|
job.thinking = profile.thinking;
|
|
278
292
|
if (profile.model) job.model = profile.model;
|
|
279
|
-
result = await
|
|
293
|
+
result = await attemptOrFallback(profile.model, profile.thinking);
|
|
280
294
|
}
|
|
281
295
|
} else {
|
|
282
|
-
result = await
|
|
296
|
+
result = await attemptOrFallback(profile.model, profile.thinking);
|
|
283
297
|
}
|
|
284
298
|
job.status = result.status;
|
|
285
299
|
job.report = result.output.trim() || undefined;
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -20,8 +20,7 @@ import { archiveTask as archiveTaskOnDisk, deleteTask as deleteTaskOnDisk, listA
|
|
|
20
20
|
import { discardPlannedTask, listPlannedTasks, type PlannedTask } from "../state/backlog.ts";
|
|
21
21
|
import { appendMetrics, readClassifierMetrics, readMetrics, type MetricStatus } from "../state/metrics.ts";
|
|
22
22
|
import { describeToolCall } from "../pi/activity.ts";
|
|
23
|
-
import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates,
|
|
24
|
-
import { panelLines } from "../pi/zen.ts";
|
|
23
|
+
import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates, setMinimized, taskSnapshot } from "../pi/ui.ts";
|
|
25
24
|
import { shortTitle } from "../text.ts";
|
|
26
25
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
27
26
|
import { modelRef, resolveLobbyProfile, resolvePanelProfile } from "../pi/model-support.ts";
|
|
@@ -36,14 +35,14 @@ import { checkThinking } from "../pi/model-support.ts";
|
|
|
36
35
|
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
37
36
|
import { SessionChats } from "./session-files.ts";
|
|
38
37
|
import { answerMessage, askUser, questionnaires, type Asker } from "./ask.ts";
|
|
39
|
-
import { QuickFixQueue } from "./quickfix.ts";
|
|
38
|
+
import { jobTitle, QuickFixQueue } from "./quickfix.ts";
|
|
39
|
+
import { miniLine, type MiniInput } from "./mini.ts";
|
|
40
40
|
import { PlanningSession, type PlannerSeed } from "./planner.ts";
|
|
41
41
|
import { execCommand, IssuesState } from "./issues.ts";
|
|
42
42
|
import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type TabId } from "./view.ts";
|
|
43
43
|
import { lobbyTheme } from "./theme.ts";
|
|
44
44
|
import { deliverComments, onOwnerEvent, setAuto } from "../pi/owner.ts";
|
|
45
45
|
import { openEntrySettings, openSettings } from "../pi/settings-ui.ts";
|
|
46
|
-
import { budgetClock } from "../state/budget.ts";
|
|
47
46
|
|
|
48
47
|
export const ANCHOR_KEY = "bot-lobby-anchor";
|
|
49
48
|
export { deliverComments };
|
|
@@ -59,7 +58,6 @@ interface Runtime {
|
|
|
59
58
|
visible: boolean;
|
|
60
59
|
/** Stepped aside while pi shows a dialog. */
|
|
61
60
|
asideForPrompt: boolean;
|
|
62
|
-
scene: ZenScene;
|
|
63
61
|
quickfix: QuickFixQueue;
|
|
64
62
|
planner?: PlanningSession;
|
|
65
63
|
issues: IssuesState;
|
|
@@ -97,18 +95,39 @@ let frameTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
97
95
|
* through pi.
|
|
98
96
|
*/
|
|
99
97
|
function rerender(): void {
|
|
100
|
-
|
|
98
|
+
// Hidden, only the one-line status under the editor changes.
|
|
99
|
+
if (!runtime || (!runtime.visible && !miniShown(runtime)) || frameTimer) return;
|
|
101
100
|
const wait = lastFrame + FRAME_MS - Date.now();
|
|
102
101
|
const paint = () => {
|
|
103
102
|
frameTimer = undefined;
|
|
104
103
|
lastFrame = Date.now();
|
|
105
|
-
if (runtime?.visible) runtime.tui?.requestRender();
|
|
104
|
+
if (runtime?.visible || (runtime && miniShown(runtime))) runtime.tui?.requestRender();
|
|
106
105
|
};
|
|
107
106
|
if (wait <= 0) return paint();
|
|
108
107
|
frameTimer = setTimeout(paint, wait);
|
|
109
108
|
frameTimer.unref?.();
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
/** The status line shows under the editor while the lobby is hidden (not while bot-lobby is minimized), unless settings turn it off. */
|
|
112
|
+
function miniShown(state: Runtime): boolean {
|
|
113
|
+
return !state.visible && !isMinimized() && loadConfig().lobby.miniLine;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** What the status line shows: the task, the planning round, the quick fix in hand. */
|
|
117
|
+
function miniInput(state: Runtime): MiniInput {
|
|
118
|
+
const snapshot = taskSnapshot();
|
|
119
|
+
const planner = state.planner;
|
|
120
|
+
const job = state.quickfix.running ?? state.quickfix.jobs.find((entry) => entry.status === "queued");
|
|
121
|
+
const queued = state.quickfix.jobs.filter((entry) => entry.status === "queued" && entry !== job).length;
|
|
122
|
+
return {
|
|
123
|
+
...(snapshot.task ? { task: snapshot.task } : {}),
|
|
124
|
+
runs: snapshot.runs,
|
|
125
|
+
...(planner ? { planning: { busy: planner.busy, round: planner.turns, limit: loadConfig().lobby.maxPlanningRounds, questions: planner.awaitingAnswers ? planner.questions.length : 0, ready: planner.reply?.status === "ready", saved: Boolean(planner.saved) } } : {}),
|
|
126
|
+
...(job ? { quickfix: { title: jobTitle(job), running: job.status === "running", queued } } : {}),
|
|
127
|
+
key: "Alt+L",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
function sessionModel(ctx: ExtensionContext): string | undefined {
|
|
113
132
|
return ctx.model ? modelRef(ctx.model) : undefined;
|
|
114
133
|
}
|
|
@@ -426,13 +445,6 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
426
445
|
return `deleted ${taskId} for good`;
|
|
427
446
|
}
|
|
428
447
|
|
|
429
|
-
/** A task's status box without animations, for a session other than this window's. */
|
|
430
|
-
function taskScene(state: Runtime, task: Task, width: number, height: number): string[] {
|
|
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 } : {}) });
|
|
434
|
-
}
|
|
435
|
-
|
|
436
448
|
function seatProfile(state: Runtime, member: PanelMember) {
|
|
437
449
|
return resolvePanelProfile(loadConfig(), member, {
|
|
438
450
|
lookup: modelLookup(state.ctx),
|
|
@@ -518,15 +530,9 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
518
530
|
theme: () => lobbyTheme(state.ctx.ui.theme),
|
|
519
531
|
sessionId: () => state.ctx.sessionManager.getSessionId(),
|
|
520
532
|
zen: () => {
|
|
521
|
-
const snapshot =
|
|
533
|
+
const snapshot = taskSnapshot();
|
|
522
534
|
return { ...(snapshot.task ? { task: snapshot.task } : {}), runs: snapshot.runs };
|
|
523
535
|
},
|
|
524
|
-
// The scene's height budget is 3/4 of the rows it is given.
|
|
525
|
-
scene: (width, height, animated) => state.scene.lines(width, Math.floor(height / 0.75), state.ctx.ui.theme, Date.now(), !animated),
|
|
526
|
-
advanceScene: (now) => {
|
|
527
|
-
state.scene.advance(now);
|
|
528
|
-
return state.scene.delay(now);
|
|
529
|
-
},
|
|
530
536
|
feed: lobbyFeed,
|
|
531
537
|
masterBusy: () => !state.ctx.isIdle(),
|
|
532
538
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
@@ -577,7 +583,6 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
577
583
|
sessionChat: (sessionId) => chats.chat(sessionId),
|
|
578
584
|
hasOlderChat: (sessionId) => chats.hasOlder(sessionId),
|
|
579
585
|
chatHistory: (sessionId) => chatHistory(state, sessionId),
|
|
580
|
-
taskScene: (task, width, height) => taskScene(state, task, width, height),
|
|
581
586
|
requestRender: () => tui.requestRender(),
|
|
582
587
|
};
|
|
583
588
|
}
|
|
@@ -716,7 +721,6 @@ function shutdown(): void {
|
|
|
716
721
|
state.planner?.cancel();
|
|
717
722
|
state.view?.dispose();
|
|
718
723
|
state.handle?.hide();
|
|
719
|
-
setWidgetSuppressor(() => false);
|
|
720
724
|
onRunUpdates(undefined);
|
|
721
725
|
onMinimizeChange(undefined);
|
|
722
726
|
}
|
|
@@ -755,7 +759,6 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
755
759
|
asking: false,
|
|
756
760
|
mouse: false,
|
|
757
761
|
inSettings: false,
|
|
758
|
-
scene: new ZenScene(),
|
|
759
762
|
quickfix: undefined as unknown as QuickFixQueue,
|
|
760
763
|
issues: new IssuesState(execCommand, ctx.cwd, rerender),
|
|
761
764
|
};
|
|
@@ -781,7 +784,6 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
781
784
|
// Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
|
|
782
785
|
lobbyFeed.seedChat(chatFromEntries(ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY));
|
|
783
786
|
state.unsubscribeFeed = lobbyFeed.onChange(rerender);
|
|
784
|
-
setWidgetSuppressor(() => runtime?.visible === true);
|
|
785
787
|
onRunUpdates((runs) => lobbyFeed.runs(runs));
|
|
786
788
|
onMinimizeChange((value) => {
|
|
787
789
|
if (value) hideLobby();
|
|
@@ -789,7 +791,8 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
789
791
|
// Capture pi's TUI through a zero-line widget below the editor.
|
|
790
792
|
ctx.ui.setWidget(ANCHOR_KEY, (tui) => {
|
|
791
793
|
state.tui = tui;
|
|
792
|
-
|
|
794
|
+
// Zero lines while the lobby is up (or bot-lobby is minimized); one status line when it is hidden.
|
|
795
|
+
return { render: (width: number) => (miniShown(state) ? [miniLine(miniInput(state), width, lobbyTheme(state.ctx.ui.theme))] : []), invalidate: () => {} };
|
|
793
796
|
}, { placement: "belowEditor" });
|
|
794
797
|
// The owner's clock (pi/owner.ts) delivers comments and messages and drives auto mode; the lobby logs what it did.
|
|
795
798
|
onOwnerEvent((event) => {
|
package/src/lobby/tabs/home.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Lobby tab: the
|
|
3
|
-
* and agent animations, or just their status when animations are off), then the
|
|
2
|
+
* The Lobby tab: the status of this session's task on top, then the
|
|
4
3
|
* conversation with the oracle (text only, no tool rows, no thinking), the
|
|
5
4
|
* activity log of plain-words steps from every agent, and the one place where
|
|
6
|
-
* thoughts show up. Pure: the
|
|
5
|
+
* thoughts show up. Pure: the clock arrives as `now`.
|
|
7
6
|
*/
|
|
8
7
|
import { textWidth } from "../../width.ts";
|
|
9
8
|
import type { Task } from "../../schemas/task.ts";
|
|
@@ -18,8 +17,6 @@ export type HomePane = (typeof HOME_PANES)[number];
|
|
|
18
17
|
|
|
19
18
|
export interface HomeInput {
|
|
20
19
|
task?: Task;
|
|
21
|
-
/** Draws the zen scene into at most `height` lines, animated or still; absent when no task is active. */
|
|
22
|
-
scene?: (width: number, height: number, animated: boolean) => string[];
|
|
23
20
|
chat: readonly ChatEntry[];
|
|
24
21
|
/** The oracle's reply while it streams. */
|
|
25
22
|
liveReply?: string;
|
|
@@ -48,8 +45,6 @@ export interface HomeInput {
|
|
|
48
45
|
keys?: Record<LobbyPanel, string>;
|
|
49
46
|
/** Another session is shown: its name titles the conversation. */
|
|
50
47
|
title?: string;
|
|
51
|
-
/** The scene is another session's still status (no animations to toggle). */
|
|
52
|
-
stillScene?: boolean;
|
|
53
48
|
/** What the empty conversation says instead of the default (a session starting, say). */
|
|
54
49
|
emptyNote?: string;
|
|
55
50
|
/** What the empty activity log says instead of "No activity yet.". */
|
|
@@ -60,11 +55,6 @@ export interface HomeInput {
|
|
|
60
55
|
|
|
61
56
|
/** Wide terminals put the conversation and the activity log side by side. */
|
|
62
57
|
export const HOME_COLUMNS_MIN = 100;
|
|
63
|
-
/** Most lines the scene may take, and its share of the body; the still status needs far fewer. */
|
|
64
|
-
const SCENE_SHARE = 0.45;
|
|
65
|
-
const MAX_SCENE = 36;
|
|
66
|
-
const MAX_STILL = 10;
|
|
67
|
-
|
|
68
58
|
const SOURCE_COLORS: Record<string, LobbyColor> = {
|
|
69
59
|
MASTER: "accent",
|
|
70
60
|
DEV: "success",
|
|
@@ -490,31 +480,6 @@ function thinkingContent(input: HomeInput, thoughts: readonly ThoughtEntry[], wi
|
|
|
490
480
|
return whole([paint(theme, "dim", input.query ? `No thought matches "${input.query}".` : "Thoughts from the oracle and every agent appear here, and only here.")]);
|
|
491
481
|
}
|
|
492
482
|
|
|
493
|
-
/**
|
|
494
|
-
* The scene: with animations off (the `animations` panel, off by default)
|
|
495
|
-
* only the task's status box, what the agents are doing and the checklist;
|
|
496
|
-
* with them on, the animated oracle and agents too. Its first line names the
|
|
497
|
-
* key that toggles the animations.
|
|
498
|
-
*/
|
|
499
|
-
function sceneLines(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
500
|
-
if (!input.task || !input.scene) return [];
|
|
501
|
-
const animated = input.panels.animations && !input.stillScene;
|
|
502
|
-
const budget = Math.min(animated ? MAX_SCENE : MAX_STILL, Math.floor(height * SCENE_SHARE));
|
|
503
|
-
if (budget < (animated ? 6 : 4)) return [];
|
|
504
|
-
const lines = input.scene(width, budget, animated).slice(0, budget);
|
|
505
|
-
const note = input.keys && !input.stillScene ? `${input.keys.animations} ${animated ? "hides" : "shows"} animations` : undefined;
|
|
506
|
-
return keyNote(lines, width, note, theme);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** `note` at the right end of the first line, dimmed, when it fits beside what is there. */
|
|
510
|
-
function keyNote(lines: readonly string[], width: number, note: string | undefined, theme?: LobbyTheme): string[] {
|
|
511
|
-
const first = lines[0];
|
|
512
|
-
if (!note || first === undefined) return [...lines];
|
|
513
|
-
const gap = width - textWidth(first) - textWidth(note) - 1;
|
|
514
|
-
if (gap < 2) return [...lines];
|
|
515
|
-
return [`${first}${" ".repeat(gap)}${paint(theme, "dim", note)}`, ...lines.slice(1)];
|
|
516
|
-
}
|
|
517
|
-
|
|
518
483
|
function hiddenHint(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
519
484
|
const keys = input.keys;
|
|
520
485
|
const text = keys
|
|
@@ -536,12 +501,11 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
536
501
|
if (height <= 0) return [];
|
|
537
502
|
const { panels } = input;
|
|
538
503
|
const feed = filterFeed(input);
|
|
539
|
-
const
|
|
540
|
-
const rest = height - scene.length;
|
|
504
|
+
const rest = height;
|
|
541
505
|
const showMain = panels.conversation || panels.activity;
|
|
542
506
|
const thinkHeight = !panels.thinking ? 0 : !showMain ? rest : rest >= 18 ? Math.max(5, Math.floor(rest * 0.25)) : rest >= 10 ? 4 : 0;
|
|
543
507
|
const main = rest - thinkHeight;
|
|
544
|
-
if (!showMain && !panels.thinking) return fill(
|
|
508
|
+
if (!showMain && !panels.thinking) return fill(hiddenHint(input, width, rest, theme), height, width);
|
|
545
509
|
const matches = (count: number) => `${count} match${count === 1 ? "" : "es"}`;
|
|
546
510
|
/**
|
|
547
511
|
* One scrollable pane: only the lines it shows are drawn — its newest, or
|
|
@@ -563,7 +527,7 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
563
527
|
const thought = currentThought(feed.thoughts);
|
|
564
528
|
const thinkingNote = thought ? `${thought.source} · ${thought.live ? "thinking" : since(input.now - thought.at)}` : input.keys?.thinking;
|
|
565
529
|
const thinkingBox = pane("thinking", "Thinking", thinkingNote, (inner, need, anchor) => thinkingContent(input, feed.thoughts, inner, need, anchor, theme));
|
|
566
|
-
const top =
|
|
530
|
+
const top = 0;
|
|
567
531
|
let body: string[] = [];
|
|
568
532
|
if (main > 0 && panels.conversation && panels.activity) {
|
|
569
533
|
if (width >= HOME_COLUMNS_MIN) {
|
|
@@ -576,5 +540,5 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
576
540
|
} else if (main > 0 && panels.conversation) body = chatBox(top, 0, width, main);
|
|
577
541
|
else if (main > 0 && panels.activity) body = activityBox(top, 0, width, main);
|
|
578
542
|
const think = thinkHeight >= 3 ? thinkingBox(top + main, 0, width, thinkHeight) : [];
|
|
579
|
-
return fill([...
|
|
543
|
+
return fill([...fill(body, main), ...think], height, width);
|
|
580
544
|
}
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { TERMINAL_STATES, taskRequest, type Task } from "../../schemas/task.ts";
|
|
8
8
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
9
9
|
import type { PlanComment } from "../../state/comments.ts";
|
|
10
|
-
import { planChecklist } from "../../pi/
|
|
10
|
+
import { planChecklist } from "../../pi/plan-checklist.ts";
|
|
11
11
|
import { persistedRuns } from "../../pi/ui.ts";
|
|
12
12
|
import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|