@a-t-h-i/bot-lobby 0.5.0 → 0.5.1

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.
@@ -26,7 +26,7 @@ import { describeRun, runFromLog } from "./run-summary.ts";
26
26
  import { modelLookup } from "./tools.ts";
27
27
 
28
28
  const HELP = [
29
- "/bot-lobby Open the lobby: tasks, plan, quick fix, issues, metrics (alt+l)",
29
+ "/bot-lobby Open the lobby: tasks, plan, quick fix, metrics (alt+l)",
30
30
  "/bot-lobby <request> Start a task through the workflow",
31
31
  "/bot-lobby status [taskId] Show the active task",
32
32
  "/bot-lobby tasks List tasks",
@@ -9,10 +9,12 @@ import { Container, type Component, type Focusable, fuzzyFilter, getKeybindings,
9
9
  import {
10
10
  INHERIT_MODEL,
11
11
  isThinkingLevel,
12
+ LOBBY_PANELS,
12
13
  SCOUT_THINKING,
13
14
  SUBAGENT_KINDS,
14
15
  type AgentModelConfig,
15
16
  type BotLobbyConfig,
17
+ type LobbyPanel,
16
18
  type SubagentKind,
17
19
  } from "../schemas/configuration.ts";
18
20
  import { globalConfigPath, loadConfig, saveConfig } from "../state/project.ts";
@@ -314,6 +316,15 @@ async function editEntry(pi: ExtensionAPI, ctx: ExtensionContext, kind: Settings
314
316
  }
315
317
  }
316
318
 
319
+ /** One agent's settings on their own: the lobby's Quick fix and Plan tabs open their agent's entry directly. */
320
+ export async function openEntrySettings(pi: ExtensionAPI, ctx: ExtensionContext, kind: SettingsKind): Promise<void> {
321
+ if (ctx.mode !== "tui") {
322
+ ctx.ui.notify(`bot-lobby settings live in ${globalConfigPath()}; edit that file outside the TUI.`, "info");
323
+ return;
324
+ }
325
+ await editEntry(pi, ctx, kind);
326
+ }
327
+
317
328
  /**
318
329
  * Subagents never inherit silently: any subagent whose model is still unset is
319
330
  * pinned to the session's current model and saved, so settings always show
@@ -338,6 +349,57 @@ function entryDescription(kind: SettingsKind, view: EntryView): string {
338
349
  return `${view.model} · ${thinking}${limit}${custom}`;
339
350
  }
340
351
 
352
+ /** The lobby's on/off settings as the settings menu lists them; `panel:*` are the Lobby tab's panes. */
353
+ export type LobbySwitch = "autoOpen" | "autoAsk" | "mouse" | "issues" | `panel:${LobbyPanel}`;
354
+
355
+ const PANEL_SWITCH_LABELS: Record<LobbyPanel, string> = { scene: "Zen scene pane", conversation: "Conversation pane", activity: "Activity log pane", thinking: "Thinking pane" };
356
+
357
+ export const LOBBY_SWITCHES: ReadonlyArray<{ id: LobbySwitch; label: string; help: string }> = [
358
+ { id: "autoOpen", label: "Open with a task", help: "open the lobby when this session starts or resumes a task" },
359
+ { id: "autoAsk", label: "Ask at once", help: "put the panel's questions to you as soon as a round ends, while the Plan tab is open" },
360
+ { id: "mouse", label: "Mouse", help: "click tabs and draft lines, scroll with the wheel (shift+drag still selects text)" },
361
+ { id: "issues", label: "Issues tab", help: "the GitHub Issues tab" },
362
+ ...LOBBY_PANELS.map((panel) => ({ id: `panel:${panel}` as const, label: PANEL_SWITCH_LABELS[panel], help: "shown on the Lobby tab; its key in the lobby toggles it too" })),
363
+ ];
364
+
365
+ export function lobbySwitch(config: BotLobbyConfig, id: LobbySwitch): boolean {
366
+ if (id.startsWith("panel:")) return config.lobby.panels[id.slice("panel:".length) as LobbyPanel];
367
+ return config.lobby[id as Exclude<LobbySwitch, `panel:${string}`>];
368
+ }
369
+
370
+ /** The config with one lobby setting flipped. */
371
+ export function toggleLobbySwitch(config: BotLobbyConfig, id: LobbySwitch): BotLobbyConfig {
372
+ const value = !lobbySwitch(config, id);
373
+ if (id.startsWith("panel:")) {
374
+ const panel = id.slice("panel:".length) as LobbyPanel;
375
+ return { ...config, lobby: { ...config.lobby, panels: { ...config.lobby.panels, [panel]: value } } };
376
+ }
377
+ return { ...config, lobby: { ...config.lobby, [id]: value } };
378
+ }
379
+
380
+ /** On/off settings for the lobby; enter flips one and saves it. Key rebinding stays in the file (lobby.keys). */
381
+ async function editLobby(ctx: ExtensionContext): Promise<void> {
382
+ for (;;) {
383
+ const config = loadConfig();
384
+ const items: SelectItem[] = LOBBY_SWITCHES.map((entry) => ({ value: entry.id, label: entry.label, description: `${lobbySwitch(config, entry.id) ? "on" : "off"} · ${entry.help}` }));
385
+ items.push({ value: "back", label: "Back", description: `keys: lobby.keys in ${globalConfigPath()}` });
386
+ const choice = await pick(ctx, "bot-lobby settings · Lobby", items);
387
+ if (!choice || choice === "back") return;
388
+ saveConfig(toggleLobbySwitch(config, choice as LobbySwitch));
389
+ }
390
+ }
391
+
392
+ function lobbySummary(config: BotLobbyConfig): string {
393
+ const hidden = LOBBY_PANELS.filter((panel) => !config.lobby.panels[panel]);
394
+ return [
395
+ config.lobby.autoOpen ? "opens with a task" : "opens on alt+l",
396
+ config.lobby.autoAsk ? "asks at once" : "asks on enter",
397
+ config.lobby.mouse ? "mouse" : "no mouse",
398
+ ...(config.lobby.issues ? ["issues tab"] : []),
399
+ ...(hidden.length > 0 ? [`hidden: ${hidden.join(", ")}`] : []),
400
+ ].join(" · ");
401
+ }
402
+
341
403
  /** Open the per-agent settings editor; every change is written to the global config. */
342
404
  export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
343
405
  if (ctx.mode !== "tui") {
@@ -352,9 +414,11 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
352
414
  for (;;) {
353
415
  const config = loadConfig();
354
416
  const items: SelectItem[] = SETTINGS_KINDS.map((kind) => ({ value: kind, label: kindLabel(kind), description: entryDescription(kind, entryView(config, kind)) }));
417
+ items.push({ value: "lobby", label: "Lobby", description: lobbySummary(config) });
355
418
  items.push({ value: "close", label: "Close" });
356
419
  const choice = await pick(ctx, "bot-lobby settings", items);
357
420
  if (!choice || choice === "close") return;
358
- await editEntry(pi, ctx, choice as SettingsKind);
421
+ if (choice === "lobby") await editLobby(ctx);
422
+ else await editEntry(pi, ctx, choice as SettingsKind);
359
423
  }
360
424
  }
@@ -83,12 +83,26 @@ export function isPanelMember(value: string): value is PanelMember {
83
83
  return (PANEL_MEMBERS as readonly string[]).includes(value);
84
84
  }
85
85
 
86
+ /** Lobby panes that can be shown or hidden (the Lobby tab's scene, conversation, activity log and thinking). */
87
+ export const LOBBY_PANELS = ["scene", "conversation", "activity", "thinking"] as const;
88
+ export type LobbyPanel = (typeof LOBBY_PANELS)[number];
89
+
86
90
  /** The full-screen lobby. */
87
91
  export interface LobbyConfig {
88
92
  /** Open by itself when this session starts or resumes a task. */
89
93
  autoOpen: boolean;
90
94
  /** Who sits on the planning panel next to the oracle, until toggled in the Plan tab. */
91
95
  planningPanel: PanelMember[];
96
+ /** Put the panel's questions to the user as soon as a round ends, while the Plan tab is open. */
97
+ autoAsk: boolean;
98
+ /** Show the GitHub Issues tab (off for now). */
99
+ issues: boolean;
100
+ /** Which panes show; toggled with keys in the lobby and remembered here. */
101
+ panels: Record<LobbyPanel, boolean>;
102
+ /** Key overrides by action name, e.g. `{ "toggleThinking": "alt+t" }`. */
103
+ keys: Record<string, string>;
104
+ /** Clicks and the wheel work in the lobby (click a draft line to comment on it); shift+drag still selects text. */
105
+ mouse: boolean;
92
106
  }
93
107
 
94
108
  export interface BotLobbyConfig {
@@ -135,7 +149,15 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
135
149
  scratchpadMaxParagraphs: 4,
136
150
  scratchpadMaxChars: 2000,
137
151
  },
138
- lobby: { autoOpen: true, planningPanel: [...PANEL_MEMBERS] },
152
+ lobby: {
153
+ autoOpen: true,
154
+ planningPanel: [...PANEL_MEMBERS],
155
+ autoAsk: true,
156
+ issues: false,
157
+ panels: { scene: true, conversation: true, activity: true, thinking: true },
158
+ keys: {},
159
+ mouse: true,
160
+ },
139
161
  };
140
162
 
141
163
  function positive(value: unknown): number | undefined {
@@ -159,14 +181,26 @@ function normalizeScout(override: Partial<ScoutConfig> | undefined): ScoutConfig
159
181
  };
160
182
  }
161
183
 
184
+ function flag(value: unknown, fallback: boolean): boolean {
185
+ return typeof value === "boolean" ? value : fallback;
186
+ }
187
+
162
188
  function normalizeLobby(value: unknown): LobbyConfig {
163
- const source = value as { autoOpen?: unknown; planningPanel?: unknown } | undefined;
189
+ const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown } | undefined;
190
+ const defaults = DEFAULT_CONFIG.lobby;
164
191
  const panel = Array.isArray(source?.planningPanel)
165
192
  ? [...new Set(source.planningPanel.filter((entry): entry is PanelMember => typeof entry === "string" && isPanelMember(entry)))]
166
- : [...DEFAULT_CONFIG.lobby.planningPanel];
193
+ : [...defaults.planningPanel];
194
+ const panels = (source?.panels ?? {}) as Partial<Record<LobbyPanel, unknown>>;
195
+ const keys = source?.keys && typeof source.keys === "object" ? source.keys as Record<string, unknown> : {};
167
196
  return {
168
- autoOpen: typeof source?.autoOpen === "boolean" ? source.autoOpen : DEFAULT_CONFIG.lobby.autoOpen,
197
+ autoOpen: flag(source?.autoOpen, defaults.autoOpen),
169
198
  planningPanel: PANEL_MEMBERS.filter((member) => panel.includes(member)),
199
+ autoAsk: flag(source?.autoAsk, defaults.autoAsk),
200
+ issues: flag(source?.issues, defaults.issues),
201
+ panels: Object.fromEntries(LOBBY_PANELS.map((name) => [name, flag(panels[name], defaults.panels[name])])) as Record<LobbyPanel, boolean>,
202
+ keys: Object.fromEntries(Object.entries(keys).filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].trim().length > 0)),
203
+ mouse: flag(source?.mouse, defaults.mouse),
170
204
  };
171
205
  }
172
206