@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.
Files changed (71) hide show
  1. package/README.md +70 -79
  2. package/package.json +4 -2
  3. package/prompts/master.md +3 -2
  4. package/prompts/qa.md +33 -11
  5. package/prompts/reviewer.md +5 -2
  6. package/prompts/worker.md +5 -2
  7. package/src/ask/relay.ts +1 -2
  8. package/src/ask/tool.ts +4 -6
  9. package/src/ask/types.ts +16 -0
  10. package/src/ask/web.ts +63 -0
  11. package/src/index.ts +7 -1
  12. package/src/lobby/ask.ts +4 -5
  13. package/src/lobby/blocks.ts +57 -0
  14. package/src/lobby/feed.ts +1 -1
  15. package/src/lobby/host.ts +178 -0
  16. package/src/lobby/keys.ts +10 -41
  17. package/src/lobby/prompt-hub.ts +30 -77
  18. package/src/lobby/prompts.ts +8 -12
  19. package/src/lobby/runtime.ts +49 -310
  20. package/src/lobby/service.ts +54 -134
  21. package/src/lobby/task-rows.ts +28 -0
  22. package/src/pi/commands.ts +14 -20
  23. package/src/pi/model-settings.ts +112 -0
  24. package/src/pi/start-task.ts +1 -1
  25. package/src/pi/tools.ts +6 -7
  26. package/src/pi/ui.ts +0 -9
  27. package/src/schemas/configuration.ts +9 -33
  28. package/src/state/attachments.ts +82 -0
  29. package/src/webui/api/index.ts +17 -15
  30. package/src/webui/api/lobby.ts +4 -3
  31. package/src/webui/api/planner.ts +6 -4
  32. package/src/webui/api/quickfix.ts +4 -3
  33. package/src/webui/api/sessions.ts +9 -7
  34. package/src/webui/api/settings.ts +20 -8
  35. package/src/webui/api/status.ts +4 -15
  36. package/src/webui/api/tasks.ts +11 -15
  37. package/src/webui/command.ts +35 -19
  38. package/src/webui/dev/fake-service.ts +1 -2
  39. package/src/webui/dev/fixtures.ts +2 -5
  40. package/src/webui/events.ts +1 -1
  41. package/src/webui/protocol.ts +22 -11
  42. package/src/webui/server.ts +42 -1
  43. package/src/webui/uploads.ts +68 -0
  44. package/webui/dist/assets/index-B1PnCyl2.css +2 -0
  45. package/webui/dist/assets/index-DI9Yq9IC.js +90 -0
  46. package/webui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  47. package/webui/dist/build.json +142 -110
  48. package/webui/dist/index.html +4 -3
  49. package/src/ask/dialog.ts +0 -193
  50. package/src/ask/image.ts +0 -202
  51. package/src/ask/png.ts +0 -179
  52. package/src/ask/state.ts +0 -175
  53. package/src/ask/view.ts +0 -163
  54. package/src/lobby/layout.ts +0 -496
  55. package/src/lobby/markdown.ts +0 -121
  56. package/src/lobby/mini.ts +0 -167
  57. package/src/lobby/tabs/excalidraw.ts +0 -95
  58. package/src/lobby/tabs/git.ts +0 -162
  59. package/src/lobby/tabs/home.ts +0 -544
  60. package/src/lobby/tabs/issues.ts +0 -73
  61. package/src/lobby/tabs/knowledge.ts +0 -135
  62. package/src/lobby/tabs/metrics.ts +0 -300
  63. package/src/lobby/tabs/plan.ts +0 -262
  64. package/src/lobby/tabs/quickfix.ts +0 -136
  65. package/src/lobby/tabs/tasks.ts +0 -412
  66. package/src/lobby/theme.ts +0 -30
  67. package/src/lobby/view.ts +0 -3018
  68. package/src/pi/settings-ui.ts +0 -708
  69. package/src/width.ts +0 -102
  70. package/webui/dist/assets/index-Bk-lpAGm.css +0 -2
  71. package/webui/dist/assets/index-E6MPcbdC.js +0 -80
@@ -1,49 +1,40 @@
1
1
  /**
2
- * The lobby inside pi: one runtime per interactive session. It mounts the
3
- * full-screen view as an overlay on pi's own TUI (captured through a zero-line
4
- * anchor widget, so pi does not treat the lobby as a blocking dialog), steps
5
- * aside while a real dialog asks the user something, narrates the Master's own
6
- * turn into the feed, passes plan comments to the Master that owns a task and
7
- * records each Master turn in the metrics log.
2
+ * The lobby inside pi: one runtime per master session. It owns the lobby's
3
+ * state (the quick-fix queue, planner, issues, pulls, knowledge), narrates the
4
+ * Master's own turn into the feed, passes plan comments to the Master that
5
+ * owns a task and records each Master turn in the metrics log. The web server
6
+ * (src/webui) is its only surface.
8
7
  */
9
8
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
- import { getSelectListTheme } from "@earendil-works/pi-coding-agent";
11
- import { Key, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
12
- import { detectProjectRoot, loadConfig, saveConfig } from "../state/project.ts";
9
+ import { detectProjectRoot, loadConfig } from "../state/project.ts";
13
10
  import { appendMetrics, type MetricStatus } from "../state/metrics.ts";
14
11
  import { describeToolCall } from "../pi/activity.ts";
15
- import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates, setMinimized, taskSnapshot } from "../pi/ui.ts";
12
+ import { currentZenTask, onRunUpdates } from "../pi/ui.ts";
16
13
  import type { WorkspaceInfo } from "../execution/workspace.ts";
17
14
  import { basename } from "node:path";
18
15
  import { isSubagentProcess } from "../pi/quiet.ts";
19
16
  import { modelRef } from "../pi/model-support.ts";
20
17
  import { modelLookup } from "../pi/tools.ts";
21
18
  import { setQuickFixHandoff } from "../pi/route.ts";
22
- import { currentWebServer } from "../webui/server.ts";
23
19
  import type { Domain } from "../schemas/agent.ts";
24
- import type { LobbyPanel } from "../schemas/configuration.ts";
25
20
  import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike } from "./feed.ts";
26
21
  import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
27
22
  import { checkThinking } from "../pi/model-support.ts";
28
23
  import { jobTitle, QuickFixQueue } from "./quickfix.ts";
29
- import type { Asker } from "./ask.ts";
30
- import { miniLine, type MiniInput } from "./mini.ts";
24
+ import type { Asker } from "../ask/types.ts";
31
25
  import type { PlanningSession } from "./planner.ts";
32
26
  import { execCommand, IssuesState } from "./issues.ts";
33
27
  import { PullsState } from "./pulls.ts";
34
28
  import { PullReviews } from "./pr-review.ts";
35
29
  import { KnowledgeBook } from "./knowledge.ts";
36
30
  import { ExcalidrawBook } from "../excalidraw/sessions.ts";
37
- import { LobbyView, type LobbyHost, type TabId } from "./view.ts";
38
- import { createLobbyService, lobbyProfile, miniShown, repaintLobby, reviewProfile, seatProfile, setServiceState, takeReopenAfterSwitch, savePlan as serviceSavePlan, answerPanel as serviceAnswerPanel, type LobbyService } from "./service.ts";
31
+ import type { LobbyService } from "./host.ts";
32
+ import { createLobbyService, lobbyNotify, lobbyProfile, reviewProfile, seatProfile, setServiceState, savePlan as serviceSavePlan, answerPanel as serviceAnswerPanel } from "./service.ts";
39
33
  import { lobbyTopics } from "./topics.ts";
40
- import { promptHub } from "./prompt-hub.ts";
41
- export { backgroundSessions, FRAME_MS, setSessionLauncher } from "./service.ts";
42
- import { lobbyTheme } from "./theme.ts";
34
+ export { backgroundSessions, setSessionLauncher } from "./service.ts";
43
35
  import { deliverComments, onOwnerEvent } from "../pi/owner.ts";
44
- import { openEntrySettings, openSettings } from "../pi/settings-ui.ts";
36
+ import { pushNotice } from "../webui/notices.ts";
45
37
 
46
- export const ANCHOR_KEY = "bot-lobby-anchor";
47
38
  export { deliverComments };
48
39
 
49
40
  export interface Runtime {
@@ -51,12 +42,6 @@ export interface Runtime {
51
42
  configDir: string;
52
43
  ctx: ExtensionContext;
53
44
  root: string;
54
- tui?: TUI;
55
- view?: LobbyView;
56
- handle?: OverlayHandle;
57
- visible: boolean;
58
- /** Stepped aside while pi shows a dialog. */
59
- asideForPrompt: boolean;
60
45
  quickfix: QuickFixQueue;
61
46
  planner?: PlanningSession;
62
47
  issues: IssuesState;
@@ -65,19 +50,11 @@ export interface Runtime {
65
50
  knowledge: KnowledgeBook;
66
51
  excalidraw: ExcalidrawBook;
67
52
  unsubscribeFeed?: () => void;
68
- /** The terminal's subscription to every topic (repaints, as before). */
69
- unsubscribeTopics?: () => void;
70
- /** The terminal's registration as the hub's only prompt surface. */
71
- unregisterTerminalSurface?: () => void;
72
- /** Puts the panel's questions to the user: the questionnaire unless a test sets another. */
53
+ /** Puts the panel's questions to the user: the web page's questionnaire unless a test sets another. */
73
54
  asker?: Asker;
74
- /** A questionnaire is on screen. */
55
+ /** A questionnaire is open in the page. */
75
56
  asking: boolean;
76
- /** The lobby turned the terminal's mouse reporting on (pi's regular screen only). */
77
- mouse: boolean;
78
- /** Settings opened from the lobby are on screen; the lobby stays aside until they close. */
79
- inSettings: boolean;
80
- /** The repository (or folder) and branch the lobby's title shows, and whether git is being asked now. */
57
+ /** The repository (or folder) and branch the page's title shows, and whether git is being asked now. */
81
58
  workspace: WorkspaceInfo;
82
59
  readingWorkspace: boolean;
83
60
  }
@@ -85,42 +62,11 @@ export interface Runtime {
85
62
  let runtime: Runtime | undefined;
86
63
  let lobbyService: LobbyService | undefined;
87
64
 
88
- /** The lobby's backend, once its service has started (terminal and web alike). */
65
+ /** The lobby's backend, once its service has started. */
89
66
  export function currentLobbyService(): LobbyService | undefined {
90
67
  return lobbyService;
91
68
  }
92
69
 
93
- export function isLobbyVisible(): boolean {
94
- return runtime?.visible === true;
95
- }
96
-
97
- /** The lobby's view, once it has been shown (tests drive it through this). */
98
- export function lobbyView(): LobbyView | undefined {
99
- return runtime?.view;
100
- }
101
-
102
- /** Repaint through the lobby service's frame throttle (see service.ts). */
103
- function rerender(): void {
104
- repaintLobby();
105
- }
106
-
107
- /** What the status line shows: the task, the planning round, the quick fix in hand. */
108
- function miniInput(state: Runtime): MiniInput {
109
- const snapshot = taskSnapshot();
110
- const planner = state.planner;
111
- const job = state.quickfix.running ?? state.quickfix.jobs.find((entry) => entry.status === "queued");
112
- const queued = state.quickfix.jobs.filter((entry) => entry.status === "queued" && entry !== job).length;
113
- const webLink = currentWebServer()?.link;
114
- return {
115
- ...(snapshot.task ? { task: snapshot.task } : {}),
116
- runs: snapshot.runs,
117
- ...(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) } } : {}),
118
- ...(job ? { quickfix: { title: jobTitle(job), running: job.status === "running", queued } } : {}),
119
- ...(webLink ? { webLink } : {}),
120
- key: "Alt+L",
121
- };
122
- }
123
-
124
70
  /** Save the plan as a pending task; the service holds the implementation. */
125
71
  export async function savePlan(state: Runtime | undefined = runtime): Promise<string> {
126
72
  return serviceSavePlan(state);
@@ -131,156 +77,6 @@ export async function answerPanel(state: Runtime | undefined = runtime): Promise
131
77
  return serviceAnswerPanel(state);
132
78
  }
133
79
 
134
- function host(state: Runtime, tui: TUI): LobbyHost {
135
- return {
136
- ...createLobbyService(state),
137
- rows: () => tui.terminal.rows,
138
- theme: () => lobbyTheme(state.ctx.ui.theme),
139
- hide: () => hideLobby(),
140
- panels: () => loadConfig().lobby.panels,
141
- savePanels: (panels) => savePanels(panels),
142
- keys: () => loadConfig().lobby.keys,
143
- openSettings: (entry) => lobbySettings(state, entry),
144
- editText: (title, text) => editText(state, title, text),
145
- requestRender: () => tui.requestRender(),
146
- };
147
- }
148
-
149
- /**
150
- * Run something pi draws itself (its settings menus, its multi-line editor)
151
- * with the lobby stepped aside for the whole visit, not only for each dialog,
152
- * so it does not flash between them; the lobby rereads the config after.
153
- */
154
- async function whileAside<T>(state: Runtime, run: () => Promise<T>, failure: string): Promise<T | undefined> {
155
- if (state.inSettings) return undefined;
156
- state.inSettings = true;
157
- setMouse(state, false);
158
- state.handle?.setHidden(true);
159
- try {
160
- return await run();
161
- } catch (error) {
162
- state.ctx.ui.notify(`bot-lobby: ${failure} — ${(error as Error).message}`, "warning");
163
- return undefined;
164
- } finally {
165
- state.inSettings = false;
166
- state.asideForPrompt = false;
167
- if (state.visible && state.handle) {
168
- state.handle.setHidden(false);
169
- state.handle.focus();
170
- setMouse(state, true);
171
- }
172
- state.view?.reloadConfig();
173
- rerender();
174
- }
175
- }
176
-
177
- /** bot-lobby's settings (or one agent's entry) from inside the lobby. */
178
- async function lobbySettings(state: Runtime, entry?: "quickfix" | "planner"): Promise<void> {
179
- await whileAside(state, () => (entry ? openEntrySettings(state.pi, state.ctx, entry) : openSettings(state.pi, state.ctx)), "settings failed");
180
- }
181
-
182
- /** A text edited in pi's multi-line editor (Enter saves, Shift+Enter is a new line); undefined when cancelled. */
183
- async function editText(state: Runtime, title: string, text: string): Promise<string | undefined> {
184
- return whileAside(state, () => state.ctx.ui.editor(title, text), "the editor failed");
185
- }
186
-
187
- /** Remember which panes show, keeping every other setting as the file has it now. */
188
- function savePanels(panels: Record<LobbyPanel, boolean>): void {
189
- try {
190
- const config = loadConfig();
191
- saveConfig({ ...config, lobby: { ...config.lobby, panels: { ...panels } } });
192
- } catch {
193
- // A read-only config only means the choice lasts for this session.
194
- }
195
- }
196
-
197
- /** SGR mouse reporting: presses, releases and the wheel, in cell coordinates. */
198
- const MOUSE_ON = "\x1b[?1000h\x1b[?1006h";
199
- const MOUSE_OFF = "\x1b[?1006l\x1b[?1000l";
200
-
201
- /**
202
- * In pi's regular screen nothing reports the mouse, so the lobby turns
203
- * reporting on while it is up (clicks pick tabs and draft lines, the wheel
204
- * scrolls) and off whenever it steps aside. Full-screen pi reports the mouse
205
- * itself and hands the events to the lobby.
206
- */
207
- function setMouse(state: Runtime, on: boolean): void {
208
- const tui = state.tui;
209
- if (!tui || tui.mode === "fullscreen") return;
210
- const want = on && loadConfig().lobby.mouse;
211
- if (want === state.mouse) return;
212
- state.mouse = want;
213
- tui.terminal.write(want ? MOUSE_ON : MOUSE_OFF);
214
- }
215
-
216
- /** Show the lobby (optionally on `tab`); false when there is no interactive TUI. */
217
- export function showLobby(tab?: TabId): boolean {
218
- const state = runtime;
219
- if (!state?.tui) return false;
220
- if (isMinimized()) setMinimized(false);
221
- if (!state.view || !state.handle) {
222
- const theme = state.ctx.ui.theme;
223
- state.view = new LobbyView(state.tui, host(state, state.tui), { borderColor: (text) => theme.fg("border", text), selectList: getSelectListTheme() });
224
- state.handle = state.tui.showOverlay(state.view, { width: "100%", maxHeight: "100%", anchor: "top-left" });
225
- } else {
226
- state.handle.setHidden(false);
227
- state.handle.focus();
228
- }
229
- state.visible = true;
230
- state.asideForPrompt = false;
231
- if (tab) state.view.setTab(tab);
232
- state.view.start();
233
- setMouse(state, true);
234
- applyStatus(state.ctx, state.root, state.configDir);
235
- state.tui.requestRender();
236
- return true;
237
- }
238
-
239
- export function hideLobby(): void {
240
- const state = runtime;
241
- if (!state?.view || !state.visible) return;
242
- state.visible = false;
243
- state.asideForPrompt = false;
244
- setMouse(state, false);
245
- state.view.stop();
246
- state.handle?.setHidden(true);
247
- applyStatus(state.ctx, state.root, state.configDir);
248
- state.tui?.requestRender();
249
- }
250
-
251
- export function toggleLobby(): boolean {
252
- if (isLobbyVisible()) {
253
- hideLobby();
254
- return true;
255
- }
256
- return showLobby();
257
- }
258
-
259
- /** Open the lobby when this session drives a task and the config asks for it. */
260
- export function autoOpenLobby(): void {
261
- if (runtime && loadConfig().lobby.autoOpen && currentZenTask()) showLobby("lobby");
262
- }
263
-
264
- /** Step aside while pi shows a dialog (an approval, a question), and come back after. */
265
- function promptStarted(): void {
266
- lobbyTopics.bump("status");
267
- const state = runtime;
268
- if (!state?.visible || !state.handle || state.inSettings) return;
269
- state.asideForPrompt = true;
270
- setMouse(state, false);
271
- state.handle.setHidden(true);
272
- }
273
-
274
- function promptEnded(): void {
275
- lobbyTopics.bump("status");
276
- const state = runtime;
277
- if (!state?.asideForPrompt || !state.handle || state.inSettings) return;
278
- state.asideForPrompt = false;
279
- if (!state.visible) return;
280
- state.handle.setHidden(false);
281
- setMouse(state, true);
282
- }
283
-
284
80
  function shutdown(): void {
285
81
  const state = runtime;
286
82
  if (!state) return;
@@ -288,23 +84,17 @@ function shutdown(): void {
288
84
  lobbyService = undefined;
289
85
  setServiceState(undefined);
290
86
  setQuickFixHandoff(undefined);
291
- setMouse(state, false);
292
87
  state.unsubscribeFeed?.();
293
- state.unsubscribeTopics?.();
294
- state.unregisterTerminalSurface?.();
295
88
  state.quickfix.cancelAll();
296
89
  state.reviews.cancelAll();
297
90
  state.planner?.cancel();
298
- state.view?.dispose();
299
- state.handle?.hide();
300
91
  onRunUpdates(undefined);
301
- onMinimizeChange(undefined);
302
92
  }
303
93
 
304
94
  /**
305
95
  * The oracle routed a request to the quick-fix agent: queue it (no "looks like
306
96
  * a task" hold, the oracle already decided), on its builder's settings when it
307
- * is a quick feature, and show it on the Quick fix tab.
97
+ * is a quick feature, and tell the page where it went.
308
98
  */
309
99
  function handToQuickFix(state: Runtime, request: string, builder: Domain | undefined, reason: string): string {
310
100
  const quick = lobbyProfile(state, "quickfix");
@@ -314,24 +104,18 @@ function handToQuickFix(state: Runtime, request: string, builder: Domain | undef
314
104
  ...(profile ? { profile } : {}),
315
105
  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.` : ""}`,
316
106
  });
317
- showLobby("quickfix");
318
107
  lobbyTopics.bump("quickfix");
319
- state.view?.showQuickFix(job.id);
108
+ pushNotice("The oracle sent your request to the quick-fix agent.", "info");
320
109
  return job.id;
321
110
  }
322
111
 
323
- /** Whether the web server wants the lobby service without the terminal (a later step flips this on). */
324
- function webServerOn(): boolean {
325
- return false;
326
- }
327
-
328
112
  /**
329
- * Start the lobby's backend: state, service and background wiring. Runs in
330
- * the terminal and, once the web server exists, whenever it is on.
113
+ * Start the lobby's backend: state, service and background wiring. Runs for
114
+ * every interactive master session, not a subagent.
331
115
  */
332
116
  export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string): Runtime | undefined {
333
117
  shutdown();
334
- if (isSubagentProcess() || (ctx.mode !== "tui" && !webServerOn())) return undefined;
118
+ if (isSubagentProcess() || !servesPage(ctx)) return undefined;
335
119
  const root = detectProjectRoot(ctx.cwd, configDir);
336
120
  const workflow = loadConfig().workflow;
337
121
  const state: Runtime = {
@@ -339,22 +123,12 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
339
123
  configDir,
340
124
  ctx,
341
125
  root,
342
- visible: false,
343
- asideForPrompt: false,
344
126
  asking: false,
345
- mouse: false,
346
- inSettings: false,
347
127
  workspace: { name: basename(ctx.cwd) || ctx.cwd },
348
128
  readingWorkspace: false,
349
129
  quickfix: undefined as unknown as QuickFixQueue,
350
- issues: new IssuesState(execCommand, ctx.cwd, () => {
351
- lobbyTopics.bump("issues");
352
- rerender();
353
- }),
354
- pulls: new PullsState(execCommand, ctx.cwd, () => {
355
- lobbyTopics.bump("git");
356
- rerender();
357
- }),
130
+ issues: new IssuesState(execCommand, ctx.cwd, () => lobbyTopics.bump("issues")),
131
+ pulls: new PullsState(execCommand, ctx.cwd, () => lobbyTopics.bump("git")),
358
132
  reviews: undefined as unknown as PullReviews,
359
133
  knowledge: new KnowledgeBook({
360
134
  root,
@@ -374,14 +148,8 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
374
148
  stallTimeoutMs: workflow.stallTimeoutMs,
375
149
  toolStallTimeoutMs: workflow.toolStallTimeoutMs,
376
150
  feed: lobbyFeed,
377
- onChange: () => {
378
- lobbyTopics.bump("git");
379
- rerender();
380
- },
381
- notify: (message, level) => {
382
- lobbyTopics.bump("notices");
383
- if (!state.visible) ctx.ui.notify(message, level);
384
- },
151
+ onChange: () => lobbyTopics.bump("git"),
152
+ notify: (message, level) => lobbyNotify(state, message, level),
385
153
  classifier: classifier(),
386
154
  });
387
155
  state.quickfix = new QuickFixQueue({
@@ -392,14 +160,8 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
392
160
  stallTimeoutMs: workflow.stallTimeoutMs,
393
161
  toolStallTimeoutMs: workflow.toolStallTimeoutMs,
394
162
  feed: lobbyFeed,
395
- onChange: () => {
396
- lobbyTopics.bump("quickfix");
397
- rerender();
398
- },
399
- notify: (message, level) => {
400
- lobbyTopics.bump("notices");
401
- if (!state.visible) ctx.ui.notify(message, level);
402
- },
163
+ onChange: () => lobbyTopics.bump("quickfix"),
164
+ notify: (message, level) => lobbyNotify(state, message, level),
403
165
  hints: hintsFor({ cwd: ctx.cwd, root, configDir }),
404
166
  classifier: classifier(),
405
167
  effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
@@ -407,30 +169,21 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
407
169
  runtime = state;
408
170
  lobbyService = createLobbyService(state);
409
171
  setServiceState(state);
410
- // Topic routing (src/lobby/topics.ts): the terminal hears every topic and
411
- // repaints, as before. Sources bump on their way to rerender — lobbyFeed
412
- // (lobby), run updates (tasks), quick fix (quickfix), planner (planner),
413
- // background sessions (sessions), metrics (metrics), pulls/reviews (git),
414
- // issues (issues), the prompt hub (prompts), owner/notify events (notices),
415
- // dialogs/busy/workspace (status), link checks (excalidraw). File polling
416
- // for tasks/plans/knowledge starts once a non-terminal listener attaches.
417
- state.unsubscribeTopics = lobbyTopics.onChange(rerender);
418
- state.unregisterTerminalSurface = promptHub.registerSurface({ name: "terminal", show() {}, withdraw() {} });
172
+ // Topic routing (src/lobby/topics.ts): sources bump a topic when their data
173
+ // changes and the web page rereads it. lobbyFeed (lobby), run updates (tasks),
174
+ // quick fix (quickfix), planner (planner), background sessions (sessions),
175
+ // metrics (metrics), pulls/reviews (git), issues (issues), the prompt hub
176
+ // (prompts), owner/notify events (notices), busy/workspace (status), link
177
+ // checks (excalidraw).
419
178
  setQuickFixHandoff((request, builder, reason) => (runtime === state ? handToQuickFix(state, request, builder, reason) : undefined));
420
179
  lobbyFeed.clear();
421
180
  // Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
422
181
  lobbyFeed.seedChat(chatFromEntries(ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY));
423
- state.unsubscribeFeed = lobbyFeed.onChange(() => {
424
- lobbyTopics.bump("lobby");
425
- rerender();
426
- });
182
+ state.unsubscribeFeed = lobbyFeed.onChange(() => lobbyTopics.bump("lobby"));
427
183
  onRunUpdates((runs) => {
428
184
  lobbyFeed.runs(runs);
429
185
  lobbyTopics.bump("tasks");
430
186
  });
431
- onMinimizeChange((value) => {
432
- if (value) hideLobby();
433
- });
434
187
  // The owner's clock (pi/owner.ts) delivers comments and messages and drives auto mode; the lobby logs what it did.
435
188
  onOwnerEvent((event) => {
436
189
  lobbyTopics.bump("notices");
@@ -439,26 +192,17 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
439
192
  else if (event.kind === "auto") lobbyFeed.log("LOBBY", `auto mode ${event.on ? "on" : "off"} for ${event.taskId}`, event.on ? "success" : "info");
440
193
  else if (event.kind === "nudge") lobbyFeed.log("LOBBY", `auto mode: keeping the oracle going on ${event.taskId}`, "info");
441
194
  else lobbyFeed.log("LOBBY", `auto mode: no progress on ${event.taskId} — it needs you`, "warning");
442
- rerender();
443
195
  });
444
196
  return state;
445
197
  }
446
198
 
447
- /** Mount the terminal overlay: the zero-line anchor widget that captures pi's TUI. Runs only in the terminal. */
448
- function mountTerminalLobby(state: Runtime): void {
449
- const ctx = state.ctx;
450
- // Capture pi's TUI through a zero-line widget below the editor.
451
- ctx.ui.setWidget(ANCHOR_KEY, (tui) => {
452
- state.tui = tui;
453
- // Zero lines while the lobby is up (or bot-lobby is minimized); one status line when it is hidden.
454
- return { render: (width: number) => (miniShown(state) ? [miniLine(miniInput(state), width, lobbyTheme(state.ctx.ui.theme))] : []), invalidate: () => {} };
455
- }, { placement: "belowEditor" });
456
- }
457
-
458
- /** Create the session's lobby runtime (interactive master sessions only). */
459
- export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string): void {
460
- const state = startLobbyService(pi, ctx, configDir);
461
- if (state && ctx.mode === "tui") mountTerminalLobby(state);
199
+ /**
200
+ * Whether this pi process serves the web page: an interactive session. A
201
+ * one-shot run has nobody to answer, and the background sessions the lobby
202
+ * starts (RPC) are shown by the page of the window that started them.
203
+ */
204
+ export function servesPage(ctx: ExtensionContext): boolean {
205
+ return ctx.mode === "tui";
462
206
  }
463
207
 
464
208
  interface MasterTurn {
@@ -481,28 +225,23 @@ function turnStatus(messages: readonly unknown[]): MetricStatus {
481
225
  return "success";
482
226
  }
483
227
 
484
- /** Narrate the Master's own turn into the feed and record it as a metric; the lobby follows pi's dialogs. */
228
+ /** Narrate the Master's own turn into the feed and record it as a metric. */
485
229
  export function registerLobbyEvents(pi: ExtensionAPI, configDir: string): void {
486
230
  if (isSubagentProcess()) return;
487
231
  let turn: MasterTurn | undefined;
488
232
  const track = (ctx: ExtensionContext) => {
489
233
  if (runtime) runtime.ctx = ctx;
490
234
  };
491
- pi.registerShortcut(Key.alt("l"), {
492
- description: "bot-lobby: open or hide the lobby",
493
- handler: (ctx) => {
494
- if (!toggleLobby()) ctx.ui.notify("The lobby needs pi's interactive terminal UI.", "warning");
495
- },
496
- });
497
235
  pi.on("session_start", (_event, ctx) => {
498
- initLobby(pi, ctx, configDir);
499
- // A switch asked for from the lobby lands back in the lobby, on the session it switched to.
500
- if (takeReopenAfterSwitch()) showLobby("lobby");
501
- else autoOpenLobby();
236
+ startLobbyService(pi, ctx, configDir);
502
237
  });
503
238
  pi.on("session_shutdown", () => shutdown());
504
- pi.on("ui_prompt_start", () => promptStarted());
505
- pi.on("ui_prompt_end", () => promptEnded());
239
+ pi.on("ui_prompt_start", () => {
240
+ lobbyTopics.bump("status");
241
+ });
242
+ pi.on("ui_prompt_end", () => {
243
+ lobbyTopics.bump("status");
244
+ });
506
245
  pi.on("agent_start", (_event, ctx) => {
507
246
  track(ctx);
508
247
  lobbyTopics.bump("status");