@a-t-h-i/bot-lobby 0.6.13 → 0.6.15
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 +70 -79
- package/package.json +4 -2
- package/prompts/master.md +3 -2
- package/prompts/qa.md +33 -11
- package/prompts/reviewer.md +5 -2
- package/prompts/worker.md +5 -2
- package/src/ask/relay.ts +1 -2
- package/src/ask/tool.ts +4 -6
- package/src/ask/types.ts +16 -0
- package/src/ask/web.ts +63 -0
- package/src/index.ts +7 -1
- package/src/lobby/ask.ts +4 -5
- package/src/lobby/blocks.ts +57 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +178 -0
- package/src/lobby/keys.ts +10 -41
- package/src/lobby/prompt-hub.ts +30 -77
- package/src/lobby/prompts.ts +8 -12
- package/src/lobby/runtime.ts +49 -310
- package/src/lobby/service.ts +54 -134
- package/src/lobby/task-rows.ts +28 -0
- package/src/pi/commands.ts +14 -20
- package/src/pi/model-settings.ts +112 -0
- package/src/pi/start-task.ts +1 -1
- package/src/pi/tools.ts +6 -7
- package/src/pi/ui.ts +0 -9
- package/src/schemas/configuration.ts +9 -33
- package/src/state/attachments.ts +82 -0
- package/src/webui/api/index.ts +17 -15
- package/src/webui/api/lobby.ts +4 -3
- package/src/webui/api/planner.ts +6 -4
- package/src/webui/api/quickfix.ts +4 -3
- package/src/webui/api/sessions.ts +9 -7
- package/src/webui/api/settings.ts +20 -8
- package/src/webui/api/status.ts +4 -15
- package/src/webui/api/tasks.ts +11 -15
- package/src/webui/command.ts +35 -19
- package/src/webui/dev/fake-service.ts +1 -2
- package/src/webui/dev/fixtures.ts +2 -5
- package/src/webui/events.ts +1 -1
- package/src/webui/protocol.ts +22 -11
- package/src/webui/server.ts +42 -1
- package/src/webui/uploads.ts +68 -0
- package/webui/dist/assets/index-B1PnCyl2.css +2 -0
- package/webui/dist/assets/index-DI9Yq9IC.js +90 -0
- package/webui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/webui/dist/build.json +142 -110
- package/webui/dist/index.html +4 -3
- package/src/ask/dialog.ts +0 -193
- package/src/ask/image.ts +0 -202
- package/src/ask/png.ts +0 -179
- package/src/ask/state.ts +0 -175
- package/src/ask/view.ts +0 -163
- package/src/lobby/layout.ts +0 -496
- package/src/lobby/markdown.ts +0 -121
- package/src/lobby/mini.ts +0 -167
- package/src/lobby/tabs/excalidraw.ts +0 -95
- package/src/lobby/tabs/git.ts +0 -162
- package/src/lobby/tabs/home.ts +0 -544
- package/src/lobby/tabs/issues.ts +0 -73
- package/src/lobby/tabs/knowledge.ts +0 -135
- package/src/lobby/tabs/metrics.ts +0 -300
- package/src/lobby/tabs/plan.ts +0 -262
- package/src/lobby/tabs/quickfix.ts +0 -136
- package/src/lobby/tabs/tasks.ts +0 -412
- package/src/lobby/theme.ts +0 -30
- package/src/lobby/view.ts +0 -3018
- package/src/pi/settings-ui.ts +0 -708
- package/src/width.ts +0 -102
- package/webui/dist/assets/index-Bk-lpAGm.css +0 -2
- package/webui/dist/assets/index-E6MPcbdC.js +0 -80
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain Markdown helpers the lobby shares: model text with escaped line
|
|
3
|
+
* breaks made real, and a reply split into blocks that can be rendered one by
|
|
4
|
+
* one (the page renders them, not the terminal).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Line breaks a model wrote out as `\n` (text escaped twice on its way here,
|
|
9
|
+
* so a whole plan arrives as one line): made real when the text has no real
|
|
10
|
+
* line break at all, outside inline code, where `\n` is usually meant.
|
|
11
|
+
*/
|
|
12
|
+
export function unescapeBreaks(text: string): string {
|
|
13
|
+
if (text.includes("\n") || !text.includes("\\n")) return text;
|
|
14
|
+
return text
|
|
15
|
+
.split(/(`[^`\n]*`)/)
|
|
16
|
+
.map((part, index) => (index % 2 === 1 ? part : part.replace(/(?:\\r)?\\n/g, "\n")))
|
|
17
|
+
.join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A fenced code block's opening or closing line. */
|
|
21
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
22
|
+
/** A list item's first line: after a blank line it continues the list above, so blocks never split there. */
|
|
23
|
+
const LIST_ITEM = /^(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Markdown split into blocks that render, one by one, into the same output as
|
|
27
|
+
* the whole (each block's lines, a blank line between blocks, blocks that
|
|
28
|
+
* render to nothing left out). A split falls on a blank line outside code
|
|
29
|
+
* fences that is followed by an unindented line that does not start a list
|
|
30
|
+
* item — the only places where one block can never run into the next. A
|
|
31
|
+
* streaming reply can then keep its finished blocks rendered and render only
|
|
32
|
+
* the one still being written.
|
|
33
|
+
*/
|
|
34
|
+
export function markdownBlocks(text: string): string[] {
|
|
35
|
+
const lines = text.split("\n");
|
|
36
|
+
const blocks: string[] = [];
|
|
37
|
+
let fence: string | undefined;
|
|
38
|
+
let start = 0;
|
|
39
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
40
|
+
const line = lines[index]!;
|
|
41
|
+
const marker = FENCE.exec(line)?.[1];
|
|
42
|
+
if (marker) {
|
|
43
|
+
if (!fence) fence = marker;
|
|
44
|
+
else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (fence || line.trim() || index === 0) continue;
|
|
48
|
+
const next = lines[index + 1];
|
|
49
|
+
if (next === undefined || !/^\S/.test(next) || LIST_ITEM.test(next)) continue;
|
|
50
|
+
const block = lines.slice(start, index).join("\n");
|
|
51
|
+
if (block.trim()) blocks.push(block);
|
|
52
|
+
start = index + 1;
|
|
53
|
+
}
|
|
54
|
+
const last = lines.slice(start).join("\n");
|
|
55
|
+
if (last.trim()) blocks.push(last);
|
|
56
|
+
return blocks;
|
|
57
|
+
}
|
package/src/lobby/feed.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
9
9
|
import { agentName } from "../pi/run-summary.ts";
|
|
10
|
-
import { markdownBlocks } from "./
|
|
10
|
+
import { markdownBlocks } from "./blocks.ts";
|
|
11
11
|
|
|
12
12
|
export type ActivityKind = "info" | "success" | "warning" | "error";
|
|
13
13
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lobby's backend offers: the service interface the web server
|
|
3
|
+
* drives, and the shapes of the sessions it lists. Pure types (the
|
|
4
|
+
* implementation is `service.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import type { Task } from "../schemas/task.ts";
|
|
7
|
+
import type { AgentRun } from "../schemas/findings.ts";
|
|
8
|
+
import type { PlannedTask } from "../state/backlog.ts";
|
|
9
|
+
import type { PlanComment } from "../state/comments.ts";
|
|
10
|
+
import type { MetricRecord } from "../state/metrics.ts";
|
|
11
|
+
import type { BotLobbyConfig, LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
|
|
12
|
+
import type { ChatEntry, LobbyFeed } from "./feed.ts";
|
|
13
|
+
import type { BackgroundSession } from "./sessions.ts";
|
|
14
|
+
import type { QuickFixQueue } from "./quickfix.ts";
|
|
15
|
+
import type { PlannerSeed, PlanningSession } from "./planner.ts";
|
|
16
|
+
import type { IssuesState } from "./issues.ts";
|
|
17
|
+
import type { PullsState } from "./pulls.ts";
|
|
18
|
+
import type { PullReviews } from "./pr-review.ts";
|
|
19
|
+
import type { KnowledgeBook } from "./knowledge.ts";
|
|
20
|
+
import type { ExcalidrawBook, ExcalidrawSession } from "../excalidraw/sessions.ts";
|
|
21
|
+
import type { WorkspaceInfo } from "../execution/workspace.ts";
|
|
22
|
+
|
|
23
|
+
export { TAB_IDS, TAB_LABELS, visibleTabs } from "./prompts.ts";
|
|
24
|
+
export type { TabId } from "./prompts.ts";
|
|
25
|
+
|
|
26
|
+
/** What a checked Excalidraw session reported. */
|
|
27
|
+
export interface SessionCheck {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Which session the Lobby tab shows and talks to: this window, one it started
|
|
34
|
+
* in the background, one running in another terminal, or a task whose
|
|
35
|
+
* session is not running at all.
|
|
36
|
+
*/
|
|
37
|
+
export type SessionView =
|
|
38
|
+
| { kind: "here" }
|
|
39
|
+
| { kind: "background"; key: string }
|
|
40
|
+
| { kind: "other"; sessionId: string }
|
|
41
|
+
| { kind: "idle"; taskId: string };
|
|
42
|
+
|
|
43
|
+
export type SessionWhere = "this window" | "background" | "other terminal" | "not running";
|
|
44
|
+
|
|
45
|
+
/** One row of the session browser. */
|
|
46
|
+
export interface SessionEntry {
|
|
47
|
+
view: SessionView;
|
|
48
|
+
name: string;
|
|
49
|
+
where: SessionWhere;
|
|
50
|
+
task?: Task;
|
|
51
|
+
/** What it is doing: working, idle, starting, ended, or its task's state. */
|
|
52
|
+
status: string;
|
|
53
|
+
auto: boolean;
|
|
54
|
+
/** Questions it waits on you for. */
|
|
55
|
+
waiting: number;
|
|
56
|
+
/** The pi session it is (absent for a task no session ever owned). */
|
|
57
|
+
sessionId?: string;
|
|
58
|
+
/** The process running it, for sessions in other terminals. */
|
|
59
|
+
pid?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A session running in this project, as its heartbeat tells (see state/presence.ts). */
|
|
63
|
+
export interface LiveSession {
|
|
64
|
+
sessionId: string;
|
|
65
|
+
pid: number;
|
|
66
|
+
name?: string;
|
|
67
|
+
taskId?: string;
|
|
68
|
+
mode: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What `switchTo` needs: the session to run in this window, and the background process to stop first. */
|
|
72
|
+
export interface SwitchTarget {
|
|
73
|
+
name: string;
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
background?: BackgroundSession;
|
|
76
|
+
/** A task no session owns: this window claims it instead. */
|
|
77
|
+
claimTaskId?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One model Pi offers, for the settings page's picker: `provider/id`, its label and thinking levels. */
|
|
81
|
+
export interface ModelChoice {
|
|
82
|
+
id: string;
|
|
83
|
+
label: string;
|
|
84
|
+
thinkingLevels: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Everything the web server needs from pi and bot-lobby. */
|
|
88
|
+
export interface LobbyService {
|
|
89
|
+
sessionId(): string | undefined;
|
|
90
|
+
/** This session's active task and its runs, as the lobby reads them. */
|
|
91
|
+
zen(): { task?: Task; runs: readonly AgentRun[] };
|
|
92
|
+
feed: LobbyFeed;
|
|
93
|
+
masterBusy(): boolean;
|
|
94
|
+
tasks(): Task[];
|
|
95
|
+
plans(): PlannedTask[];
|
|
96
|
+
comments(taskId: string): PlanComment[];
|
|
97
|
+
metrics(): MetricRecord[];
|
|
98
|
+
/** Classifier calls (kind `classifier`), kept out of the agent tables. */
|
|
99
|
+
classifierMetrics?(): MetricRecord[];
|
|
100
|
+
/** Send text to the oracle, or start a task when none is active; returns a notice. */
|
|
101
|
+
toOracle(text: string): string | undefined;
|
|
102
|
+
comment(taskId: string, text: string): string;
|
|
103
|
+
startPlanned(plan: PlannedTask): string;
|
|
104
|
+
discardPlan(id: string): void;
|
|
105
|
+
abortMaster(): void;
|
|
106
|
+
quickfix: QuickFixQueue;
|
|
107
|
+
planner(): PlanningSession | undefined;
|
|
108
|
+
/** Start a planning session (replacing any other) with these seats on the panel. */
|
|
109
|
+
newPlanner(seed?: PlannerSeed, seats?: readonly PanelMember[]): PlanningSession;
|
|
110
|
+
/** The oracle puts the panel's open questions to the user, one questionnaire at a time; returns a notice. */
|
|
111
|
+
answerPanel(): Promise<string>;
|
|
112
|
+
/** Save the plan as a pending task, offering to split a long one into several first; returns a notice. */
|
|
113
|
+
savePlan(): Promise<string>;
|
|
114
|
+
/** The seats a new session starts with, from settings. */
|
|
115
|
+
defaultPanel(): readonly PanelMember[];
|
|
116
|
+
/** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
|
|
117
|
+
planningRounds?(): number;
|
|
118
|
+
/** `model · thinking` a panel seat runs on. */
|
|
119
|
+
seatLabel(member: PanelMember): string;
|
|
120
|
+
issues: IssuesState;
|
|
121
|
+
/** The Git tab's pull requests, read through `gh`. */
|
|
122
|
+
pulls: PullsState;
|
|
123
|
+
/** Reviews of pull requests by an agent, and Jev's reads of them. */
|
|
124
|
+
reviews: PullReviews;
|
|
125
|
+
/** Every agent's knowledge files: read, edited, commented on. */
|
|
126
|
+
knowledge: KnowledgeBook;
|
|
127
|
+
/** The shared Excalidraw sessions (five at most) and which agents each is assigned to. */
|
|
128
|
+
excalidraw: ExcalidrawBook;
|
|
129
|
+
/** Join a session's room for a moment and report what is there. */
|
|
130
|
+
checkExcalidraw(session: ExcalidrawSession): Promise<SessionCheck>;
|
|
131
|
+
/** The effective config, never a secret; the web settings page reads it. */
|
|
132
|
+
config?(): BotLobbyConfig;
|
|
133
|
+
/** Save a whole config, the way the settings menu does; the web settings page writes it. */
|
|
134
|
+
saveConfig?(config: BotLobbyConfig): void;
|
|
135
|
+
/** Reload cached config (keys, panes) after a settings change. */
|
|
136
|
+
configChanged?(): void;
|
|
137
|
+
/** The models Pi offers, for the settings page's picker. */
|
|
138
|
+
models?(): ModelChoice[];
|
|
139
|
+
/** The model this window's session runs on (`provider/id`), when it has one. */
|
|
140
|
+
sessionModel?(): string | undefined;
|
|
141
|
+
/** The Issues tab is switched on (`lobby.issues`). */
|
|
142
|
+
issuesEnabled(): boolean;
|
|
143
|
+
/** This window's pi session name, when it has one. */
|
|
144
|
+
sessionName(): string | undefined;
|
|
145
|
+
/** Background sessions this window started, oldest first. */
|
|
146
|
+
sessions(): readonly BackgroundSession[];
|
|
147
|
+
/** Start a task in a new background session, named after the task; the session, or why not. */
|
|
148
|
+
startSession(start: { request?: string; plan?: PlannedTask; auto?: boolean }): BackgroundSession | string;
|
|
149
|
+
/** Auto mode for any task, whichever session drives it. */
|
|
150
|
+
isAuto(taskId: string): boolean;
|
|
151
|
+
setAuto(taskId: string, on: boolean): void;
|
|
152
|
+
/** Leave a message for a task's oracle, delivered by whichever session drives it (now or once one resumes it); returns a notice. */
|
|
153
|
+
sendToTask(taskId: string, text: string): string;
|
|
154
|
+
/** Leave a message for a session running in another terminal; it delivers it within seconds. Returns a notice. */
|
|
155
|
+
sendToSession(sessionId: string, text: string): string;
|
|
156
|
+
/** Sessions running in this project, other than this window's, from their heartbeats. */
|
|
157
|
+
liveSessions(): readonly LiveSession[];
|
|
158
|
+
/** Run another session in this window (stopping its background process first); resolves to a notice. */
|
|
159
|
+
switchTo(target: SwitchTarget): Promise<string>;
|
|
160
|
+
/** Archived tasks, most recently archived first. */
|
|
161
|
+
archivedTasks(): Task[];
|
|
162
|
+
/** Archive, restore or delete a task; each returns a notice. */
|
|
163
|
+
archiveTask(taskId: string): string;
|
|
164
|
+
restoreTask(taskId: string): string;
|
|
165
|
+
deleteTask(taskId: string, where: "list" | "archive"): string;
|
|
166
|
+
/** Another session's conversation, read from its saved session file (its newest messages). */
|
|
167
|
+
sessionChat(sessionId: string): ChatEntry[];
|
|
168
|
+
/** Whether another session has messages earlier than `sessionChat` returns. */
|
|
169
|
+
hasOlderChat(sessionId: string): boolean;
|
|
170
|
+
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
171
|
+
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
172
|
+
profileLabel(kind: LobbyAgentKind): string;
|
|
173
|
+
/** The repository (or folder) and branch the title shows; the folder's name alone until git has answered. */
|
|
174
|
+
workspace?(): WorkspaceInfo;
|
|
175
|
+
/** Ask for the workspace to be read again (the branch may have changed); it arrives through `workspace()`. */
|
|
176
|
+
refreshWorkspace?(): void;
|
|
177
|
+
}
|
|
178
|
+
|
package/src/lobby/keys.ts
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The lobby's shortcuts in one table: every action has a default key, a
|
|
3
|
-
* for the help overlay, and can be rebound under `lobby.keys` in the
|
|
4
|
-
* (`{ "
|
|
5
|
-
*
|
|
2
|
+
* The web lobby's shortcuts in one table: every action has a default key, a
|
|
3
|
+
* line for the help overlay, and can be rebound under `lobby.keys` in the
|
|
4
|
+
* config (`{ "search": "alt+f" }`). Chords are written `alt+k`,
|
|
5
|
+
* `ctrl+f`, `shift+tab`; the page matches them on the physical key.
|
|
6
6
|
*/
|
|
7
|
-
import { Key, matchesKey, type KeyId } from "@earendil-works/pi-tui";
|
|
8
7
|
|
|
9
8
|
export const LOBBY_ACTIONS = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
newSession: { key: "alt+n", help: "start a task in a new session, named after it" },
|
|
17
|
-
toggleAuto: { key: "alt+g", help: "auto mode: the oracle drives the task to completion without asking" },
|
|
18
|
-
nextTab: { key: "tab", help: "next tab" },
|
|
19
|
-
prevTab: { key: "shift+tab", help: "previous tab" },
|
|
20
|
-
toggleConversation: { key: "alt+c", help: "show or hide the conversation" },
|
|
21
|
-
toggleActivity: { key: "alt+a", help: "show or hide the activity log" },
|
|
22
|
-
toggleThinking: { key: "alt+k", help: "show or hide thinking" },
|
|
23
|
-
scrollUp: { key: "pageUp", help: "scroll up a page" },
|
|
24
|
-
scrollDown: { key: "pageDown", help: "scroll down a page" },
|
|
9
|
+
help: { key: "alt+h", help: "show or hide this list" },
|
|
10
|
+
settings: { key: "alt+s", help: "settings: each agent's model and effort" },
|
|
11
|
+
savePlan: { key: "ctrl+s", help: "save the plan from the Plan tab" },
|
|
12
|
+
sessions: { key: "alt+o", help: "browse and message your sessions" },
|
|
13
|
+
nextTab: { key: "alt+]", help: "next tab" },
|
|
14
|
+
prevTab: { key: "alt+[", help: "previous tab" },
|
|
25
15
|
} as const;
|
|
26
16
|
|
|
27
17
|
export type LobbyAction = keyof typeof LOBBY_ACTIONS;
|
|
@@ -37,27 +27,6 @@ export function keyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap
|
|
|
37
27
|
return map;
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
/** Alt+digit jumps to the tab at `index`; the same key the TUI matches. */
|
|
41
|
-
export function tabJumpKey(index: number): KeyId {
|
|
42
|
-
return Key.alt(String(index + 1) as "1");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** `keyMap` with the web-only tab cycling defaults; user overrides win. */
|
|
46
|
-
export function webKeyMap(overrides: Readonly<Record<string, string>> = {}): KeyMap {
|
|
47
|
-
const map = keyMap(overrides);
|
|
48
|
-
if (!overrides.nextTab?.trim()) map.nextTab = "alt+]";
|
|
49
|
-
if (!overrides.prevTab?.trim()) map.prevTab = "alt+[";
|
|
50
|
-
return map;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The action `data` triggers under `map`, if any. */
|
|
54
|
-
export function actionFor(data: string, map: KeyMap): LobbyAction | undefined {
|
|
55
|
-
for (const action of Object.keys(map) as LobbyAction[]) {
|
|
56
|
-
if (matchesKey(data, map[action] as KeyId)) return action;
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
30
|
/** `alt+k` reads `Alt+K` in hints and help. */
|
|
62
31
|
export function keyLabel(key: string): string {
|
|
63
32
|
return key
|
package/src/lobby/prompt-hub.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bot-lobby's questions
|
|
3
|
-
* answer
|
|
4
|
-
* `
|
|
5
|
-
* cancelled caller's `signal`
|
|
6
|
-
*
|
|
2
|
+
* Bot-lobby's questions, answered in the web page. `open` puts a prompt in
|
|
3
|
+
* the page's queue, `answer` settles it once (a late answer is refused), and
|
|
4
|
+
* `ask` is what the agents' code awaits: it opens a prompt and resolves with
|
|
5
|
+
* the answer, or with how it was put away. A cancelled caller's `signal`
|
|
6
|
+
* withdraws the prompt.
|
|
7
7
|
*/
|
|
8
8
|
import { lobbyTopics } from "./topics.ts";
|
|
9
9
|
|
|
10
10
|
export type PromptKind = "questionnaire" | "choose" | "confirm" | "text" | "sessionDialog";
|
|
11
11
|
|
|
12
|
-
/** A pending question in the shape the
|
|
12
|
+
/** A pending question in the shape the web API lists. */
|
|
13
13
|
export interface WebPrompt {
|
|
14
14
|
id: string;
|
|
15
15
|
kind: PromptKind;
|
|
@@ -18,13 +18,6 @@ export interface WebPrompt {
|
|
|
18
18
|
payload: unknown;
|
|
19
19
|
}
|
|
20
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
21
|
interface Pending {
|
|
29
22
|
prompt: WebPrompt;
|
|
30
23
|
}
|
|
@@ -42,30 +35,13 @@ function nextId(seq: number): string {
|
|
|
42
35
|
return `p${seq}-${Date.now().toString(36)}`;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
|
|
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. */
|
|
38
|
+
/** The questions waiting for the page; the first answer wins. */
|
|
54
39
|
export class PromptHub {
|
|
55
40
|
private seq = 0;
|
|
56
41
|
private readonly pendingMap = new Map<string, Pending>();
|
|
57
|
-
private readonly
|
|
42
|
+
private readonly waiters = new Map<string, (outcome: Outcome) => void>();
|
|
58
43
|
private readonly outcomes: Outcome[] = [];
|
|
59
44
|
|
|
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
45
|
/** The questions still waiting for an answer, oldest first. */
|
|
70
46
|
pending(): WebPrompt[] {
|
|
71
47
|
return [...this.pendingMap.values()].map((entry) => entry.prompt);
|
|
@@ -76,14 +52,13 @@ export class PromptHub {
|
|
|
76
52
|
this.seq += 1;
|
|
77
53
|
const prompt: WebPrompt = { id: nextId(this.seq), kind, createdAt: Date.now(), from, payload };
|
|
78
54
|
this.pendingMap.set(prompt.id, { prompt });
|
|
79
|
-
showOn(this.surfaces.values(), prompt);
|
|
80
55
|
lobbyTopics.bump("prompts");
|
|
81
56
|
return prompt;
|
|
82
57
|
}
|
|
83
58
|
|
|
84
59
|
/**
|
|
85
60
|
* Settle `id` with `value`. True when this call won; false when the prompt
|
|
86
|
-
* is already settled or unknown (the
|
|
61
|
+
* is already settled or unknown (the web API answers 409 then).
|
|
87
62
|
*/
|
|
88
63
|
answer(id: string, value: unknown): boolean {
|
|
89
64
|
if (!this.pendingMap.has(id)) return false;
|
|
@@ -105,57 +80,35 @@ export class PromptHub {
|
|
|
105
80
|
|
|
106
81
|
private settle(id: string, value: unknown, how: Outcome["how"]): void {
|
|
107
82
|
this.pendingMap.delete(id);
|
|
108
|
-
|
|
109
|
-
this.outcomes.push(
|
|
83
|
+
const outcome: Outcome = { id, value, how };
|
|
84
|
+
this.outcomes.push(outcome);
|
|
110
85
|
if (this.outcomes.length > MAX_OUTCOMES) this.outcomes.splice(0, this.outcomes.length - MAX_OUTCOMES);
|
|
86
|
+
const waiter = this.waiters.get(id);
|
|
87
|
+
this.waiters.delete(id);
|
|
88
|
+
waiter?.(outcome);
|
|
111
89
|
lobbyTopics.bump("prompts");
|
|
112
90
|
}
|
|
113
91
|
|
|
114
92
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* all. With only the terminal registered this is exactly today's behaviour.
|
|
93
|
+
* Open a prompt and wait for its outcome: the page's answer, or `dismissed`
|
|
94
|
+
* / `cancelled` when it is put away or `signal` aborts first.
|
|
118
95
|
*/
|
|
119
|
-
|
|
120
|
-
const prompt = this.open(kind, from, payload);
|
|
96
|
+
ask(kind: PromptKind, from: string, payload: unknown, options?: { signal?: AbortSignal }): Promise<{ how: Outcome["how"]; value: unknown }> {
|
|
121
97
|
const signal = options?.signal;
|
|
122
|
-
if (signal?.aborted) return
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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;
|
|
98
|
+
if (signal?.aborted) return Promise.resolve({ how: "cancelled", value: undefined });
|
|
99
|
+
const prompt = this.open(kind, from, payload);
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const onAbort = () => {
|
|
102
|
+
if (this.pendingMap.has(prompt.id)) this.settle(prompt.id, undefined, "cancelled");
|
|
103
|
+
};
|
|
104
|
+
this.waiters.set(prompt.id, (outcome) => {
|
|
105
|
+
signal?.removeEventListener("abort", onAbort);
|
|
106
|
+
resolve({ how: outcome.how, value: outcome.value });
|
|
107
|
+
});
|
|
108
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
109
|
+
});
|
|
157
110
|
}
|
|
158
111
|
}
|
|
159
112
|
|
|
160
|
-
/** The process-wide hub
|
|
113
|
+
/** The process-wide hub. */
|
|
161
114
|
export const promptHub = new PromptHub();
|
package/src/lobby/prompts.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Strings the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* this file directly.
|
|
2
|
+
* Strings the server and the page share: tab metadata and the short line
|
|
3
|
+
* shown when a question was answered elsewhere. Pure on purpose (no
|
|
4
|
+
* `node:*`): the web page imports this file directly.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "
|
|
7
|
+
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "excalidraw", "git", "knowledge", "metrics"] as const;
|
|
9
8
|
export type TabId = (typeof TAB_IDS)[number];
|
|
10
9
|
|
|
11
10
|
export const TAB_LABELS: Record<TabId, string> = {
|
|
@@ -25,11 +24,8 @@ export function visibleTabs(issues: boolean): TabId[] {
|
|
|
25
24
|
return TAB_IDS.filter((tab) => issues || tab !== "issues");
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
/** A question answered in
|
|
29
|
-
export const
|
|
27
|
+
/** A question answered in another window of the page (or put away) before this one's answer arrived. */
|
|
28
|
+
export const ANSWERED_ELSEWHERE = "already answered in another window";
|
|
30
29
|
|
|
31
|
-
/**
|
|
32
|
-
export const
|
|
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";
|
|
30
|
+
/** Opens the list of files a message carries (see src/webui/uploads.ts); the page shows them as thumbnails and chips. */
|
|
31
|
+
export const ATTACHMENTS_MARK = "Attached files:";
|