@a-t-h-i/bot-lobby 0.6.12 → 0.6.13
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 +11 -3
- package/package.json +31 -4
- package/src/ask/tool.ts +2 -1
- package/src/lobby/keys.ts +14 -1
- package/src/lobby/mini.ts +9 -1
- package/src/lobby/models/metrics.ts +14 -0
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/prompt-hub.ts +161 -0
- package/src/lobby/prompts.ts +35 -0
- package/src/lobby/runtime.ts +115 -566
- package/src/lobby/service.ts +655 -0
- package/src/lobby/tabs/home.ts +1 -1
- package/src/lobby/tabs/metrics.ts +4 -6
- package/src/lobby/topics.ts +78 -0
- package/src/lobby/view.ts +29 -24
- package/src/pi/commands.ts +8 -2
- package/src/pi/owner.ts +2 -0
- package/src/pi/settings-ui.ts +52 -1
- package/src/pi/tools.ts +4 -3
- package/src/schemas/configuration.ts +33 -1
- package/src/state/changes.ts +1 -1
- package/src/state/persistence.ts +1 -1
- package/src/state/presence.ts +2 -0
- package/src/webui/api/excalidraw.ts +105 -0
- package/src/webui/api/gh-error.ts +17 -0
- package/src/webui/api/git.ts +98 -0
- package/src/webui/api/index.ts +379 -0
- package/src/webui/api/issues.ts +50 -0
- package/src/webui/api/knowledge.ts +79 -0
- package/src/webui/api/lobby.ts +91 -0
- package/src/webui/api/metrics.ts +63 -0
- package/src/webui/api/plan-facts.ts +29 -0
- package/src/webui/api/planner.ts +98 -0
- package/src/webui/api/plans.ts +44 -0
- package/src/webui/api/prompts.ts +35 -0
- package/src/webui/api/quickfix.ts +48 -0
- package/src/webui/api/sessions.ts +114 -0
- package/src/webui/api/settings.ts +93 -0
- package/src/webui/api/status.ts +55 -0
- package/src/webui/api/tasks.ts +204 -0
- package/src/webui/auth.ts +153 -0
- package/src/webui/command.ts +152 -0
- package/src/webui/dev/fake-service.ts +546 -0
- package/src/webui/dev/fixtures.ts +180 -0
- package/src/webui/dev/serve.ts +54 -0
- package/src/webui/events.ts +207 -0
- package/src/webui/files.ts +88 -0
- package/src/webui/notices.ts +61 -0
- package/src/webui/open.ts +35 -0
- package/src/webui/protocol.ts +565 -0
- package/src/webui/server.ts +315 -0
- package/src/webui/static.ts +125 -0
- package/webui/dist/assets/index-Bk-lpAGm.css +2 -0
- package/webui/dist/assets/index-E6MPcbdC.js +80 -0
- package/webui/dist/build.json +494 -0
- package/webui/dist/icon-192.png +0 -0
- package/webui/dist/icon-512.png +0 -0
- package/webui/dist/index.html +18 -0
- package/webui/dist/manifest.webmanifest +36 -0
- package/webui/dist/offline.html +57 -0
- package/webui/dist/sw.js +88 -0
- package/webui/dist/vite.svg +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Bot-Lobby
|
|
2
2
|
|
|
3
|
-
A [Pi](https://pi.dev) extension that
|
|
3
|
+
A [Pi](https://pi.dev) extension that forces you to vibe code intentionally.
|
|
4
|
+
You still describe what you want in plain words, but you don't just hand it
|
|
5
|
+
over: you and the model work out together what's needed until the picture is
|
|
6
|
+
clear, and then a multi-agent software team builds what you agreed on.
|
|
4
7
|
|
|
5
8
|

|
|
6
9
|
|
|
@@ -12,8 +15,9 @@ work to three domain agents — **Designer+Frontend**, **Backend** and **QA**
|
|
|
12
15
|
each running in its own isolated `pi` process. QA's reviewer is the quality
|
|
13
16
|
gate before anything is marked done.
|
|
14
17
|
|
|
15
|
-
The rule: **
|
|
16
|
-
|
|
18
|
+
The rule: **you decide with the LLMs, and the engine enforces.** Agents
|
|
19
|
+
propose; you shape and approve the plan with the oracle; the extension
|
|
20
|
+
validates every state change, permission and approval through one
|
|
17
21
|
`orchestrate` tool.
|
|
18
22
|
|
|
19
23
|
The oracle is meant to be your most capable model; the agents can be smaller
|
|
@@ -61,6 +65,10 @@ extra instructions.
|
|
|
61
65
|
| `/bot-lobby settings \| config` | Edit settings / show the effective config |
|
|
62
66
|
| `/bot-lobby knowledge` | Knowledge file sizes |
|
|
63
67
|
| `/bot-lobby minimize \| restore` | Hide bot-lobby in this session (`ctrl+shift+m`) |
|
|
68
|
+
| `/bot-lobby web` | Start the browser UI on this machine (loopback only) and open its link |
|
|
69
|
+
| `/bot-lobby web link` | Print the browser UI's link |
|
|
70
|
+
| `/bot-lobby web stop` | Stop the browser UI |
|
|
71
|
+
| `/bot-lobby web reset` | Reset the browser UI's link (its old cookies stop working) |
|
|
64
72
|
|
|
65
73
|
## Quick fix or the team
|
|
66
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
|
28
|
-
"prompts"
|
|
28
|
+
"prompts",
|
|
29
|
+
"webui/dist"
|
|
29
30
|
],
|
|
30
31
|
"publishConfig": {
|
|
31
32
|
"access": "public"
|
|
@@ -37,7 +38,11 @@
|
|
|
37
38
|
"image": "https://raw.githubusercontent.com/a-t-h-i/bot-lobby/main/docs/gallery.png"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
|
-
"typecheck": "tsc --noEmit",
|
|
41
|
+
"typecheck": "tsc --noEmit && tsc -p webui --noEmit",
|
|
42
|
+
"web:dev": "node src/webui/dev/serve.ts",
|
|
43
|
+
"web:build": "vite build --config webui/vite.config.ts && node webui/scripts/build-info.mjs",
|
|
44
|
+
"web:typecheck": "tsc -p webui --noEmit",
|
|
45
|
+
"web:check": "playwright test",
|
|
41
46
|
"test": "node --test --test-reporter=dot test/*.test.ts"
|
|
42
47
|
},
|
|
43
48
|
"peerDependencies": {
|
|
@@ -50,10 +55,32 @@
|
|
|
50
55
|
"@earendil-works/pi-ai": "0.87.0",
|
|
51
56
|
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
52
57
|
"@earendil-works/pi-tui": "0.87.0",
|
|
58
|
+
"@axe-core/playwright": "4.13.0",
|
|
59
|
+
"@playwright/test": "1.62.0",
|
|
60
|
+
"@shadcn/react": "0.3.1",
|
|
61
|
+
"@tailwindcss/vite": "4.3.3",
|
|
53
62
|
"@types/node": "^22.10.0",
|
|
63
|
+
"@types/react": "19.3.0",
|
|
64
|
+
"@types/react-dom": "19.3.0",
|
|
65
|
+
"@vitejs/plugin-react": "6.1.1",
|
|
66
|
+
"class-variance-authority": "0.7.1",
|
|
67
|
+
"clsx": "2.1.1",
|
|
68
|
+
"lucide-react": "1.49.0",
|
|
69
|
+
"radix-ui": "1.6.7",
|
|
70
|
+
"react": "19.3.0",
|
|
71
|
+
"react-dom": "19.3.0",
|
|
72
|
+
"react-markdown": "10.1.0",
|
|
73
|
+
"recharts": "3.10.1",
|
|
74
|
+
"remark-gfm": "4.0.1",
|
|
75
|
+
"shiki": "4.5.0",
|
|
54
76
|
"socket.io": "4.8.4",
|
|
77
|
+
"sonner": "2.0.8",
|
|
78
|
+
"tailwind-merge": "3.7.0",
|
|
79
|
+
"tailwindcss": "4.3.3",
|
|
80
|
+
"tw-animate-css": "1.4.0",
|
|
55
81
|
"typebox": "1.3.27",
|
|
56
|
-
"typescript": "^5.7.0"
|
|
82
|
+
"typescript": "^5.7.0",
|
|
83
|
+
"vite": "8.3.2"
|
|
57
84
|
},
|
|
58
85
|
"dependencies": {
|
|
59
86
|
"https-proxy-agent": "^9.1.0",
|
package/src/ask/tool.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Container, Text } from "@earendil-works/pi-tui";
|
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
13
13
|
import { askUser, type Asker } from "./dialog.ts";
|
|
14
|
+
import { promptHub } from "../lobby/prompt-hub.ts";
|
|
14
15
|
import { isImagePath } from "./image.ts";
|
|
15
16
|
import { relayAsker, relayEnabled } from "./relay.ts";
|
|
16
17
|
import { ASK_TOOL, MAX_HEADER, MAX_LABEL, MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, RESERVED, type AskQuestion, type AskResult } from "./types.ts";
|
|
@@ -114,7 +115,7 @@ export function registerAskTool(pi: ExtensionAPI, ask?: Asker): void {
|
|
|
114
115
|
const questions = (params as { questions: AskQuestion[] }).questions;
|
|
115
116
|
const invalid = invalidQuestions(questions);
|
|
116
117
|
if (invalid) throw new Error(`${invalid}.`);
|
|
117
|
-
const result = await asker(questions, ctx, signal);
|
|
118
|
+
const result = await promptHub.run("questionnaire", "oracle", { questions }, () => asker(questions, ctx, signal), { signal });
|
|
118
119
|
return { content: [{ type: "text", text: answerSummary(questions, result) }], details: result };
|
|
119
120
|
},
|
|
120
121
|
renderCall(args, theme) {
|
package/src/lobby/keys.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`{ "toggleThinking": "alt+t" }`). They work in typing and browsing mode
|
|
5
5
|
* alike, so each default is a key that never types a character.
|
|
6
6
|
*/
|
|
7
|
-
import { matchesKey, type KeyId } from "@earendil-works/pi-tui";
|
|
7
|
+
import { Key, matchesKey, type KeyId } from "@earendil-works/pi-tui";
|
|
8
8
|
|
|
9
9
|
export const LOBBY_ACTIONS = {
|
|
10
10
|
hide: { key: "alt+l", help: "hide the lobby (back to pi)" },
|
|
@@ -37,6 +37,19 @@ export function keyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap
|
|
|
37
37
|
return map;
|
|
38
38
|
}
|
|
39
39
|
|
|
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
|
+
|
|
40
53
|
/** The action `data` triggers under `map`, if any. */
|
|
41
54
|
export function actionFor(data: string, map: KeyMap): LobbyAction | undefined {
|
|
42
55
|
for (const action of Object.keys(map) as LobbyAction[]) {
|
package/src/lobby/mini.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface MiniInput {
|
|
|
36
36
|
runs: readonly AgentRun[];
|
|
37
37
|
planning?: MiniPlanning;
|
|
38
38
|
quickfix?: MiniQuickFix;
|
|
39
|
+
/** The loopback web UI's link, while its server runs. */
|
|
40
|
+
webLink?: string;
|
|
39
41
|
/** The key that opens the lobby, as shown. */
|
|
40
42
|
key: string;
|
|
41
43
|
}
|
|
@@ -107,10 +109,16 @@ function quickFixSegment(quickfix: MiniQuickFix | undefined, theme?: LobbyTheme)
|
|
|
107
109
|
return `${paint(theme, "accent", "quick fix")} ${paint(theme, "success", quickfix.running ? "◐" : "○")} ${paint(theme, "muted", quickfix.title)}${paint(theme, "dim", queued)}`;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/** The web link, while the server runs; pure: the link arrives as input. */
|
|
113
|
+
function webSegment(link: string | undefined, theme?: LobbyTheme): string | undefined {
|
|
114
|
+
if (!link) return undefined;
|
|
115
|
+
return paint(theme, "dim", link);
|
|
116
|
+
}
|
|
117
|
+
|
|
110
118
|
/** The status line, at most `width` columns. */
|
|
111
119
|
export function miniLine(input: MiniInput, width: number, theme?: LobbyTheme): string {
|
|
112
120
|
if (width < 12) return "";
|
|
113
|
-
const segments = [taskSegment(input, theme), planningSegment(input.planning, theme), quickFixSegment(input.quickfix, theme)].filter((segment): segment is string => Boolean(segment));
|
|
121
|
+
const segments = [taskSegment(input, theme), planningSegment(input.planning, theme), quickFixSegment(input.quickfix, theme), webSegment(input.webLink, theme)].filter((segment): segment is string => Boolean(segment));
|
|
114
122
|
const body = segments.length > 0 ? segments.join(paint(theme, "dim", " │ ")) : paint(theme, "dim", "idle");
|
|
115
123
|
const hint = paint(theme, "dim", ` ${input.key} opens`);
|
|
116
124
|
const line = ` ${paint(theme, "accent", "◆ bot-lobby")} ${body}`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure metrics data helpers shared by the terminal tab and the web API.
|
|
3
|
+
* This module touches neither the terminal nor pi-tui (unlike
|
|
4
|
+
* `../tabs/metrics.ts`, which draws through `layout.ts`), so the loopback
|
|
5
|
+
* server can import it freely.
|
|
6
|
+
*/
|
|
7
|
+
import type { MetricRecord } from "../../state/metrics.ts";
|
|
8
|
+
|
|
9
|
+
/** Runs whose agent, model, thinking level, kind, status or task mention `query`. */
|
|
10
|
+
export function filterRecords(records: readonly MetricRecord[], query: string | undefined): MetricRecord[] {
|
|
11
|
+
const needle = query?.trim().toLowerCase();
|
|
12
|
+
if (!needle) return [...records];
|
|
13
|
+
return records.filter((record) => [record.agent, record.model ?? "", record.thinking ?? "", record.kind, record.status, record.taskId ?? ""].join(" ").toLowerCase().includes(needle));
|
|
14
|
+
}
|
package/src/lobby/planner.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { describeToolCall } from "../pi/activity.ts";
|
|
|
27
27
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
28
28
|
import { savePlannedTask, type IssueRef, type PlannedTask } from "../state/backlog.ts";
|
|
29
29
|
import { planSteps } from "../pi/plan-checklist.ts";
|
|
30
|
-
import { KEEP_WHOLE, MAX_SPLIT_REVISIONS, partBrief, partInfo, parseSplit, splitFailedQuestion,
|
|
30
|
+
import { KEEP_WHOLE, MAX_SPLIT_REVISIONS, partBrief, partInfo, parseSplit, splitFailedQuestion, splitProblems, splitPrompt, splitQuestion, splitRequest, type SplitProposal } from "./split.ts";
|
|
31
31
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
32
32
|
import { PANEL_MEMBERS, type PanelMember } from "../schemas/configuration.ts";
|
|
33
33
|
import { truncate } from "../text.ts";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot-lobby's questions answerable from several surfaces, with the first
|
|
3
|
+
* answer winning. A surface registers (`terminal` today; the web page later);
|
|
4
|
+
* `open` shows the prompt on every surface, `answer` settles it once, and a
|
|
5
|
+
* cancelled caller's `signal` withdraws every surface. The terminal drives
|
|
6
|
+
* its prompts through `run`, which wraps today's code unchanged.
|
|
7
|
+
*/
|
|
8
|
+
import { lobbyTopics } from "./topics.ts";
|
|
9
|
+
|
|
10
|
+
export type PromptKind = "questionnaire" | "choose" | "confirm" | "text" | "sessionDialog";
|
|
11
|
+
|
|
12
|
+
/** A pending question in the shape the later web API lists. */
|
|
13
|
+
export interface WebPrompt {
|
|
14
|
+
id: string;
|
|
15
|
+
kind: PromptKind;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
from: string;
|
|
18
|
+
payload: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One surface a prompt shows on; `withdraw` takes it back off screen. */
|
|
22
|
+
export interface PromptSurface {
|
|
23
|
+
name: string;
|
|
24
|
+
show(prompt: WebPrompt): void;
|
|
25
|
+
withdraw(id: string): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Pending {
|
|
29
|
+
prompt: WebPrompt;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Outcome {
|
|
33
|
+
id: string;
|
|
34
|
+
value: unknown;
|
|
35
|
+
how: "answered" | "dismissed" | "cancelled";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** How many settled prompts are remembered (late answers report 409 off this). */
|
|
39
|
+
const MAX_OUTCOMES = 50;
|
|
40
|
+
|
|
41
|
+
function nextId(seq: number): string {
|
|
42
|
+
return `p${seq}-${Date.now().toString(36)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function showOn(surfaces: Iterable<PromptSurface>, prompt: WebPrompt): void {
|
|
46
|
+
for (const surface of surfaces) surface.show(prompt);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function withdrawFrom(surfaces: Iterable<PromptSurface>, id: string): void {
|
|
50
|
+
for (const surface of surfaces) surface.withdraw(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Questions from several surfaces; the first answer wins. */
|
|
54
|
+
export class PromptHub {
|
|
55
|
+
private seq = 0;
|
|
56
|
+
private readonly pendingMap = new Map<string, Pending>();
|
|
57
|
+
private readonly surfaces = new Map<string, PromptSurface>();
|
|
58
|
+
private readonly outcomes: Outcome[] = [];
|
|
59
|
+
|
|
60
|
+
/** Put a surface on show duty; returns its unregister function. */
|
|
61
|
+
registerSurface(surface: PromptSurface): () => void {
|
|
62
|
+
this.surfaces.set(surface.name, surface);
|
|
63
|
+
for (const entry of this.pendingMap.values()) surface.show(entry.prompt);
|
|
64
|
+
return () => {
|
|
65
|
+
this.surfaces.delete(surface.name);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The questions still waiting for an answer, oldest first. */
|
|
70
|
+
pending(): WebPrompt[] {
|
|
71
|
+
return [...this.pendingMap.values()].map((entry) => entry.prompt);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Show a prompt on every surface; the answer settles it later. */
|
|
75
|
+
open(kind: PromptKind, from: string, payload: unknown): WebPrompt {
|
|
76
|
+
this.seq += 1;
|
|
77
|
+
const prompt: WebPrompt = { id: nextId(this.seq), kind, createdAt: Date.now(), from, payload };
|
|
78
|
+
this.pendingMap.set(prompt.id, { prompt });
|
|
79
|
+
showOn(this.surfaces.values(), prompt);
|
|
80
|
+
lobbyTopics.bump("prompts");
|
|
81
|
+
return prompt;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Settle `id` with `value`. True when this call won; false when the prompt
|
|
86
|
+
* is already settled or unknown (the later web API answers 409 then).
|
|
87
|
+
*/
|
|
88
|
+
answer(id: string, value: unknown): boolean {
|
|
89
|
+
if (!this.pendingMap.has(id)) return false;
|
|
90
|
+
this.settle(id, value, "answered");
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Take `id` back without an answer; false when already settled or unknown. */
|
|
95
|
+
dismiss(id: string): boolean {
|
|
96
|
+
if (!this.pendingMap.has(id)) return false;
|
|
97
|
+
this.settle(id, undefined, "dismissed");
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What settled `id`, if it is still remembered. */
|
|
102
|
+
outcome(id: string): Outcome | undefined {
|
|
103
|
+
return this.outcomes.find((entry) => entry.id === id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private settle(id: string, value: unknown, how: Outcome["how"]): void {
|
|
107
|
+
this.pendingMap.delete(id);
|
|
108
|
+
withdrawFrom(this.surfaces.values(), id);
|
|
109
|
+
this.outcomes.push({ id, value, how });
|
|
110
|
+
if (this.outcomes.length > MAX_OUTCOMES) this.outcomes.splice(0, this.outcomes.length - MAX_OUTCOMES);
|
|
111
|
+
lobbyTopics.bump("prompts");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run today's terminal code for a prompt: it shows on every surface, the
|
|
116
|
+
* terminal's `show` answers it, and a cancelled `signal` withdraws them
|
|
117
|
+
* all. With only the terminal registered this is exactly today's behaviour.
|
|
118
|
+
*/
|
|
119
|
+
async run<T>(kind: PromptKind, from: string, payload: unknown, show: (prompt: WebPrompt) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T> {
|
|
120
|
+
const prompt = this.open(kind, from, payload);
|
|
121
|
+
const signal = options?.signal;
|
|
122
|
+
if (signal?.aborted) return this.cancelled(prompt.id, show, prompt);
|
|
123
|
+
const onAbort = () => this.dismiss(prompt.id);
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
try {
|
|
126
|
+
const value = await show(prompt);
|
|
127
|
+
return this.claim(prompt.id, value);
|
|
128
|
+
} finally {
|
|
129
|
+
signal?.removeEventListener("abort", onAbort);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The caller was already cancelled: withdraw every surface, keep today's result. */
|
|
134
|
+
private async cancelled<T>(id: string, show: (prompt: WebPrompt) => Promise<T>, prompt: WebPrompt): Promise<T> {
|
|
135
|
+
this.pendingMap.delete(id);
|
|
136
|
+
withdrawFrom(this.surfaces.values(), id);
|
|
137
|
+
lobbyTopics.bump("prompts");
|
|
138
|
+
return this.remember(id, await show(prompt));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Claim the win for `value`, unless another surface answered first. */
|
|
142
|
+
private claim<T>(id: string, value: T): T {
|
|
143
|
+
if (!this.pendingMap.has(id)) return this.remembered(id, value);
|
|
144
|
+
this.settle(id, value, "answered");
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private remember<T>(id: string, value: T): T {
|
|
149
|
+
if (!this.outcome(id)) this.outcomes.push({ id, value, how: "cancelled" });
|
|
150
|
+
lobbyTopics.bump("prompts");
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private remembered<T>(id: string, fallback: T): T {
|
|
155
|
+
const outcome = this.outcome(id);
|
|
156
|
+
return (outcome?.value ?? fallback) as T;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The process-wide hub; the terminal surface registers at lobby start. */
|
|
161
|
+
export const promptHub = new PromptHub();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strings the terminal lobby and the web lobby share: tab metadata and the
|
|
3
|
+
* short status lines shown while a question is answered elsewhere. Pure on
|
|
4
|
+
* purpose (no `node:*`, no `@earendil-works/pi-tui`): the web page imports
|
|
5
|
+
* this file directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "metrics", "git", "knowledge", "excalidraw"] as const;
|
|
9
|
+
export type TabId = (typeof TAB_IDS)[number];
|
|
10
|
+
|
|
11
|
+
export const TAB_LABELS: Record<TabId, string> = {
|
|
12
|
+
lobby: "Lobby",
|
|
13
|
+
tasks: "Tasks",
|
|
14
|
+
plan: "Plan",
|
|
15
|
+
quickfix: "Quick fix",
|
|
16
|
+
issues: "Issues",
|
|
17
|
+
metrics: "Metrics",
|
|
18
|
+
git: "Git",
|
|
19
|
+
knowledge: "Knowledge",
|
|
20
|
+
excalidraw: "Excalidraw",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The tabs on show: Issues only while `lobby.issues` switches it on. */
|
|
24
|
+
export function visibleTabs(issues: boolean): TabId[] {
|
|
25
|
+
return TAB_IDS.filter((tab) => issues || tab !== "issues");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A question answered in the terminal while the web lobby watched it. */
|
|
29
|
+
export const ANSWERED_IN_TERMINAL = "already answered in the terminal";
|
|
30
|
+
|
|
31
|
+
/** Pi itself is asking in the terminal; the web lobby waits. */
|
|
32
|
+
export const PI_ASKING_IN_TERMINAL = "pi is asking in the terminal";
|
|
33
|
+
|
|
34
|
+
/** The window is too narrow to show the lobby. */
|
|
35
|
+
export const NARROW_WINDOW = "the window is too narrow to show the lobby";
|