@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 +146 -51
- package/package.json +5 -2
- package/prompts/panel.md +8 -3
- package/prompts/planner.md +9 -4
- package/src/lobby/ask.ts +226 -0
- package/src/lobby/keys.ts +51 -0
- package/src/lobby/layout.ts +192 -3
- package/src/lobby/markdown.ts +48 -0
- package/src/lobby/planner.ts +121 -14
- package/src/lobby/runtime.ts +158 -7
- package/src/lobby/tabs/home.ts +121 -43
- package/src/lobby/tabs/metrics.ts +155 -42
- package/src/lobby/tabs/plan.ts +117 -35
- package/src/lobby/tabs/quickfix.ts +40 -12
- package/src/lobby/tabs/tasks.ts +46 -12
- package/src/lobby/view.ts +868 -113
- package/src/pi/commands.ts +1 -1
- package/src/pi/settings-ui.ts +65 -1
- package/src/schemas/configuration.ts +38 -4
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
|
|
71
|
-
`select`/`input` prompts (or the Master asks in plain text), just with
|
|
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,
|
|
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
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
you ▸ add a login page with email + password
|
|
111
|
-
oracle ▸ Proposal
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
sort (runs, average time, success,
|
|
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).
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
(
|
|
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
|
- …
|
package/prompts/planner.md
CHANGED
|
@@ -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
|
-
-
|
|
27
|
-
|
|
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
|
-
(
|
|
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:
|
package/src/lobby/ask.ts
ADDED
|
@@ -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
|
+
}
|