@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.
package/README.md CHANGED
@@ -67,14 +67,17 @@ recorded in the task.
67
67
  pi install npm:@juicesharp/rpiv-ask-user-question
68
68
  ```
69
69
 
70
- It is optional. Without it `clarify` still works through Pi's built-in
71
- `select`/`input` prompts (or the Master asks in plain text), just with less
72
- structure.
70
+ It is optional for the Master. Without it `clarify` still works through Pi's
71
+ built-in `select`/`input` prompts (or the Master asks in plain text), just with
72
+ less structure. The lobby's planning panel uses the same questionnaire on its
73
+ own — the library ships as a bot-lobby dependency, so the panel's questions
74
+ arrive one at a time with options whether or not you install the tool for the
75
+ Master (see [The lobby](#the-lobby)).
73
76
 
74
77
  ## Usage
75
78
 
76
79
  ```
77
- /bot-lobby Open the lobby (alt+l): tasks, planning, quick fixes, issues, metrics
80
+ /bot-lobby Open the lobby (alt+l): tasks, planning, quick fixes, metrics
78
81
  /bot-lobby <request> Start a task and hand it to the Master
79
82
  /bot-lobby status [taskId] Active task, state, approvals, blockers, legal next states
80
83
  /bot-lobby tasks Task list (plus any unreadable task state)
@@ -96,26 +99,27 @@ structure.
96
99
  ## The lobby
97
100
 
98
101
  The lobby is bot-lobby's full-screen home: a tabbed view over every task in the
99
- project, your planning, quick fixes, GitHub issues and model performance, with
100
- one prompt at the bottom whose target follows the tab. It opens by itself when
102
+ project, your planning, quick fixes and model performance, with one prompt at
103
+ the bottom whose target follows the tab. It opens by itself when
101
104
  this session starts (or resumes) a task — the small zen widget returns whenever
102
105
  you hide it — and `alt+l` or `/bot-lobby` opens and hides it at any time, with
103
106
  or without a task.
104
107
 
105
108
  ```
106
- ◆ bot-lobby │ 1 Lobby 2 Tasks 2 3 Plan 4 Quick fix ⠋ 5 Issues 6 Metrics ⠋ TASK-add-login implementing
107
- ───────────────────────────────────────────────────────────────────────────────────────────────────────────
109
+ ◆ bot-lobby │ 1 Lobby 2 Tasks 2 3 Plan 2? 4 Quick fix ⠋ 5 Metrics ⠋ TASK-add-login implementing Alt+H keys
108
110
  (the zen scene: the oracle, DEV · DESIGN · RESEARCH · QA, the plan checklist)
109
- ── Conversation · TASK-add-login ──────────────────── ┬ ── Activity ───────────────────────────────────────
110
- you ▸ add a login page with email + password │ 12:04 MASTER ✓ scouting designer, backend
111
- oracle ▸ Proposal: │ 12:06 DEV ⠋ reading auth.ts…
112
- - LoginForm component │ 12:06 DESIGN ⠋ editing LoginForm.tsx…
113
- - POST /api/login with rate limiting │ 12:06 QUICK FIX ✓ done: rename getUser
114
- ── Thinking ───────────────────────────────────────────────────────────────────────── DEV · 12s ago ──
115
- The auth module already exposes a session helper; reuse it rather than adding a new one.
116
- ── message the oracle ─────────────────────────────────────────────────────────────────────────────────
111
+ ╭ Conversation · TASK-add-login ─────────────── Alt+C ╮ ╭ Activity ──────────────────────────────── Alt+A ╮
112
+ │ you ▸ add a login page with email + password │ │ 12:04 MASTER ✓ scouting designer, backend │
113
+ │ oracle ▸ Proposal │ │ 12:06 DEV ⠋ reading auth.ts… │
114
+ │ • LoginForm component │ │ 12:06 DESIGN ⠋ editing LoginForm.tsx… │
115
+ │ • POST /api/login with rate limiting │ │ 12:06 QUICK FIX ✓ done: rename getUser │
116
+ ╰──────────────────────────────────────────────────────╯ ╰─────────────────────────────────────────────────╯
117
+ ╭ Thinking ────────────────────────────────────────────────────────────────────────────────── DEV · 12s ago ╮
118
+ │ The auth module already exposes a session helper; reuse it rather than adding a new one. │
119
+ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
120
+ ── message the oracle ───────────────────────────────────────────────────────────────────────────────────────
117
121
  _
118
- TYPE enter send · shift+enter newline · esc browse · tab next tab · alt+l hide lobby
122
+ TYPE enter send shift+enter newline esc browse tab next tab alt+h keys alt+l hide
119
123
  ```
120
124
 
121
125
  - **1 Lobby** — the task's zen scene, then the conversation with the oracle
@@ -126,7 +130,12 @@ or without a task.
126
130
  thoughts show up: the oracle's live thought as it streams, and each finished
127
131
  thought from a subagent, quick fix or the planner (pi's own transcript,
128
132
  behind the lobby, still carries the oracle's thinking blocks; `ctrl+t`
129
- collapses them there). The prompt talks to the
133
+ collapses them there). The oracle's replies render as Markdown. Every pane
134
+ can be hidden and brought back — `alt+z` the scene, `alt+c` the
135
+ conversation, `alt+a` the activity log, `alt+k` thinking — and the rest take
136
+ its room; the choice is remembered (`lobby.panels`). Each pane scrolls on
137
+ its own (see **Scrolling** below), and a pane scrolled back stays on what
138
+ you are reading while new lines arrive. The prompt talks to the
130
139
  oracle (while it works, enter steers the running turn; `esc` stops it); with
131
140
  no task, it starts one.
132
141
  - **2 Tasks** — every task in the project: this session's, the ones other pi
@@ -145,41 +154,105 @@ or without a task.
145
154
  chairs on the Planner model: it reads the seats' questions and notes, folds
146
155
  every answer into the draft plan (with a *Decisions by domain* section) and
147
156
  asks only what no single seat owns. Each round the seats run in parallel,
148
- read-only, then the oracle; the questions arrive numbered and attributed
149
- (`3. QA Which browsers must pass?`), you answer them all in one message,
150
- and every seat reads every answer the next round — so the agents that later
151
- build the task start aligned. A roster shows what each seat is doing and
152
- whether it is READY; the plan is READY only when every seat and the oracle
153
- agree, and the draft pane lists what each seat said the plan must respect.
154
- While browsing, `1`–`4` seat or unseat DEV, DESIGN, QA and RESEARCH for the
155
- next round, `enter` switches between the conversation and the draft, `s`
156
- saves the plan to the pending tasks list, `n` starts over, `r` retries a
157
- round that failed or lost a seat, and `x` stops one.
157
+ read-only, then the oracle. Every question comes with two to four options,
158
+ the seat's recommendation first, and the oracle puts them to you **one at a
159
+ time** through the ask-user-question questionnaire: a tab per question
160
+ labelled with the seat that asked it (`QA`, `DEV`…), its options with what
161
+ each means, and a row to type your own answer or add a note (four questions
162
+ per questionnaire; more follow in the next one). It opens by itself when a
163
+ round ends while the Plan tab is showing (`lobby.autoAsk`), and otherwise
164
+ when you press `enter` on the empty prompt or `a` while browsing; `esc` puts
165
+ it away with your answers so far kept, and `enter` resumes. Your answers go
166
+ back attributed (`3. [QA] Which browsers must pass? → evergreen only`), and
167
+ every seat reads every answer the next round — so the agents that later
168
+ build the task start aligned. You can still type a free reply instead.
169
+ Without the library the same questions come through pi's own select and
170
+ input dialogs. A roster shows what each seat is doing and whether it is
171
+ READY; the plan is READY only when every seat and the oracle agree. The
172
+ draft plan renders as Markdown (headings, lists, code, tables) beside the
173
+ conversation, followed by what each seat said the plan must respect.
174
+ **Comment on any line of the draft**: click it, or press `enter` to move to
175
+ the draft, pick a line with `↑↓` and press `c`, then type the comment. The
176
+ line is marked `◆` with your comment beneath it, and the comment goes to the
177
+ panel with your answers — or starts a round by itself when no question is
178
+ open. While browsing, `1`–`4` seat or unseat DEV, DESIGN, QA and RESEARCH
179
+ for the next round, `s` saves the plan to the pending tasks list, `n` starts
180
+ over, `r` retries a round that failed or lost a seat, `x` stops one, and `m`
181
+ opens the oracle's (Planner) settings.
158
182
  - **4 Quick fix** — a direct prompt, the way you would ask pi, that skips the
159
183
  whole workflow: one coding agent (full tools) makes the change right away
160
184
  while any task keeps running. Quick fixes run one at a time in the order you
161
185
  send them; each shows its steps and final report, and `x` cancels one. A
162
186
  request that turns out to be large is reported back instead of attempted.
163
- - **5 Issues** — the repository's open GitHub issues through the `gh` CLI (it
164
- owns sign-in; bot-lobby stores no token). `enter` reads one with its
165
- comments, `n` files a new one (first line is the title), `r` refreshes, and
166
- `p` plans it: the Plan tab opens seeded with the issue, and the saved plan
167
- keeps a link to it, so an issue becomes a task only after it has been
168
- planned.
169
- - **6 Metrics** — model performance across every Master turn, subagent run,
170
- quick fix, planning seat and oracle planning turn: per model and thinking level, the number of runs,
171
- success rate, mean/median/p90 time, turns, tools, tokens, output tokens per
172
- second and cost (columns drop from the right on narrow terminals); how long a
173
- task takes from request to done by the oracle's model and thinking level; and
174
- where the time goes by agent. `g` splits the table by agent, `s` cycles the
175
- sort (runs, average time, success, cost).
187
+ `m` opens the quick fix agent's settings — model, thinking level, time
188
+ limit and instructions — right there (the same entry as in
189
+ `/bot-lobby settings`); the tab shows what it runs on.
190
+ - **5 Metrics** — model performance across every Master turn, subagent run,
191
+ quick fix, planning seat and oracle planning turn, as a dashboard: tiles for
192
+ runs (with a sparkline of recent run times), success rate, average and p90
193
+ run time, cost and tasks; average run time per model and thinking level as
194
+ bars; success rate per model as meters marked `✓` (≥90%), `!` (≥70%) or `✗`;
195
+ where the time goes as one bar split by agent, with a legend, and how long a
196
+ task takes from request to done by the oracle's model; then the full table —
197
+ runs, success, mean/median/p90 time, turns, tools, tokens, output tokens per
198
+ second and cost (columns drop from the right on narrow terminals). `g`
199
+ splits the table by agent, `s` cycles the sort (runs, average time, success,
200
+ cost).
201
+
202
+ The **Issues** tab (GitHub issues through the `gh` CLI, planned into tasks
203
+ through the Plan tab) is switched off for now; `"lobby": { "issues": true }`
204
+ brings it back as tab 5.
176
205
 
177
206
  **Keys.** Like a modal editor, the lobby has a typing mode (keys go to the
178
207
  prompt) and a browsing mode (`esc`; arrows move through lists, single keys run
179
208
  the tab's commands, and on Lobby, Plan and Quick fix any other key resumes
180
- typing). Everywhere: `tab`/`shift+tab` or `alt+1`…`alt+6` switch tabs,
181
- `pageup`/`pagedown` scroll, `ctrl+c` clears the prompt (or hides the lobby
182
- when it is empty) and `alt+l` hides the lobby. Anything that needs pi itself —
209
+ typing). These work in both modes:
210
+
211
+ | Key | Does |
212
+ | --- | --- |
213
+ | `alt+l` | hide the lobby (back to pi) |
214
+ | `alt+h` (or `?` while browsing) | show every key, and the current tab's |
215
+ | `alt+s` | bot-lobby settings: every agent's model, thinking and time limit, and the lobby's switches |
216
+ | `ctrl+f` (or `/` while browsing) | search the current tab |
217
+ | `tab` / `shift+tab`, `alt+1`…`alt+5` | switch tabs |
218
+ | `alt+z` / `alt+c` / `alt+a` / `alt+k` | show or hide the zen scene / conversation / activity log / thinking |
219
+ | `pageup` / `pagedown` | scroll the focused pane a page |
220
+ | `ctrl+c` | clear the prompt, or hide the lobby when it is empty |
221
+
222
+ Every shortcut can be rebound under `lobby.keys` in the config, by action name:
223
+ `hide`, `help`, `settings`, `search`, `nextTab`, `prevTab`, `toggleScene`,
224
+ `toggleConversation`, `toggleActivity`, `toggleThinking`, `scrollUp`,
225
+ `scrollDown` — e.g. `"keys": { "toggleThinking": "alt+t" }`. Pick keys that
226
+ never type a character (`alt+…`, `ctrl+…`, `f1`…).
227
+
228
+ **Scrolling.** Every pane scrolls on its own and shows a scrollbar in its
229
+ right border when it holds more than fits. While browsing, `←`/`→` move
230
+ between the tab's panes (the conversation, activity log and thinking on
231
+ Lobby; the conversation and draft on Plan; the list and detail on Tasks and
232
+ Quick fix) and the focused one lights up; `↑`/`↓` scroll it a line (or move
233
+ a list's selection, or the draft's cursor), `pageup`/`pagedown` a page, and
234
+ `home`/`end` jump to its oldest line or back to its newest. The conversation,
235
+ activity log and thinking are newest-last: scrolled back, a pane shows `↓N`
236
+ for the lines below it and holds still while new ones arrive; `end` follows
237
+ the newest again. Details stop at their last line. The Thinking pane keeps
238
+ every recent thought, so earlier ones are a scroll away.
239
+
240
+ **Search.** `ctrl+f` opens a search bar above the prompt; as you type, the tab
241
+ narrows to what matches and every match is highlighted: the conversation,
242
+ activity log and thoughts on Lobby; tasks and plans (by id, title, request,
243
+ proposal or plan) on Tasks; the conversation on Plan (the draft stays whole,
244
+ highlighted); jobs on Quick fix; runs (by agent, model, thinking level, kind or
245
+ task) on Metrics. `enter` keeps the search while you browse the results,
246
+ `esc` clears it, and each tab keeps its own.
247
+
248
+ **Mouse.** Clicking a tab opens it, clicking a pane gives it the keys,
249
+ clicking a draft plan line comments on it, clicking the prompt starts typing,
250
+ and the wheel scrolls whichever pane is under the pointer. In pi's regular
251
+ screen the lobby turns mouse reporting on only while it is showing (hold
252
+ `shift` to select text with the mouse); in full-screen pi, pi reports the
253
+ mouse itself. `"lobby": { "mouse": false }` turns clicks off.
254
+
255
+ Anything that needs pi itself —
183
256
  built-in slash commands, `/model`, the tool-row toggle — works with the lobby
184
257
  hidden; bot-lobby's own `/bot-lobby …` commands also work from the Lobby
185
258
  prompt. When the Master asks you something (an approval, a clarifying
@@ -446,7 +519,7 @@ top-level `/bot-lobby-settings`) and persist globally to
446
519
  "researcher": { "model": "anthropic/claude-sonnet-5", "thinking": "low", "instructions": "", "timeoutMs": 600000 },
447
520
  "quickFix": { "model": "anthropic/claude-sonnet-5", "thinking": "low", "instructions": "", "timeoutMs": 600000 },
448
521
  "planner": { "model": "anthropic/claude-sonnet-5", "thinking": "high", "instructions": "", "timeoutMs": 300000 },
449
- "lobby": { "autoOpen": true, "planningPanel": ["backend", "designer", "qa", "researcher"] },
522
+ "lobby": { "autoOpen": true, "planningPanel": ["backend", "designer", "qa", "researcher"], "autoAsk": true, "issues": false, "mouse": true },
450
523
  "workflow": {
451
524
  "maxReviewIterations": 2,
452
525
  "maxParallelScouts": 3,
@@ -489,10 +562,29 @@ take custom instructions, and run on the session's model until you pin one.
489
562
  Planning seats reuse their domain's entry — DEV the Backend's, DESIGN the
490
563
  Designer's, QA the QA's, RESEARCH the Researcher's model, thinking and
491
564
  instructions — so a seat plans on the model that will later build its part.
492
- `lobby.planningPanel` names the seats a new planning session starts with
493
- (every seat by default; `[]` lets the oracle plan alone), and
494
- `lobby.autoOpen` (default `true`) opens the lobby by itself when this session
495
- starts or resumes a task.
565
+ The `lobby` entry shapes the lobby itself; `/bot-lobby settings` → **Lobby**
566
+ flips its switches, and key rebinding lives in the file:
567
+
568
+ ```json
569
+ "lobby": {
570
+ "autoOpen": true,
571
+ "planningPanel": ["backend", "designer", "qa", "researcher"],
572
+ "autoAsk": true,
573
+ "issues": false,
574
+ "mouse": true,
575
+ "panels": { "scene": true, "conversation": true, "activity": true, "thinking": true },
576
+ "keys": { "toggleThinking": "alt+t" }
577
+ }
578
+ ```
579
+
580
+ `planningPanel` names the seats a new planning session starts with (every
581
+ seat by default; `[]` lets the oracle plan alone); `autoOpen` opens the lobby
582
+ by itself when this session starts or resumes a task; `autoAsk` puts the
583
+ panel's questions to you as soon as a round ends while the Plan tab is
584
+ showing (otherwise `enter` on the empty prompt does); `issues` shows the
585
+ GitHub Issues tab (off for now); `mouse` turns clicks and the wheel on;
586
+ `panels` is which Lobby panes show (the pane keys update it); `keys` rebinds
587
+ shortcuts by action name.
496
588
 
497
589
  `thinking` must be one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`,
498
590
  `max`; a legacy `inherit` or unknown value falls back to `medium`. The thinking
@@ -591,13 +683,16 @@ src/
591
683
  ├── prompts/ Layer loader + compiler
592
684
  ├── lobby/
593
685
  │ ├── runtime.ts Mounts the full-screen lobby on pi's TUI, dialogs hand-off, comment delivery, Master metrics
594
- │ ├── view.ts The tabbed view: tab bar, per-tab prompt, typing/browsing modes, keys
686
+ │ ├── view.ts The tabbed view: tab bar, per-tab prompt, typing/browsing modes, search, help, mouse
687
+ │ ├── keys.ts The shortcut table and its config overrides
688
+ │ ├── ask.ts The panel's questions through the ask-user-question questionnaire (or pi's dialogs)
689
+ │ ├── markdown.ts Markdown through pi's renderer, cached per theme and width
595
690
  │ ├── tabs/ Pure renderers: home, tasks, plan, quickfix, issues, metrics
596
691
  │ ├── feed.ts Activity log, thinking pane and conversation store
597
692
  │ ├── quickfix.ts Direct-change jobs, one at a time
598
693
  │ ├── planner.ts The planning panel: seats and the oracle per round, reply parsing, saving a plan
599
694
  │ ├── issues.ts GitHub issues through the gh CLI
600
- │ └── layout.ts Exact-width columns, rules, wrapping and scroll windows
695
+ │ └── layout.ts Boxes, exact-width columns, wrapping, highlights, bars, meters and sparklines
601
696
  ├── state/ Project root, config, task persistence, state mutation, comments, backlog, metrics
602
697
  ├── schemas/ Task, agent, findings, configuration types
603
698
  └── pi/ Commands, lifecycle, orchestrate tool, status widget
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-t-h-i/bot-lobby",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Structured multi-agent software engineering orchestrator for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,11 +38,14 @@
38
38
  "typebox": "*"
39
39
  },
40
40
  "devDependencies": {
41
- "@earendil-works/pi-coding-agent": "0.87.0",
42
41
  "@earendil-works/pi-ai": "0.87.0",
42
+ "@earendil-works/pi-coding-agent": "0.87.0",
43
43
  "@earendil-works/pi-tui": "0.87.0",
44
44
  "@types/node": "^22.10.0",
45
45
  "typebox": "1.3.27",
46
46
  "typescript": "^5.7.0"
47
+ },
48
+ "dependencies": {
49
+ "@juicesharp/rpiv-ask-user-question": "^2.11.0"
47
50
  }
48
51
  }
package/prompts/panel.md CHANGED
@@ -17,7 +17,9 @@ questions and the user's answers, and the oracle's current draft plan.
17
17
  task is built or verified. Never repeat a question that has been answered,
18
18
  or one another member already asked this round.
19
19
  - Ask at most two questions, the most important first. Make each specific and
20
- answerable; offer options (`a) … b) …`) and say which you would pick.
20
+ answerable, and give it two to four options the user can pick from, your
21
+ recommendation first with `(Recommended)` after its label. The user can
22
+ always type their own answer instead, so do not add an "Other" option.
21
23
  - If an answer from the user is vague or conflicts with what you see in the
22
24
  repository, say so and ask again.
23
25
  - Report what the plan must respect from your seat under Notes: facts from
@@ -31,9 +33,12 @@ questions and the user's answers, and the oracle's current draft plan.
31
33
  OPEN or READY
32
34
 
33
35
  ## Questions
34
- 1. …
36
+ 1. The question, ending with a question mark?
37
+ - Short label (Recommended) — what choosing it means
38
+ - Another label — what choosing it means
35
39
 
36
- (Omit Questions when READY.)
40
+ (Two to four options per question, labels of one to five words. Omit
41
+ Questions when READY.)
37
42
 
38
43
  ## Notes
39
44
  - …
@@ -23,8 +23,10 @@ below.
23
23
  only cross-cutting ones the members did not ask: scope and non-goals,
24
24
  priorities, trade-offs between domains, sequencing, rollout and rollback.
25
25
  Never repeat a member's question. Each one must be specific and answerable.
26
- - Offer concrete options when they help (`a) … b) …`), and say which you
27
- would pick and why.
26
+ - Give every question two to four options the user can pick from, your
27
+ recommendation first with `(Recommended)` after its label. The user answers
28
+ the panel's questions one at a time and can always type their own answer,
29
+ so never add an "Other" option.
28
30
  - Challenge answers that are vague, contradictory or risky, and ask again.
29
31
  Do not accept "whatever you think" for a decision with real trade-offs:
30
32
  propose one and ask the user to confirm it.
@@ -44,10 +46,13 @@ GRILLING or READY
44
46
  Three to six words naming the task.
45
47
 
46
48
  ## Questions
47
- 1. The most important open question.
49
+ 1. The most important open question?
50
+ - Short label (Recommended) — what choosing it means
51
+ - Another label — what choosing it means
48
52
  2. …
49
53
 
50
- (Omit the Questions section when READY.)
54
+ (Two to four options per question, labels of one to five words. Omit the
55
+ Questions section when READY.)
51
56
 
52
57
  ## Plan
53
58
  The current draft, in Markdown:
@@ -0,0 +1,226 @@
1
+ /**
2
+ * The oracle puts the planning panel's questions to the user one at a time
3
+ * through the ask-user-question library (`@juicesharp/rpiv-ask-user-question`):
4
+ * a tabbed questionnaire with each seat's options, the recommended one first,
5
+ * and a free-text row on every question. The library only publishes a pi
6
+ * extension entry point, so its `ask_user_question` tool is captured by
7
+ * calling that entry with a `pi` whose `registerTool` keeps the definition
8
+ * instead of registering it; its `execute` then runs the real questionnaire.
9
+ * Without the library, pi's own select and input dialogs ask the same
10
+ * questions in the same order.
11
+ */
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+ import type { PanelQuestion } from "./planner.ts";
14
+
15
+ /** The library's limits: at most 4 questions per questionnaire, 2-4 options each. */
16
+ export const MAX_QUESTIONS = 4;
17
+ export const MIN_OPTIONS = 2;
18
+ export const MAX_OPTIONS = 4;
19
+ const MAX_HEADER = 16;
20
+ const MAX_LABEL = 60;
21
+ /** Labels the library reserves for its own rows. */
22
+ const RESERVED = new Set(["other", "type something.", "next"]);
23
+
24
+ export interface AskOption {
25
+ label: string;
26
+ description: string;
27
+ }
28
+
29
+ export interface AskQuestion {
30
+ question: string;
31
+ header: string;
32
+ options: AskOption[];
33
+ multiSelect?: boolean;
34
+ }
35
+
36
+ export interface AskAnswer {
37
+ questionIndex: number;
38
+ question: string;
39
+ kind: "option" | "custom" | "multi";
40
+ answer: string | null;
41
+ selected?: string[];
42
+ notes?: string;
43
+ }
44
+
45
+ export interface AskResult {
46
+ answers: AskAnswer[];
47
+ cancelled: boolean;
48
+ globalNote?: string;
49
+ }
50
+
51
+ /** Puts up to `MAX_QUESTIONS` questions to the user and returns what they chose. */
52
+ export type Asker = (questions: readonly AskQuestion[], ctx: ExtensionContext) => Promise<AskResult>;
53
+
54
+ interface ToolLike {
55
+ execute(toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext): Promise<{ details?: unknown }>;
56
+ }
57
+
58
+ function clip(text: string, max: number): string {
59
+ const flat = text.replace(/\s+/g, " ").trim();
60
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
61
+ }
62
+
63
+ /** Filler options for a question the panel asked without any. */
64
+ const DEFAULT_OPTIONS: readonly AskOption[] = [
65
+ { label: "Go with the recommendation", description: "Let the panel pick the option it recommends." },
66
+ { label: "Leave it open", description: "Skip this one for now; the panel may ask again." },
67
+ ];
68
+
69
+ /** A panel question in the library's schema: seat as the chip, 2-4 unique, unreserved option labels. */
70
+ export function toAskQuestion(question: PanelQuestion): AskQuestion {
71
+ const seen = new Set<string>();
72
+ const options: AskOption[] = [];
73
+ for (const option of question.options) {
74
+ let label = clip(option.label, MAX_LABEL);
75
+ if (!label || RESERVED.has(label.toLowerCase())) continue;
76
+ for (let n = 2; seen.has(label.toLowerCase()); n++) label = clip(`${option.label} (${n})`, MAX_LABEL);
77
+ seen.add(label.toLowerCase());
78
+ options.push({ label, description: option.description.trim() || label });
79
+ if (options.length === MAX_OPTIONS) break;
80
+ }
81
+ for (const filler of DEFAULT_OPTIONS) {
82
+ if (options.length >= MIN_OPTIONS) break;
83
+ if (!seen.has(filler.label.toLowerCase())) options.push({ ...filler });
84
+ }
85
+ return { question: question.text.trim(), header: clip(question.from, MAX_HEADER), options };
86
+ }
87
+
88
+ /** Split the round's questions into questionnaires of at most `MAX_QUESTIONS`, with unique question texts. */
89
+ export function questionnaires(questions: readonly PanelQuestion[]): AskQuestion[][] {
90
+ const seen = new Set<string>();
91
+ const asked = questions.map((question) => {
92
+ const ask = toAskQuestion(question);
93
+ let text = ask.question;
94
+ for (let n = 2; seen.has(text.toLowerCase()); n++) text = `${ask.question} (${question.from}${n > 2 ? ` ${n}` : ""})`;
95
+ seen.add(text.toLowerCase());
96
+ return { ...ask, question: text };
97
+ });
98
+ const chunks: AskQuestion[][] = [];
99
+ for (let i = 0; i < asked.length; i += MAX_QUESTIONS) chunks.push(asked.slice(i, i + MAX_QUESTIONS));
100
+ return chunks;
101
+ }
102
+
103
+ function answerText(answer: AskAnswer | undefined): string | undefined {
104
+ if (!answer) return undefined;
105
+ if (answer.kind === "multi") return answer.selected && answer.selected.length > 0 ? answer.selected.join(", ") : undefined;
106
+ return answer.answer?.trim() || undefined;
107
+ }
108
+
109
+ /**
110
+ * The user's turn for the next round: every question with who asked it and
111
+ * the answer (`→ …`), their notes, and what they left unanswered. Undefined
112
+ * when nothing was answered.
113
+ */
114
+ export function answerMessage(questions: readonly PanelQuestion[], results: readonly AskResult[]): string | undefined {
115
+ const lines: string[] = [];
116
+ const skipped: string[] = [];
117
+ const notes: string[] = [];
118
+ let answered = 0;
119
+ results.forEach((result, chunk) => {
120
+ if (result.globalNote?.trim()) notes.push(result.globalNote.trim());
121
+ const offset = chunk * MAX_QUESTIONS;
122
+ for (let i = 0; i < MAX_QUESTIONS; i++) {
123
+ const question = questions[offset + i];
124
+ if (!question) break;
125
+ const answer = result.answers.find((entry) => entry.questionIndex === i);
126
+ const text = answerText(answer);
127
+ const label = `${offset + i + 1}. [${question.from}] ${question.text.replace(/\s+/g, " ").trim()}`;
128
+ if (!text) {
129
+ skipped.push(label);
130
+ continue;
131
+ }
132
+ answered += 1;
133
+ lines.push(`${label}\n → ${text}${answer?.kind === "custom" ? " (in my words)" : ""}`);
134
+ if (answer?.notes?.trim()) lines.push(` note: ${answer.notes.trim()}`);
135
+ }
136
+ });
137
+ if (answered === 0 && notes.length === 0) return undefined;
138
+ return [
139
+ "Answers to the panel's questions:",
140
+ ...lines,
141
+ ...(notes.length > 0 ? ["", `Note: ${notes.join(" ")}`] : []),
142
+ ...(skipped.length > 0 ? ["", "Not answered this round:", ...skipped] : []),
143
+ ].join("\n");
144
+ }
145
+
146
+ /**
147
+ * Capture the library's tool through its public extension entry: the entry
148
+ * registers the tool on the `pi` it is given, so a `pi` whose `registerTool`
149
+ * keeps the definition (and whose `on` ignores the library's own tool-list
150
+ * reconciler) yields it without touching the session's tools. Undefined when
151
+ * the library is missing or registers nothing.
152
+ */
153
+ /**
154
+ * The library ships TypeScript sources; a non-literal specifier keeps them out
155
+ * of this package's typecheck (they are compiled by pi's loader at runtime).
156
+ */
157
+ const ASK_LIBRARY: string = "@juicesharp/rpiv-ask-user-question";
158
+
159
+ export async function loadAskTool(pi: ExtensionAPI, load: () => Promise<unknown> = () => import(ASK_LIBRARY)): Promise<ToolLike | undefined> {
160
+ try {
161
+ const module = (await load()) as { default?: (pi: ExtensionAPI) => void };
162
+ if (typeof module.default !== "function") return undefined;
163
+ let captured: ToolLike | undefined;
164
+ const capture = new Proxy(pi, {
165
+ get(target, property, receiver) {
166
+ if (property === "registerTool") return (tool: ToolLike & { name?: string }) => {
167
+ if (tool.name === "ask_user_question") captured = tool;
168
+ };
169
+ if (property === "on") return () => () => {};
170
+ return Reflect.get(target, property, receiver);
171
+ },
172
+ });
173
+ module.default(capture);
174
+ return captured;
175
+ } catch {
176
+ return undefined;
177
+ }
178
+ }
179
+
180
+ /** Run one questionnaire through the captured tool. */
181
+ export function toolAsker(tool: ToolLike): Asker {
182
+ return async (questions, ctx) => {
183
+ const result = await tool.execute(`bot-lobby-panel-${Date.now().toString(36)}`, { questions }, undefined, undefined, ctx);
184
+ const details = result.details as Partial<AskResult> | undefined;
185
+ return { answers: details?.answers ?? [], cancelled: details?.cancelled !== false, ...(details?.globalNote ? { globalNote: details.globalNote } : {}) };
186
+ };
187
+ }
188
+
189
+ const TYPE_ANSWER = "Type an answer…";
190
+ const SKIP = "Skip";
191
+
192
+ /** The same questions through pi's built-in dialogs: pick an option, type an answer, or skip; esc stops. */
193
+ export function dialogAsker(): Asker {
194
+ return async (questions, ctx) => {
195
+ const answers: AskAnswer[] = [];
196
+ for (const [index, question] of questions.entries()) {
197
+ const choices = [...question.options.map((option) => option.label), TYPE_ANSWER, SKIP];
198
+ const title = `${question.header} · ${index + 1}/${questions.length}\n\n${question.question}`;
199
+ const choice = await ctx.ui.select(title, choices);
200
+ if (choice === undefined) return { answers, cancelled: true };
201
+ if (choice === SKIP) continue;
202
+ if (choice === TYPE_ANSWER) {
203
+ const typed = await ctx.ui.input(question.question, "your answer");
204
+ if (typed === undefined) return { answers, cancelled: true };
205
+ if (typed.trim()) answers.push({ questionIndex: index, question: question.question, kind: "custom", answer: typed.trim() });
206
+ continue;
207
+ }
208
+ answers.push({ questionIndex: index, question: question.question, kind: "option", answer: choice });
209
+ }
210
+ return { answers, cancelled: false };
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Put every question to the user, one questionnaire after another. Stopping
216
+ * a questionnaire (esc) stops the rest; what was answered before it is kept.
217
+ */
218
+ export async function askPanel(questions: readonly PanelQuestion[], ask: Asker, ctx: ExtensionContext): Promise<{ results: AskResult[]; stopped: boolean }> {
219
+ const results: AskResult[] = [];
220
+ for (const chunk of questionnaires(questions)) {
221
+ const result = await ask(chunk, ctx);
222
+ if (result.cancelled) return { results, stopped: true };
223
+ results.push(result);
224
+ }
225
+ return { results, stopped: false };
226
+ }
@@ -0,0 +1,51 @@
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.
6
+ */
7
+ import { matchesKey, type KeyId } from "@earendil-works/pi-tui";
8
+
9
+ 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
+ nextTab: { key: "tab", help: "next tab" },
15
+ prevTab: { key: "shift+tab", help: "previous tab" },
16
+ toggleScene: { key: "alt+z", help: "show or hide the zen scene" },
17
+ toggleConversation: { key: "alt+c", help: "show or hide the conversation" },
18
+ toggleActivity: { key: "alt+a", help: "show or hide the activity log" },
19
+ toggleThinking: { key: "alt+k", help: "show or hide thinking" },
20
+ scrollUp: { key: "pageUp", help: "scroll up a page" },
21
+ scrollDown: { key: "pageDown", help: "scroll down a page" },
22
+ } as const;
23
+
24
+ export type LobbyAction = keyof typeof LOBBY_ACTIONS;
25
+
26
+ export type KeyMap = Record<LobbyAction, string>;
27
+
28
+ /** Defaults with the config's overrides on top; unknown action names are ignored. */
29
+ export function keyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap {
30
+ const map = Object.fromEntries(Object.entries(LOBBY_ACTIONS).map(([action, entry]) => [action, entry.key])) as KeyMap;
31
+ for (const [action, key] of Object.entries(overrides)) {
32
+ if (action in map && key.trim()) map[action as LobbyAction] = key.trim().toLowerCase();
33
+ }
34
+ return map;
35
+ }
36
+
37
+ /** The action `data` triggers under `map`, if any. */
38
+ export function actionFor(data: string, map: KeyMap): LobbyAction | undefined {
39
+ for (const action of Object.keys(map) as LobbyAction[]) {
40
+ if (matchesKey(data, map[action] as KeyId)) return action;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /** `alt+k` reads `Alt+K` in hints and help. */
46
+ export function keyLabel(key: string): string {
47
+ return key
48
+ .split("+")
49
+ .map((part) => (part.length === 1 ? part.toUpperCase() : part[0]!.toUpperCase() + part.slice(1)))
50
+ .join("+");
51
+ }