@a-t-h-i/bot-lobby 0.6.14 → 0.6.16

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 (77) hide show
  1. package/README.md +72 -81
  2. package/package.json +4 -2
  3. package/prompts/master.md +11 -7
  4. package/prompts/panel.md +10 -6
  5. package/prompts/planner.md +13 -10
  6. package/prompts/qa.md +33 -11
  7. package/prompts/reviewer.md +5 -2
  8. package/prompts/worker.md +5 -2
  9. package/src/ask/relay.ts +1 -2
  10. package/src/ask/tool.ts +6 -8
  11. package/src/ask/types.ts +16 -0
  12. package/src/ask/web.ts +63 -0
  13. package/src/classifier/triage.ts +4 -3
  14. package/src/index.ts +7 -1
  15. package/src/lobby/ask.ts +4 -5
  16. package/src/lobby/blocks.ts +57 -0
  17. package/src/lobby/feed.ts +1 -1
  18. package/src/lobby/host.ts +178 -0
  19. package/src/lobby/keys.ts +10 -41
  20. package/src/lobby/planner.ts +4 -4
  21. package/src/lobby/prompt-hub.ts +30 -77
  22. package/src/lobby/prompts.ts +8 -12
  23. package/src/lobby/runtime.ts +49 -310
  24. package/src/lobby/service.ts +54 -134
  25. package/src/lobby/split.ts +1 -1
  26. package/src/lobby/task-rows.ts +28 -0
  27. package/src/pi/commands.ts +14 -20
  28. package/src/pi/model-settings.ts +112 -0
  29. package/src/pi/start-task.ts +1 -1
  30. package/src/pi/tools.ts +7 -8
  31. package/src/pi/ui.ts +0 -9
  32. package/src/schemas/configuration.ts +9 -33
  33. package/src/state/attachments.ts +82 -0
  34. package/src/webui/api/index.ts +17 -15
  35. package/src/webui/api/lobby.ts +4 -3
  36. package/src/webui/api/planner.ts +6 -4
  37. package/src/webui/api/quickfix.ts +4 -3
  38. package/src/webui/api/sessions.ts +9 -7
  39. package/src/webui/api/settings.ts +20 -8
  40. package/src/webui/api/status.ts +4 -15
  41. package/src/webui/api/tasks.ts +11 -15
  42. package/src/webui/command.ts +35 -19
  43. package/src/webui/dev/fake-service.ts +1 -2
  44. package/src/webui/dev/fixtures.ts +2 -5
  45. package/src/webui/events.ts +1 -1
  46. package/src/webui/protocol.ts +22 -11
  47. package/src/webui/server.ts +42 -1
  48. package/src/webui/uploads.ts +68 -0
  49. package/webui/dist/assets/index-h1uo5XHE.js +92 -0
  50. package/webui/dist/assets/index-kCj0-8z_.css +2 -0
  51. package/webui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  52. package/webui/dist/build.json +159 -115
  53. package/webui/dist/index.html +8 -5
  54. package/webui/dist/manifest.webmanifest +9 -4
  55. package/src/ask/dialog.ts +0 -193
  56. package/src/ask/image.ts +0 -202
  57. package/src/ask/png.ts +0 -179
  58. package/src/ask/state.ts +0 -175
  59. package/src/ask/view.ts +0 -163
  60. package/src/lobby/layout.ts +0 -496
  61. package/src/lobby/markdown.ts +0 -121
  62. package/src/lobby/mini.ts +0 -167
  63. package/src/lobby/tabs/excalidraw.ts +0 -95
  64. package/src/lobby/tabs/git.ts +0 -162
  65. package/src/lobby/tabs/home.ts +0 -544
  66. package/src/lobby/tabs/issues.ts +0 -73
  67. package/src/lobby/tabs/knowledge.ts +0 -135
  68. package/src/lobby/tabs/metrics.ts +0 -300
  69. package/src/lobby/tabs/plan.ts +0 -262
  70. package/src/lobby/tabs/quickfix.ts +0 -136
  71. package/src/lobby/tabs/tasks.ts +0 -412
  72. package/src/lobby/theme.ts +0 -30
  73. package/src/lobby/view.ts +0 -3018
  74. package/src/pi/settings-ui.ts +0 -708
  75. package/src/width.ts +0 -102
  76. package/webui/dist/assets/index-CMn1bsps.css +0 -2
  77. package/webui/dist/assets/index-Dj1XZA3O.js +0 -80
package/src/ask/types.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * diagram) shown beside the options.
7
7
  */
8
8
 
9
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+
9
11
  /** The questionnaire tool's name, in this session and in the agents that may ask. */
10
12
  export const ASK_TOOL = "ask_user_question";
11
13
 
@@ -53,3 +55,17 @@ export interface AskResult {
53
55
  cancelled: boolean;
54
56
  globalNote?: string;
55
57
  }
58
+
59
+ /**
60
+ * Puts up to `MAX_QUESTIONS` questions to the user and returns what they
61
+ * chose. `from` names who asks when it is not this session's model (an agent
62
+ * whose questions the oracle relays).
63
+ */
64
+ export type Asker = (questions: readonly AskQuestion[], ctx: ExtensionContext, signal?: AbortSignal, from?: string) => Promise<AskResult>;
65
+
66
+ const IMAGE_EXTENSIONS = /\.(png|jpe?g|gif|webp)$/i;
67
+
68
+ /** Whether a path names an image the page can show. */
69
+ export function isImagePath(path: string): boolean {
70
+ return IMAGE_EXTENSIONS.test(path.trim());
71
+ }
package/src/ask/web.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Putting questions to the user: every question goes to the web page, through
3
+ * the prompt hub, and waits there for the answer. Nothing is drawn in the
4
+ * terminal. A question nobody can answer (no page is served, as in a print
5
+ * run) is put away at once, so a model never waits on a screen that is not
6
+ * there.
7
+ */
8
+ import { isAbsolute, resolve } from "node:path";
9
+ import { currentWebServer } from "../webui/server.ts";
10
+ import { promptHub } from "../lobby/prompt-hub.ts";
11
+ import type { AskAnswer, AskQuestion, AskResult, Asker } from "./types.ts";
12
+
13
+ let availability: (() => boolean) | undefined;
14
+
15
+ /** Whether the page is up to answer. */
16
+ export function canAsk(): boolean {
17
+ return availability ? availability() : currentWebServer() !== undefined;
18
+ }
19
+
20
+ /** Tests say whether a page is there to answer. */
21
+ export function setCanAsk(next: (() => boolean) | undefined): void {
22
+ availability = next;
23
+ }
24
+
25
+ /** Image paths made absolute, so the page's preview route finds them whatever the cwd. */
26
+ function withAbsoluteImages(questions: readonly AskQuestion[], cwd: string): AskQuestion[] {
27
+ return questions.map((question) => ({
28
+ ...question,
29
+ options: question.options.map((option) => (option.image?.trim() && !isAbsolute(option.image.trim()) ? { ...option, image: resolve(cwd, option.image.trim()) } : option)),
30
+ }));
31
+ }
32
+
33
+ /** What the page sent back, kept to the shape of a result; anything else reads as put away. */
34
+ function readResult(value: unknown): AskResult | undefined {
35
+ const raw = value as Partial<AskResult> | null;
36
+ if (!raw || !Array.isArray(raw.answers)) return undefined;
37
+ return {
38
+ answers: raw.answers.filter((answer): answer is AskAnswer => typeof answer === "object" && answer !== null && typeof (answer as AskAnswer).questionIndex === "number"),
39
+ cancelled: raw.cancelled === true,
40
+ ...(typeof raw.globalNote === "string" ? { globalNote: raw.globalNote } : {}),
41
+ };
42
+ }
43
+
44
+ /** The questionnaire in the page. */
45
+ export const askUser: Asker = async (questions, ctx, signal, from) => {
46
+ if (questions.length === 0 || !canAsk()) return { answers: [], cancelled: true };
47
+ const outcome = await promptHub.ask("questionnaire", from ?? "oracle", { questions: withAbsoluteImages(questions, ctx.cwd) }, signal ? { signal } : undefined);
48
+ return (outcome.how === "answered" ? readResult(outcome.value) : undefined) ?? { answers: [], cancelled: true };
49
+ };
50
+
51
+ /** A free-text answer (possibly several lines) in the page; undefined when put away. */
52
+ export async function askText(question: string, from = "orchestrate"): Promise<string | undefined> {
53
+ if (!canAsk()) return undefined;
54
+ const outcome = await promptHub.ask("text", from, { question });
55
+ return outcome.how === "answered" && typeof outcome.value === "string" ? outcome.value : undefined;
56
+ }
57
+
58
+ /** One of `options` picked in the page; undefined when put away. */
59
+ export async function askChoice(title: string, options: string[], from = "orchestrate"): Promise<string | undefined> {
60
+ if (!canAsk()) return undefined;
61
+ const outcome = await promptHub.ask("choose", from, { title, options });
62
+ return outcome.how === "answered" && typeof outcome.value === "string" ? outcome.value : undefined;
63
+ }
@@ -208,13 +208,14 @@ export function triageLine(triage: TaskTriage): string {
208
208
 
209
209
  /**
210
210
  * A clarify question the classifier can answer: its pick must be the
211
- * recommended option (marked `(Recommended)`, else the first), confident and
212
- * clearly ahead; otherwise the question goes to the user as before.
211
+ * recommended option (marked `(Recommended)`; an unmarked question always goes
212
+ * to the user), confident and clearly ahead.
213
213
  */
214
214
  export async function answerClarify(classifier: Classifier, question: string, options: readonly string[], context: { request: string; notes: string; proposal?: string }, signal?: AbortSignal): Promise<AutoAnswer | undefined> {
215
215
  if (options.length < 2 || !classifier.enabled("answers")) return undefined;
216
216
  const labels = options.map((option) => option.replace(/\s*\(recommended\)\s*/i, " ").trim());
217
217
  const marked = options.findIndex((option) => /\(recommended\)/i.test(option));
218
- const decided = await autoAnswer(classifier, [{ index: 0, from: "MASTER", text: question, options: labels.map((label) => ({ label, description: "" })), recommended: labels[marked >= 0 ? marked : 0]! }], { request: context.request, conversation: context.notes, ...(context.proposal ? { draft: context.proposal } : {}) }, signal);
218
+ if (marked < 0) return undefined;
219
+ const decided = await autoAnswer(classifier, [{ index: 0, from: "MASTER", text: question, options: labels.map((label) => ({ label, description: "" })), recommended: labels[marked]! }], { request: context.request, conversation: context.notes, ...(context.proposal ? { draft: context.proposal } : {}) }, signal);
219
220
  return decided?.[0];
220
221
  }
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ import { registerLifecycle } from "./pi/events.ts";
4
4
  import { registerOrchestrateTool } from "./pi/tools.ts";
5
5
  import { registerRouteTool } from "./pi/route.ts";
6
6
  import { onTransition } from "./state/task-state.ts";
7
+ import { releaseAttachments } from "./state/attachments.ts";
8
+ import { TERMINAL_STATES } from "./schemas/task.ts";
7
9
  import { pingTransition } from "./pi/notify.ts";
8
10
  import { isSubagentProcess } from "./pi/quiet.ts";
9
11
  import { registerDeskClient } from "./desk/client-extension.ts";
@@ -25,7 +27,11 @@ export default function (pi: ExtensionAPI): void {
25
27
  // After the lifecycle, so the lobby opens over a task the status already knows.
26
28
  registerLobbyEvents(pi, CONFIG_DIR_NAME);
27
29
  registerOwner(pi, CONFIG_DIR_NAME);
28
- onTransition((task) => pingTransition(task));
30
+ // A finished task takes the files attached during it along.
31
+ onTransition((task) => {
32
+ pingTransition(task);
33
+ if (TERMINAL_STATES.includes(task.state)) releaseAttachments(task.id);
34
+ });
29
35
  registerCommands(pi, CONFIG_DIR_NAME);
30
36
  registerOrchestrateTool(pi, CONFIG_DIR_NAME);
31
37
  // A new request one agent can do alone: the oracle confirms, and the lobby hands it to the quick-fix agent.
package/src/lobby/ask.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * The oracle puts the planning panel's questions to the user through the
3
- * questionnaire (`../ask`): a tab per question with each seat's options, the
4
- * recommended one first, and a row for an answer in the user's own words.
3
+ * questionnaire (`../ask`): a card per question with each seat's options (the
4
+ * recommended one first when the answer is obvious), and a field for an answer in the user's own words.
5
5
  * The panel's questions become questionnaires of at most four, and the
6
6
  * answers become the user's turn for the next round.
7
7
  */
8
8
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import type { PanelQuestion, SettledQuestion } from "./planner.ts";
10
- import { MAX_HEADER, MAX_LABEL, MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, RESERVED, type AskAnswer, type AskOption, type AskQuestion, type AskResult } from "../ask/types.ts";
11
- import type { Asker } from "../ask/dialog.ts";
10
+ import { MAX_HEADER, MAX_LABEL, MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, RESERVED, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type Asker } from "../ask/types.ts";
12
11
 
13
12
  export { MAX_QUESTIONS, MIN_OPTIONS, MAX_OPTIONS, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type Asker };
14
- export { askUser, dialogAsker } from "../ask/dialog.ts";
13
+ export { askUser } from "../ask/web.ts";
15
14
 
16
15
  function clip(text: string, max: number): string {
17
16
  const flat = text.replace(/\s+/g, " ").trim();
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Plain Markdown helpers the lobby shares: model text with escaped line
3
+ * breaks made real, and a reply split into blocks that can be rendered one by
4
+ * one (the page renders them, not the terminal).
5
+ */
6
+
7
+ /**
8
+ * Line breaks a model wrote out as `\n` (text escaped twice on its way here,
9
+ * so a whole plan arrives as one line): made real when the text has no real
10
+ * line break at all, outside inline code, where `\n` is usually meant.
11
+ */
12
+ export function unescapeBreaks(text: string): string {
13
+ if (text.includes("\n") || !text.includes("\\n")) return text;
14
+ return text
15
+ .split(/(`[^`\n]*`)/)
16
+ .map((part, index) => (index % 2 === 1 ? part : part.replace(/(?:\\r)?\\n/g, "\n")))
17
+ .join("");
18
+ }
19
+
20
+ /** A fenced code block's opening or closing line. */
21
+ const FENCE = /^ {0,3}(`{3,}|~{3,})/;
22
+ /** A list item's first line: after a blank line it continues the list above, so blocks never split there. */
23
+ const LIST_ITEM = /^(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
24
+
25
+ /**
26
+ * Markdown split into blocks that render, one by one, into the same output as
27
+ * the whole (each block's lines, a blank line between blocks, blocks that
28
+ * render to nothing left out). A split falls on a blank line outside code
29
+ * fences that is followed by an unindented line that does not start a list
30
+ * item — the only places where one block can never run into the next. A
31
+ * streaming reply can then keep its finished blocks rendered and render only
32
+ * the one still being written.
33
+ */
34
+ export function markdownBlocks(text: string): string[] {
35
+ const lines = text.split("\n");
36
+ const blocks: string[] = [];
37
+ let fence: string | undefined;
38
+ let start = 0;
39
+ for (let index = 0; index < lines.length; index += 1) {
40
+ const line = lines[index]!;
41
+ const marker = FENCE.exec(line)?.[1];
42
+ if (marker) {
43
+ if (!fence) fence = marker;
44
+ else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
45
+ continue;
46
+ }
47
+ if (fence || line.trim() || index === 0) continue;
48
+ const next = lines[index + 1];
49
+ if (next === undefined || !/^\S/.test(next) || LIST_ITEM.test(next)) continue;
50
+ const block = lines.slice(start, index).join("\n");
51
+ if (block.trim()) blocks.push(block);
52
+ start = index + 1;
53
+ }
54
+ const last = lines.slice(start).join("\n");
55
+ if (last.trim()) blocks.push(last);
56
+ return blocks;
57
+ }
package/src/lobby/feed.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { AgentRun } from "../schemas/findings.ts";
9
9
  import { agentName } from "../pi/run-summary.ts";
10
- import { markdownBlocks } from "./markdown.ts";
10
+ import { markdownBlocks } from "./blocks.ts";
11
11
 
12
12
  export type ActivityKind = "info" | "success" | "warning" | "error";
13
13
 
@@ -0,0 +1,178 @@
1
+ /**
2
+ * What the lobby's backend offers: the service interface the web server
3
+ * drives, and the shapes of the sessions it lists. Pure types (the
4
+ * implementation is `service.ts`).
5
+ */
6
+ import type { Task } from "../schemas/task.ts";
7
+ import type { AgentRun } from "../schemas/findings.ts";
8
+ import type { PlannedTask } from "../state/backlog.ts";
9
+ import type { PlanComment } from "../state/comments.ts";
10
+ import type { MetricRecord } from "../state/metrics.ts";
11
+ import type { BotLobbyConfig, LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
12
+ import type { ChatEntry, LobbyFeed } from "./feed.ts";
13
+ import type { BackgroundSession } from "./sessions.ts";
14
+ import type { QuickFixQueue } from "./quickfix.ts";
15
+ import type { PlannerSeed, PlanningSession } from "./planner.ts";
16
+ import type { IssuesState } from "./issues.ts";
17
+ import type { PullsState } from "./pulls.ts";
18
+ import type { PullReviews } from "./pr-review.ts";
19
+ import type { KnowledgeBook } from "./knowledge.ts";
20
+ import type { ExcalidrawBook, ExcalidrawSession } from "../excalidraw/sessions.ts";
21
+ import type { WorkspaceInfo } from "../execution/workspace.ts";
22
+
23
+ export { TAB_IDS, TAB_LABELS, visibleTabs } from "./prompts.ts";
24
+ export type { TabId } from "./prompts.ts";
25
+
26
+ /** What a checked Excalidraw session reported. */
27
+ export interface SessionCheck {
28
+ ok: boolean;
29
+ text: string;
30
+ }
31
+
32
+ /**
33
+ * Which session the Lobby tab shows and talks to: this window, one it started
34
+ * in the background, one running in another terminal, or a task whose
35
+ * session is not running at all.
36
+ */
37
+ export type SessionView =
38
+ | { kind: "here" }
39
+ | { kind: "background"; key: string }
40
+ | { kind: "other"; sessionId: string }
41
+ | { kind: "idle"; taskId: string };
42
+
43
+ export type SessionWhere = "this window" | "background" | "other terminal" | "not running";
44
+
45
+ /** One row of the session browser. */
46
+ export interface SessionEntry {
47
+ view: SessionView;
48
+ name: string;
49
+ where: SessionWhere;
50
+ task?: Task;
51
+ /** What it is doing: working, idle, starting, ended, or its task's state. */
52
+ status: string;
53
+ auto: boolean;
54
+ /** Questions it waits on you for. */
55
+ waiting: number;
56
+ /** The pi session it is (absent for a task no session ever owned). */
57
+ sessionId?: string;
58
+ /** The process running it, for sessions in other terminals. */
59
+ pid?: number;
60
+ }
61
+
62
+ /** A session running in this project, as its heartbeat tells (see state/presence.ts). */
63
+ export interface LiveSession {
64
+ sessionId: string;
65
+ pid: number;
66
+ name?: string;
67
+ taskId?: string;
68
+ mode: string;
69
+ }
70
+
71
+ /** What `switchTo` needs: the session to run in this window, and the background process to stop first. */
72
+ export interface SwitchTarget {
73
+ name: string;
74
+ sessionId?: string;
75
+ background?: BackgroundSession;
76
+ /** A task no session owns: this window claims it instead. */
77
+ claimTaskId?: string;
78
+ }
79
+
80
+ /** One model Pi offers, for the settings page's picker: `provider/id`, its label and thinking levels. */
81
+ export interface ModelChoice {
82
+ id: string;
83
+ label: string;
84
+ thinkingLevels: string[];
85
+ }
86
+
87
+ /** Everything the web server needs from pi and bot-lobby. */
88
+ export interface LobbyService {
89
+ sessionId(): string | undefined;
90
+ /** This session's active task and its runs, as the lobby reads them. */
91
+ zen(): { task?: Task; runs: readonly AgentRun[] };
92
+ feed: LobbyFeed;
93
+ masterBusy(): boolean;
94
+ tasks(): Task[];
95
+ plans(): PlannedTask[];
96
+ comments(taskId: string): PlanComment[];
97
+ metrics(): MetricRecord[];
98
+ /** Classifier calls (kind `classifier`), kept out of the agent tables. */
99
+ classifierMetrics?(): MetricRecord[];
100
+ /** Send text to the oracle, or start a task when none is active; returns a notice. */
101
+ toOracle(text: string): string | undefined;
102
+ comment(taskId: string, text: string): string;
103
+ startPlanned(plan: PlannedTask): string;
104
+ discardPlan(id: string): void;
105
+ abortMaster(): void;
106
+ quickfix: QuickFixQueue;
107
+ planner(): PlanningSession | undefined;
108
+ /** Start a planning session (replacing any other) with these seats on the panel. */
109
+ newPlanner(seed?: PlannerSeed, seats?: readonly PanelMember[]): PlanningSession;
110
+ /** The oracle puts the panel's open questions to the user, one questionnaire at a time; returns a notice. */
111
+ answerPanel(): Promise<string>;
112
+ /** Save the plan as a pending task, offering to split a long one into several first; returns a notice. */
113
+ savePlan(): Promise<string>;
114
+ /** The seats a new session starts with, from settings. */
115
+ defaultPanel(): readonly PanelMember[];
116
+ /** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
117
+ planningRounds?(): number;
118
+ /** `model · thinking` a panel seat runs on. */
119
+ seatLabel(member: PanelMember): string;
120
+ issues: IssuesState;
121
+ /** The Git tab's pull requests, read through `gh`. */
122
+ pulls: PullsState;
123
+ /** Reviews of pull requests by an agent, and Jev's reads of them. */
124
+ reviews: PullReviews;
125
+ /** Every agent's knowledge files: read, edited, commented on. */
126
+ knowledge: KnowledgeBook;
127
+ /** The shared Excalidraw sessions (five at most) and which agents each is assigned to. */
128
+ excalidraw: ExcalidrawBook;
129
+ /** Join a session's room for a moment and report what is there. */
130
+ checkExcalidraw(session: ExcalidrawSession): Promise<SessionCheck>;
131
+ /** The effective config, never a secret; the web settings page reads it. */
132
+ config?(): BotLobbyConfig;
133
+ /** Save a whole config, the way the settings menu does; the web settings page writes it. */
134
+ saveConfig?(config: BotLobbyConfig): void;
135
+ /** Reload cached config (keys, panes) after a settings change. */
136
+ configChanged?(): void;
137
+ /** The models Pi offers, for the settings page's picker. */
138
+ models?(): ModelChoice[];
139
+ /** The model this window's session runs on (`provider/id`), when it has one. */
140
+ sessionModel?(): string | undefined;
141
+ /** The Issues tab is switched on (`lobby.issues`). */
142
+ issuesEnabled(): boolean;
143
+ /** This window's pi session name, when it has one. */
144
+ sessionName(): string | undefined;
145
+ /** Background sessions this window started, oldest first. */
146
+ sessions(): readonly BackgroundSession[];
147
+ /** Start a task in a new background session, named after the task; the session, or why not. */
148
+ startSession(start: { request?: string; plan?: PlannedTask; auto?: boolean }): BackgroundSession | string;
149
+ /** Auto mode for any task, whichever session drives it. */
150
+ isAuto(taskId: string): boolean;
151
+ setAuto(taskId: string, on: boolean): void;
152
+ /** Leave a message for a task's oracle, delivered by whichever session drives it (now or once one resumes it); returns a notice. */
153
+ sendToTask(taskId: string, text: string): string;
154
+ /** Leave a message for a session running in another terminal; it delivers it within seconds. Returns a notice. */
155
+ sendToSession(sessionId: string, text: string): string;
156
+ /** Sessions running in this project, other than this window's, from their heartbeats. */
157
+ liveSessions(): readonly LiveSession[];
158
+ /** Run another session in this window (stopping its background process first); resolves to a notice. */
159
+ switchTo(target: SwitchTarget): Promise<string>;
160
+ /** Archived tasks, most recently archived first. */
161
+ archivedTasks(): Task[];
162
+ /** Archive, restore or delete a task; each returns a notice. */
163
+ archiveTask(taskId: string): string;
164
+ restoreTask(taskId: string): string;
165
+ deleteTask(taskId: string, where: "list" | "archive"): string;
166
+ /** Another session's conversation, read from its saved session file (its newest messages). */
167
+ sessionChat(sessionId: string): ChatEntry[];
168
+ /** Whether another session has messages earlier than `sessionChat` returns. */
169
+ hasOlderChat(sessionId: string): boolean;
170
+ /** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
171
+ chatHistory(sessionId?: string): readonly ChatEntry[];
172
+ profileLabel(kind: LobbyAgentKind): string;
173
+ /** The repository (or folder) and branch the title shows; the folder's name alone until git has answered. */
174
+ workspace?(): WorkspaceInfo;
175
+ /** Ask for the workspace to be read again (the branch may have changed); it arrives through `workspace()`. */
176
+ refreshWorkspace?(): void;
177
+ }
178
+
package/src/lobby/keys.ts CHANGED
@@ -1,27 +1,17 @@
1
1
  /**
2
- * The lobby's shortcuts in one table: every action has a default key, a line
3
- * for the help overlay, and can be rebound under `lobby.keys` in the config
4
- * (`{ "toggleThinking": "alt+t" }`). They work in typing and browsing mode
5
- * alike, so each default is a key that never types a character.
2
+ * The web lobby's shortcuts in one table: every action has a default key, a
3
+ * line for the help overlay, and can be rebound under `lobby.keys` in the
4
+ * config (`{ "search": "alt+f" }`). Chords are written `alt+k`,
5
+ * `ctrl+f`, `shift+tab`; the page matches them on the physical key.
6
6
  */
7
- import { Key, matchesKey, type KeyId } from "@earendil-works/pi-tui";
8
7
 
9
8
  export const LOBBY_ACTIONS = {
10
- hide: { key: "alt+l", help: "hide the lobby (back to pi)" },
11
- help: { key: "alt+h", help: "show or hide these keys" },
12
- settings: { key: "alt+s", help: "bot-lobby settings: each agent's model and thinking, the lobby" },
13
- search: { key: "ctrl+f", help: "search the current tab" },
14
- savePlan: { key: "ctrl+s", help: "save the plan being made in the Plan tab to the pending tasks" },
15
- sessions: { key: "alt+o", help: "browse sessions: view one, message it, or switch this window to it" },
16
- newSession: { key: "alt+n", help: "start a task in a new session, named after it" },
17
- toggleAuto: { key: "alt+g", help: "auto mode: the oracle drives the task to completion without asking" },
18
- nextTab: { key: "tab", help: "next tab" },
19
- prevTab: { key: "shift+tab", help: "previous tab" },
20
- toggleConversation: { key: "alt+c", help: "show or hide the conversation" },
21
- toggleActivity: { key: "alt+a", help: "show or hide the activity log" },
22
- toggleThinking: { key: "alt+k", help: "show or hide thinking" },
23
- scrollUp: { key: "pageUp", help: "scroll up a page" },
24
- scrollDown: { key: "pageDown", help: "scroll down a page" },
9
+ help: { key: "alt+h", help: "show or hide this list" },
10
+ settings: { key: "alt+s", help: "settings: each agent's model and effort" },
11
+ savePlan: { key: "ctrl+s", help: "save the plan from the Plan tab" },
12
+ sessions: { key: "alt+o", help: "browse and message your sessions" },
13
+ nextTab: { key: "alt+]", help: "next tab" },
14
+ prevTab: { key: "alt+[", help: "previous tab" },
25
15
  } as const;
26
16
 
27
17
  export type LobbyAction = keyof typeof LOBBY_ACTIONS;
@@ -37,27 +27,6 @@ export function keyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap
37
27
  return map;
38
28
  }
39
29
 
40
- /** Alt+digit jumps to the tab at `index`; the same key the TUI matches. */
41
- export function tabJumpKey(index: number): KeyId {
42
- return Key.alt(String(index + 1) as "1");
43
- }
44
-
45
- /** `keyMap` with the web-only tab cycling defaults; user overrides win. */
46
- export function webKeyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap {
47
- const map = keyMap(overrides);
48
- if (!overrides.nextTab?.trim()) map.nextTab = "alt+]";
49
- if (!overrides.prevTab?.trim()) map.prevTab = "alt+[";
50
- return map;
51
- }
52
-
53
- /** The action `data` triggers under `map`, if any. */
54
- export function actionFor(data: string, map: KeyMap): LobbyAction | undefined {
55
- for (const action of Object.keys(map) as LobbyAction[]) {
56
- if (matchesKey(data, map[action] as KeyId)) return action;
57
- }
58
- return undefined;
59
- }
60
-
61
30
  /** `alt+k` reads `Alt+K` in hints and help. */
62
31
  export function keyLabel(key: string): string {
63
32
  return key
@@ -173,10 +173,10 @@ export function roundMode(round: number, limit: number): RoundMode {
173
173
  /** What the oracle is told to do at the end of its task, by round mode. */
174
174
  export function oracleClosing(mode: RoundMode, round: number, limit: number): string {
175
175
  if (mode === "final") {
176
- return `Final round (${round} of ${limit}): no seat runs this round and nothing more is asked. Fold the user's answers into the plan, decide every point still open with its recommended option and list each under ### Assumptions, and set Status READY. Omit the Questions section. Reply in the required output format.`;
176
+ return `Final round (${round} of ${limit}): no seat runs this round and nothing more is asked. Fold the user's answers into the plan, decide every point still open (with its marked option where there is one, otherwise your best call) and list each under ### Assumptions, and set Status READY. Omit the Questions section. Reply in the required output format.`;
177
177
  }
178
178
  if (mode === "revise") {
179
- return `The planning round limit (${limit}) is reached: revise the plan for the user's latest message and comments. Ask nothing; decide anything open with its recommended option under ### Assumptions, and keep Status READY. Omit the Questions section. Reply in the required output format.`;
179
+ return `The planning round limit (${limit}) is reached: revise the plan for the user's latest message and comments. Ask nothing; decide anything open (with its marked option where there is one, otherwise your best call) under ### Assumptions, and keep Status READY. Omit the Questions section. Reply in the required output format.`;
180
180
  }
181
181
  const bound = limit > 0 ? `Round ${round} of ${limit}; in round ${limit} you settle whatever is still open alone, so ask the decisive questions now. ` : "";
182
182
  return `${bound}Continue: fold the panel's notes and the user's answers into the plan, ask what no seat owns, or declare the plan READY. Reply in the required output format.`;
@@ -225,9 +225,9 @@ export function parseOption(text: string): PanelOption {
225
225
  return split ? { label: split[1]!.trim(), description: split[2]!.trim() } : { label: flat, description: "" };
226
226
  }
227
227
 
228
- /** The option the asker recommends: the one marked `(Recommended)`, else the first. */
228
+ /** The option the asker recommends: only the one marked `(Recommended)`; nothing is implied by order. */
229
229
  export function recommendedOption(question: AskedQuestion): PanelOption | undefined {
230
- return question.options.find((option) => /\(recommended\)/i.test(option.label)) ?? question.options[0];
230
+ return question.options.find((option) => /\(recommended\)/i.test(option.label));
231
231
  }
232
232
 
233
233
  /** An option's label without its `(Recommended)` marker. */