@a-t-h-i/bot-lobby 0.6.8 → 0.6.9
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 +50 -1
- package/package.json +5 -1
- package/src/excalidraw/check.ts +31 -0
- package/src/excalidraw/client.ts +266 -0
- package/src/excalidraw/room.ts +118 -0
- package/src/excalidraw/scene.ts +746 -0
- package/src/excalidraw/sessions.ts +302 -0
- package/src/excalidraw/tools.ts +223 -0
- package/src/execution/agent-runner.ts +2 -0
- package/src/execution/pi-runner.ts +9 -3
- package/src/index.ts +3 -0
- package/src/lobby/planner.ts +6 -1
- package/src/lobby/quickfix.ts +2 -0
- package/src/lobby/runtime.ts +6 -0
- package/src/lobby/tabs/excalidraw.ts +95 -0
- package/src/lobby/view.ts +185 -6
package/src/lobby/runtime.ts
CHANGED
|
@@ -45,6 +45,8 @@ import { execCommand, IssuesState } from "./issues.ts";
|
|
|
45
45
|
import { PullsState } from "./pulls.ts";
|
|
46
46
|
import { PullReviews } from "./pr-review.ts";
|
|
47
47
|
import { KnowledgeBook } from "./knowledge.ts";
|
|
48
|
+
import { ExcalidrawBook } from "../excalidraw/sessions.ts";
|
|
49
|
+
import { checkSession } from "../excalidraw/check.ts";
|
|
48
50
|
import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type TabId } from "./view.ts";
|
|
49
51
|
import { lobbyTheme } from "./theme.ts";
|
|
50
52
|
import { deliverComments, onOwnerEvent, setAuto } from "../pi/owner.ts";
|
|
@@ -70,6 +72,7 @@ interface Runtime {
|
|
|
70
72
|
pulls: PullsState;
|
|
71
73
|
reviews: PullReviews;
|
|
72
74
|
knowledge: KnowledgeBook;
|
|
75
|
+
excalidraw: ExcalidrawBook;
|
|
73
76
|
unsubscribeFeed?: () => void;
|
|
74
77
|
/** Puts the panel's questions to the user: the questionnaire unless a test sets another. */
|
|
75
78
|
asker?: Asker;
|
|
@@ -624,6 +627,8 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
624
627
|
pulls: state.pulls,
|
|
625
628
|
reviews: state.reviews,
|
|
626
629
|
knowledge: state.knowledge,
|
|
630
|
+
excalidraw: state.excalidraw,
|
|
631
|
+
checkExcalidraw: (session) => checkSession(session.link, session.name),
|
|
627
632
|
editText: (title, text) => editText(state, title, text),
|
|
628
633
|
profileLabel: (kind) => {
|
|
629
634
|
const profile = lobbyProfile(state, kind);
|
|
@@ -848,6 +853,7 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
848
853
|
backups: () => loadConfig().knowledge.backupCount,
|
|
849
854
|
sessionId: () => ctx.sessionManager.getSessionId(),
|
|
850
855
|
}),
|
|
856
|
+
excalidraw: new ExcalidrawBook({ root }),
|
|
851
857
|
};
|
|
852
858
|
state.reviews = new PullReviews({
|
|
853
859
|
cwd: ctx.cwd,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Excalidraw tab: the shared sessions the user has added (five at most),
|
|
3
|
+
* on the left, and the picked one on the right — its link, whether agents may
|
|
4
|
+
* draw in it, the last check of it, and every agent it can be assigned to as a
|
|
5
|
+
* checklist. Assigned agents get the Excalidraw tools and join the room under
|
|
6
|
+
* their own name.
|
|
7
|
+
*/
|
|
8
|
+
import { AGENT_LABELS, EXCALIDRAW_AGENTS, MAX_SESSIONS, type ExcalidrawSession } from "../../excalidraw/sessions.ts";
|
|
9
|
+
import { bold, columns, fill, paint, rule, selectRow, spinner, split, spread, windowStart, wrap, type LobbyTheme } from "../layout.ts";
|
|
10
|
+
|
|
11
|
+
/** What the last check of a session found. */
|
|
12
|
+
export interface SessionCheck {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExcalidrawTabInput {
|
|
18
|
+
sessions: readonly ExcalidrawSession[];
|
|
19
|
+
selected: number;
|
|
20
|
+
focus: "list" | "detail";
|
|
21
|
+
/** The agent under the cursor in the checklist. */
|
|
22
|
+
agent: number;
|
|
23
|
+
checks: ReadonlyMap<string, SessionCheck>;
|
|
24
|
+
/** Ids of the sessions being checked now. */
|
|
25
|
+
checking: ReadonlySet<string>;
|
|
26
|
+
tick: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const EXCALIDRAW_COLUMNS_MIN = 90;
|
|
30
|
+
|
|
31
|
+
function agentCount(session: ExcalidrawSession): string {
|
|
32
|
+
const count = session.agents.length;
|
|
33
|
+
return count === 0 ? "no agents" : `${count} agent${count === 1 ? "" : "s"}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The sessions, the selected one in view. */
|
|
37
|
+
export function sessionRows(input: ExcalidrawTabInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
38
|
+
const rows = input.sessions.map((session, index) => {
|
|
39
|
+
const selected = index === input.selected;
|
|
40
|
+
const mark = input.checking.has(session.id) ? paint(theme, "accent", spinner(input.tick)) : input.checks.get(session.id)?.ok === false ? paint(theme, "warning", "!") : input.checks.has(session.id) ? paint(theme, "success", "✓") : " ";
|
|
41
|
+
const facts = paint(theme, session.agents.length > 0 ? "accent" : "dim", agentCount(session));
|
|
42
|
+
return selectRow(theme, spread(`${mark} ${selected ? bold(theme, session.name) : session.name}`, facts, width - 2), width, selected, input.focus === "list");
|
|
43
|
+
});
|
|
44
|
+
return rows.slice(windowStart(input.selected, rows.length, height), windowStart(input.selected, rows.length, height) + height);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyLines(width: number, theme?: LobbyTheme): string[] {
|
|
48
|
+
return [
|
|
49
|
+
...wrap(paint(theme, "dim", "No sessions yet. A session is a live Excalidraw room that you and your agents draw in together."), width),
|
|
50
|
+
"",
|
|
51
|
+
...wrap(`${paint(theme, "accent", "a")} add one: in Excalidraw, Share → Live collaboration → Start session, then paste the link`, width),
|
|
52
|
+
...wrap(`${paint(theme, "accent", "n")} or make a new room here and open its link in Excalidraw`, width),
|
|
53
|
+
"",
|
|
54
|
+
...wrap(paint(theme, "dim", `Up to ${MAX_SESSIONS} sessions; each can be assigned to one agent or several.`), width),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The picked session: its link and state, then the agents it can be assigned to. */
|
|
59
|
+
export function detailLines(input: ExcalidrawTabInput, width: number, theme?: LobbyTheme): string[] {
|
|
60
|
+
const session = input.sessions[input.selected];
|
|
61
|
+
if (!session) return [];
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
lines.push(...wrap(paint(theme, "muted", session.link), width));
|
|
64
|
+
lines.push(paint(theme, session.contribute ? "success" : "warning", session.contribute ? "agents may draw here (w: look only)" : "agents may only look (w: let them draw)"));
|
|
65
|
+
const check = input.checks.get(session.id);
|
|
66
|
+
if (input.checking.has(session.id)) lines.push(paint(theme, "accent", `${spinner(input.tick)} checking the room…`));
|
|
67
|
+
else if (check) lines.push(...wrap(paint(theme, check.ok ? "success" : "warning", `${check.ok ? "✓" : "!"} ${check.text}`), width));
|
|
68
|
+
else lines.push(paint(theme, "dim", "t checks that the room can be reached"));
|
|
69
|
+
lines.push("", rule(width, "Assigned to", theme, agentCount(session)));
|
|
70
|
+
EXCALIDRAW_AGENTS.forEach((agent, index) => {
|
|
71
|
+
const on = session.agents.includes(agent);
|
|
72
|
+
const picked = input.focus === "detail" && index === input.agent;
|
|
73
|
+
const box = on ? paint(theme, "success", "[x]") : paint(theme, "dim", "[ ]");
|
|
74
|
+
const cursor = picked ? paint(theme, "accent", "▸") : " ";
|
|
75
|
+
const label = on ? AGENT_LABELS[agent] : paint(theme, "muted", AGENT_LABELS[agent]);
|
|
76
|
+
lines.push(selectRow(theme, `${cursor} ${box} ${picked ? bold(theme, label) : label}`, width, picked, true));
|
|
77
|
+
});
|
|
78
|
+
lines.push("", ...wrap(paint(theme, "dim", "Assigned agents read the board and draw on it with excalidraw_read and excalidraw_draw, and appear in the room under their own name."), width));
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function renderExcalidraw(input: ExcalidrawTabInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
83
|
+
if (height <= 0) return [];
|
|
84
|
+
const count = `${input.sessions.length}/${MAX_SESSIONS}`;
|
|
85
|
+
if (input.sessions.length === 0) return fill([rule(width, "Excalidraw", theme, count), ...emptyLines(width, theme)], height, width);
|
|
86
|
+
const selected = Math.min(Math.max(0, input.selected), input.sessions.length - 1);
|
|
87
|
+
const view = { ...input, selected };
|
|
88
|
+
const wide = width >= EXCALIDRAW_COLUMNS_MIN;
|
|
89
|
+
const [listWidth, detailWidth] = wide ? split(width, 0.34, 3, 44) : [width, width];
|
|
90
|
+
const listPane = fill([rule(listWidth, "Sessions", theme, count), ...sessionRows(view, listWidth, height - 1, theme)], height);
|
|
91
|
+
const session = input.sessions[selected]!;
|
|
92
|
+
const detailPane = fill([rule(detailWidth, `${session.name}${input.focus === "detail" ? " ◂" : ""}`, theme), ...detailLines(view, detailWidth, theme)], height);
|
|
93
|
+
if (!wide) return fill(input.focus === "detail" ? detailPane : listPane, height, width);
|
|
94
|
+
return fill(columns(listPane, detailPane, listWidth, detailWidth, " │ ", theme), height, width);
|
|
95
|
+
}
|
package/src/lobby/view.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
|
32
32
|
import { renderIssues } from "./tabs/issues.ts";
|
|
33
33
|
import { renderGit } from "./tabs/git.ts";
|
|
34
34
|
import { renderKnowledge, type KnowledgeLayout } from "./tabs/knowledge.ts";
|
|
35
|
+
import { renderExcalidraw, type SessionCheck } from "./tabs/excalidraw.ts";
|
|
36
|
+
import { EXCALIDRAW_AGENTS, MAX_SESSIONS, type ExcalidrawBook, type ExcalidrawSession } from "../excalidraw/sessions.ts";
|
|
35
37
|
import type { EntryRef, KnowledgeBook, KnowledgeFileInfo, KnowledgeView } from "./knowledge.ts";
|
|
36
38
|
import type { PullsState } from "./pulls.ts";
|
|
37
39
|
import type { PullReviews } from "./pr-review.ts";
|
|
@@ -41,7 +43,7 @@ import { filterRecords, renderMetrics } from "./tabs/metrics.ts";
|
|
|
41
43
|
/** The mark before a branch name in the title (shared with the task details). */
|
|
42
44
|
export { BRANCH_GLYPH };
|
|
43
45
|
|
|
44
|
-
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "metrics", "git", "knowledge"] as const;
|
|
46
|
+
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "metrics", "git", "knowledge", "excalidraw"] as const;
|
|
45
47
|
export type TabId = (typeof TAB_IDS)[number];
|
|
46
48
|
|
|
47
49
|
export const TAB_LABELS: Record<TabId, string> = {
|
|
@@ -53,6 +55,7 @@ export const TAB_LABELS: Record<TabId, string> = {
|
|
|
53
55
|
metrics: "Metrics",
|
|
54
56
|
git: "Git",
|
|
55
57
|
knowledge: "Knowledge",
|
|
58
|
+
excalidraw: "Excalidraw",
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
/** The tabs on show: Issues only while `lobby.issues` switches it on. */
|
|
@@ -175,6 +178,10 @@ export interface LobbyHost {
|
|
|
175
178
|
reviews: PullReviews;
|
|
176
179
|
/** Every agent's knowledge files: read, edited, commented on. */
|
|
177
180
|
knowledge: KnowledgeBook;
|
|
181
|
+
/** The shared Excalidraw sessions (five at most) and which agents each is assigned to. */
|
|
182
|
+
excalidraw: ExcalidrawBook;
|
|
183
|
+
/** Join a session's room for a moment and report what is there. */
|
|
184
|
+
checkExcalidraw(session: ExcalidrawSession): Promise<SessionCheck>;
|
|
178
185
|
/** Edit a text in pi's multi-line editor (the lobby steps aside meanwhile); the new text, or undefined when cancelled. */
|
|
179
186
|
editText(title: string, text: string): Promise<string | undefined>;
|
|
180
187
|
/** The Issues tab is switched on (`lobby.issues`). */
|
|
@@ -380,13 +387,21 @@ export class LobbyView implements Component, Focusable {
|
|
|
380
387
|
private readonly kLayout: KnowledgeLayout = { offset: 0, total: 0, rows: 0 };
|
|
381
388
|
/** The prompt is writing an edit of the picked entry, a note on it, or a new entry after it. */
|
|
382
389
|
private kDraft: { kind: "edit" | "comment" | "add"; entry?: EntryRef } | undefined;
|
|
390
|
+
private xSelected = 0;
|
|
391
|
+
private xFocus: "list" | "detail" = "list";
|
|
392
|
+
/** The agent under the cursor in the picked session's checklist. */
|
|
393
|
+
private xAgent = 0;
|
|
394
|
+
/** The prompt is taking a link to add (with an optional name), the name of a new room, or a new name for the picked session. */
|
|
395
|
+
private xDraft: "add" | "new" | "rename" | undefined;
|
|
396
|
+
private readonly xChecks = new Map<string, SessionCheck>();
|
|
397
|
+
private readonly xChecking = new Set<string>();
|
|
383
398
|
private metricsSelected = 0;
|
|
384
399
|
private metricsBy: GroupBy = "model";
|
|
385
400
|
private metricsSort: SortKey = "runs";
|
|
386
401
|
|
|
387
402
|
/** The Tasks tab lists archived tasks too (v). */
|
|
388
403
|
showArchived = false;
|
|
389
|
-
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; knowledge: { files: KnowledgeFileInfo[]; view?: KnowledgeView }; at: number } = {
|
|
404
|
+
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; knowledge: { files: KnowledgeFileInfo[]; view?: KnowledgeView }; excalidraw: ExcalidrawSession[]; at: number } = {
|
|
390
405
|
tasks: [],
|
|
391
406
|
plans: [],
|
|
392
407
|
archived: [],
|
|
@@ -394,6 +409,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
394
409
|
metrics: [],
|
|
395
410
|
classifierMetrics: [],
|
|
396
411
|
knowledge: { files: [] },
|
|
412
|
+
excalidraw: [],
|
|
397
413
|
at: 0,
|
|
398
414
|
};
|
|
399
415
|
|
|
@@ -486,6 +502,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
486
502
|
metrics: this.tab === "metrics" ? this.host.metrics() : this.data.metrics,
|
|
487
503
|
classifierMetrics: this.tab === "metrics" ? this.host.classifierMetrics?.() ?? [] : this.data.classifierMetrics,
|
|
488
504
|
knowledge: this.tab === "knowledge" ? this.readKnowledge() : this.data.knowledge,
|
|
505
|
+
excalidraw: this.host.excalidraw.list(),
|
|
489
506
|
at: now,
|
|
490
507
|
};
|
|
491
508
|
if (this.tab === "tasks") {
|
|
@@ -535,8 +552,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
535
552
|
if (tab === this.tab) return;
|
|
536
553
|
if (!this.tabs().includes(tab)) return this.say(`the ${TAB_LABELS[tab]} tab is off — set lobby.${tab} to true in the config to bring it back`, "warning");
|
|
537
554
|
// An entry being edited is not a draft to come back to: its text was only lent to the prompt.
|
|
538
|
-
if (this.kDraft) {
|
|
555
|
+
if (this.kDraft || this.xDraft) {
|
|
539
556
|
this.kDraft = undefined;
|
|
557
|
+
this.xDraft = undefined;
|
|
540
558
|
this.editor.setText("");
|
|
541
559
|
}
|
|
542
560
|
this.drafts[this.tab] = this.editor.getText();
|
|
@@ -756,8 +774,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
756
774
|
this.commentTarget = undefined;
|
|
757
775
|
this.issueDraft = false;
|
|
758
776
|
this.gitFocusDraft = false;
|
|
759
|
-
if (this.kDraft) {
|
|
777
|
+
if (this.kDraft || this.xDraft) {
|
|
760
778
|
this.kDraft = undefined;
|
|
779
|
+
this.xDraft = undefined;
|
|
761
780
|
this.editor.setText("");
|
|
762
781
|
}
|
|
763
782
|
this.newSession = false;
|
|
@@ -786,7 +805,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
786
805
|
if (matchesKey(data, Key.home) || matchesKey(data, Key.end)) return this.scrollToEdge(matchesKey(data, Key.home));
|
|
787
806
|
const tabs = this.tabs();
|
|
788
807
|
if (!PROMPT_FIRST.has(this.tab) && /^[1-9]$/.test(data) && Number(data) <= tabs.length) return this.setTab(tabs[Number(data) - 1]!);
|
|
789
|
-
if (data === "i" && this.tab !== "metrics" && this.tab !== "tasks" && this.tab !== "issues" && this.tab !== "git" && this.tab !== "knowledge") return this.setMode("type");
|
|
808
|
+
if (data === "i" && this.tab !== "metrics" && this.tab !== "tasks" && this.tab !== "issues" && this.tab !== "git" && this.tab !== "knowledge" && this.tab !== "excalidraw") return this.setMode("type");
|
|
790
809
|
if (this.tabCommand(data)) return;
|
|
791
810
|
const printable = isPrintable(data);
|
|
792
811
|
if (printable && PROMPT_FIRST.has(this.tab)) {
|
|
@@ -812,6 +831,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
812
831
|
return this.gitFocus;
|
|
813
832
|
case "knowledge":
|
|
814
833
|
return this.kFocus;
|
|
834
|
+
case "excalidraw":
|
|
835
|
+
return this.xFocus;
|
|
815
836
|
case "metrics":
|
|
816
837
|
return "table";
|
|
817
838
|
}
|
|
@@ -864,6 +885,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
864
885
|
case "knowledge":
|
|
865
886
|
this.kFocus = direction < 0 ? "list" : "detail";
|
|
866
887
|
return;
|
|
888
|
+
case "excalidraw":
|
|
889
|
+
this.xFocus = direction < 0 ? "list" : "detail";
|
|
890
|
+
return;
|
|
867
891
|
case "metrics":
|
|
868
892
|
return;
|
|
869
893
|
}
|
|
@@ -899,11 +923,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
899
923
|
return;
|
|
900
924
|
case "knowledge":
|
|
901
925
|
return this.moveKnowledge(delta);
|
|
926
|
+
case "excalidraw":
|
|
927
|
+
return this.moveExcalidraw(delta);
|
|
902
928
|
case "metrics":
|
|
903
929
|
return this.scrollPane("table", delta);
|
|
904
930
|
}
|
|
905
931
|
}
|
|
906
932
|
|
|
933
|
+
/** Arrows on the Excalidraw tab: pick a session, or (in the checklist) an agent. */
|
|
934
|
+
private moveExcalidraw(delta: number): void {
|
|
935
|
+
const step = Math.sign(delta);
|
|
936
|
+
if (this.xFocus === "list") {
|
|
937
|
+
const next = Math.max(0, Math.min(this.data.excalidraw.length - 1, this.xSelected + step));
|
|
938
|
+
if (next !== this.xSelected) this.xAgent = 0;
|
|
939
|
+
this.xSelected = next;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this.xAgent = Math.max(0, Math.min(EXCALIDRAW_AGENTS.length - 1, this.xAgent + step));
|
|
943
|
+
}
|
|
944
|
+
|
|
907
945
|
/** Arrows and pages on the Knowledge tab: pick a file, or pick an entry of the open one (a page moves several). */
|
|
908
946
|
private moveKnowledge(delta: number): void {
|
|
909
947
|
const knowledge = this.data.knowledge;
|
|
@@ -1169,6 +1207,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1169
1207
|
return this.gitCommand(data, enter, escape);
|
|
1170
1208
|
case "knowledge":
|
|
1171
1209
|
return this.knowledgeCommand(data, enter, escape);
|
|
1210
|
+
case "excalidraw":
|
|
1211
|
+
return this.excalidrawCommand(data, enter, escape);
|
|
1172
1212
|
case "metrics":
|
|
1173
1213
|
if (data === "g") return (this.metricsBy = this.metricsBy === "model" ? "model-kind" : "model", this.metricsSelected = 0), true;
|
|
1174
1214
|
if (data === "s") return (this.metricsSort = SORT_KEYS[(SORT_KEYS.indexOf(this.metricsSort) + 1) % SORT_KEYS.length]!), true;
|
|
@@ -1853,6 +1893,110 @@ export class LobbyView implements Component, Focusable {
|
|
|
1853
1893
|
return true;
|
|
1854
1894
|
}
|
|
1855
1895
|
|
|
1896
|
+
/** Assign the picked session to the agent under the cursor, or take it back. */
|
|
1897
|
+
private toggleExcalidrawAgent(session: ExcalidrawSession): void {
|
|
1898
|
+
this.say(this.host.excalidraw.toggleAgent(session.id, EXCALIDRAW_AGENTS[this.xAgent]!));
|
|
1899
|
+
this.data.excalidraw = this.host.excalidraw.list();
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
private excalidrawCommand(data: string, enter: boolean, escape: boolean): boolean {
|
|
1903
|
+
const sessions = this.data.excalidraw;
|
|
1904
|
+
const session = sessions[this.xSelected];
|
|
1905
|
+
if (data === "a" || data === "n") {
|
|
1906
|
+
if (sessions.length >= MAX_SESSIONS) {
|
|
1907
|
+
this.say(`all ${MAX_SESSIONS} sessions are in use — remove one first (d d)`, "warning");
|
|
1908
|
+
return true;
|
|
1909
|
+
}
|
|
1910
|
+
this.xDraft = data === "a" ? "add" : "new";
|
|
1911
|
+
this.xFocus = "list";
|
|
1912
|
+
this.setMode("type");
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1915
|
+
if (escape) return (this.xFocus = "list"), true;
|
|
1916
|
+
if (!session) return false;
|
|
1917
|
+
if (enter || (data === " " && this.xFocus === "detail")) {
|
|
1918
|
+
if (this.xFocus === "list") this.xFocus = "detail";
|
|
1919
|
+
else this.toggleExcalidrawAgent(session);
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
if (data === "*") {
|
|
1923
|
+
this.say(this.host.excalidraw.toggleAll(session.id));
|
|
1924
|
+
this.data.excalidraw = this.host.excalidraw.list();
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
if (data === "w") {
|
|
1928
|
+
this.say(this.host.excalidraw.toggleContribute(session.id));
|
|
1929
|
+
this.data.excalidraw = this.host.excalidraw.list();
|
|
1930
|
+
return true;
|
|
1931
|
+
}
|
|
1932
|
+
if (data === "r") {
|
|
1933
|
+
this.xDraft = "rename";
|
|
1934
|
+
this.editor.setText(session.name);
|
|
1935
|
+
this.setMode("type");
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
if (data === "t") {
|
|
1939
|
+
if (this.xChecking.has(session.id)) return true;
|
|
1940
|
+
this.xChecking.add(session.id);
|
|
1941
|
+
this.xChecks.delete(session.id);
|
|
1942
|
+
void this.host.checkExcalidraw(session).then((result) => {
|
|
1943
|
+
this.xChecking.delete(session.id);
|
|
1944
|
+
this.xChecks.set(session.id, result);
|
|
1945
|
+
this.host.requestRender();
|
|
1946
|
+
});
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
if (data === "d") {
|
|
1950
|
+
const key = `xdelete:${session.id}`;
|
|
1951
|
+
if (this.armed !== key) {
|
|
1952
|
+
this.armed = key;
|
|
1953
|
+
this.say(`press d again to remove “${session.name}” (agents lose it at once; the room itself is untouched)`, "warning");
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
this.armed = undefined;
|
|
1957
|
+
this.say(this.host.excalidraw.remove(session.id));
|
|
1958
|
+
this.xChecks.delete(session.id);
|
|
1959
|
+
this.data.excalidraw = this.host.excalidraw.list();
|
|
1960
|
+
this.xSelected = Math.max(0, Math.min(this.xSelected, this.data.excalidraw.length - 1));
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/** The prompt's text on the Excalidraw tab: a link to add (a name may follow it), the name of a new room, or a new name. */
|
|
1967
|
+
private submitExcalidraw(body: string): void {
|
|
1968
|
+
const draft = this.xDraft;
|
|
1969
|
+
if (!draft) {
|
|
1970
|
+
this.editor.setText(body);
|
|
1971
|
+
return this.say("press a to add a session from a link, n for a new room", "warning");
|
|
1972
|
+
}
|
|
1973
|
+
const book = this.host.excalidraw;
|
|
1974
|
+
if (draft === "rename") {
|
|
1975
|
+
const session = this.data.excalidraw[this.xSelected];
|
|
1976
|
+
this.xDraft = undefined;
|
|
1977
|
+
this.setMode("browse");
|
|
1978
|
+
if (session) this.say(book.rename(session.id, body));
|
|
1979
|
+
this.data.excalidraw = book.list();
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
// A link, then optionally a name for it; or the link's name first.
|
|
1983
|
+
const parts = body.split(/\s+/);
|
|
1984
|
+
const linkAt = parts.findIndex((part) => part.includes("#room=") || /^[\w-]+,[\w-]{22}$/.test(part));
|
|
1985
|
+
const added = draft === "add" ? (linkAt < 0 ? { notice: "that is not an Excalidraw room link — it looks like https://excalidraw.com/#room=<id>,<key>" } : book.add(parts[linkAt]!, parts.filter((_, index) => index !== linkAt).join(" "))) : book.create(body);
|
|
1986
|
+
if (!added.session) {
|
|
1987
|
+
// Keep what was typed, so a slip in the link can be fixed.
|
|
1988
|
+
this.editor.setText(body);
|
|
1989
|
+
return this.say(added.notice, "warning");
|
|
1990
|
+
}
|
|
1991
|
+
this.xDraft = undefined;
|
|
1992
|
+
this.setMode("browse");
|
|
1993
|
+
this.data.excalidraw = book.list();
|
|
1994
|
+
this.xSelected = Math.max(0, this.data.excalidraw.findIndex((session) => session.id === added.session!.id));
|
|
1995
|
+
this.xFocus = "detail";
|
|
1996
|
+
this.xAgent = 0;
|
|
1997
|
+
this.say(added.notice);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1856
2000
|
/** The prompt's text on the Knowledge tab: an edit of the picked entry, a note on it, or a new entry after it. */
|
|
1857
2001
|
private submitKnowledge(body: string): void {
|
|
1858
2002
|
const draft = this.kDraft;
|
|
@@ -1900,7 +2044,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1900
2044
|
/** The wheel scrolls the pane under the pointer (the focused one when it is over none). */
|
|
1901
2045
|
private wheel(direction: number, x = -1, y = -1): void {
|
|
1902
2046
|
if (this.help) return;
|
|
1903
|
-
if (this.tab === "issues" || this.tab === "git" || this.tab === "knowledge") return this.scroll(direction * WHEEL_LINES);
|
|
2047
|
+
if (this.tab === "issues" || this.tab === "git" || this.tab === "knowledge" || this.tab === "excalidraw") return this.scroll(direction * WHEEL_LINES);
|
|
1904
2048
|
this.scrollPane(this.paneAt(x, y) ?? this.focusedPane(), direction * WHEEL_LINES, true);
|
|
1905
2049
|
}
|
|
1906
2050
|
|
|
@@ -1957,6 +2101,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
1957
2101
|
const body = text.trim();
|
|
1958
2102
|
if (!body) {
|
|
1959
2103
|
if (this.tab === "knowledge" && this.kDraft) return this.say(this.kDraft.kind === "edit" ? "nothing to save — d d deletes an entry, esc cancels" : "write something first, or esc to cancel", "warning");
|
|
2104
|
+
// A new room's name is optional: an empty enter makes it with a numbered one.
|
|
2105
|
+
if (this.tab === "excalidraw" && this.xDraft === "new") return this.submitExcalidraw("");
|
|
2106
|
+
if (this.tab === "excalidraw" && this.xDraft) return this.say(this.xDraft === "add" ? "paste the session's link first, or esc to cancel" : "write a name first, or esc to cancel", "warning");
|
|
1960
2107
|
// An empty enter on the Plan tab opens the panel's questions; on the Lobby, a background session's question.
|
|
1961
2108
|
if (this.tab === "plan" && !this.lineTarget && this.host.planner()?.awaitingAnswers) this.answerQuestions();
|
|
1962
2109
|
if (this.tab === "lobby" && !this.newSession) this.answerSessionDialog();
|
|
@@ -1998,6 +2145,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1998
2145
|
return;
|
|
1999
2146
|
case "knowledge":
|
|
2000
2147
|
return this.submitKnowledge(body);
|
|
2148
|
+
case "excalidraw":
|
|
2149
|
+
return this.submitExcalidraw(body);
|
|
2001
2150
|
case "git":
|
|
2002
2151
|
if (!this.gitFocusDraft) {
|
|
2003
2152
|
this.editor.setText(body);
|
|
@@ -2097,6 +2246,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2097
2246
|
if (this.issuesOn && this.host.issues.issues.length > 0) badges.issues = String(this.host.issues.issues.length);
|
|
2098
2247
|
if (this.host.reviews.busy) badges.git = spinner(this.tick);
|
|
2099
2248
|
else if (this.host.pulls.pulls.length > 0) badges.git = String(this.host.pulls.pulls.length);
|
|
2249
|
+
if (this.data.excalidraw.length > 0) badges.excalidraw = String(this.data.excalidraw.length);
|
|
2100
2250
|
const cells = this.tabs().map((tab, index) => {
|
|
2101
2251
|
const number = paint(theme, tab === this.tab ? "accent" : "dim", String(index + 1));
|
|
2102
2252
|
const name = tab === this.tab ? bold(theme, paint(theme, "text", TAB_LABELS[tab])) : paint(theme, "muted", TAB_LABELS[tab]);
|
|
@@ -2213,6 +2363,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
2213
2363
|
if (this.kDraft?.kind === "add") return picked ? `a new entry after “${clip(picked.text, 40)}” · enter adds it` : "a new entry · enter adds it";
|
|
2214
2364
|
return "e edits the picked entry, c comments on it, n adds one";
|
|
2215
2365
|
}
|
|
2366
|
+
case "excalidraw": {
|
|
2367
|
+
if (this.xDraft === "add") return "paste the session's link (a name may follow it) · enter adds it · esc cancels";
|
|
2368
|
+
if (this.xDraft === "new") return "a name for the new room (optional) · enter makes it · esc cancels";
|
|
2369
|
+
if (this.xDraft === "rename") return "a new name for the session · enter saves it · esc cancels";
|
|
2370
|
+
return "a adds a session from a link, n makes a new room";
|
|
2371
|
+
}
|
|
2216
2372
|
case "git": {
|
|
2217
2373
|
const pull = this.selectedPull();
|
|
2218
2374
|
if (this.gitFocusDraft) return `what should the review of #${pull?.number ?? "?"} look at first? · enter reviews`;
|
|
@@ -2317,6 +2473,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
2317
2473
|
{ key: "E", text: "edit the whole file in pi's editor" },
|
|
2318
2474
|
{ key: "r", text: "reread the files from disk" },
|
|
2319
2475
|
];
|
|
2476
|
+
case "excalidraw":
|
|
2477
|
+
return [
|
|
2478
|
+
{ key: "↑ ↓", text: "pick a session, or (in the checklist) an agent" },
|
|
2479
|
+
{ key: "enter / ← →", text: "move between the sessions and the agents; in the checklist, enter or space assigns the picked agent" },
|
|
2480
|
+
{ key: "a", text: "add a session: paste the link from Excalidraw (Share → Live collaboration → Start session)" },
|
|
2481
|
+
{ key: "n", text: "make a new room, whose link you open in Excalidraw" },
|
|
2482
|
+
{ key: "*", text: "assign the session to every agent, or (when they all have it) to none" },
|
|
2483
|
+
{ key: "w", text: "let agents draw in the session, or only look at it" },
|
|
2484
|
+
{ key: "t", text: "check the session: join the room for a moment and report who is there and what is on the board" },
|
|
2485
|
+
{ key: "r", text: "rename the session" },
|
|
2486
|
+
{ key: "d d", text: "remove the session (agents lose it at once)" },
|
|
2487
|
+
];
|
|
2320
2488
|
case "git":
|
|
2321
2489
|
return [
|
|
2322
2490
|
{ key: "↑ ↓", text: "select a pull request, or scroll its detail (PageUp/PageDown)" },
|
|
@@ -2378,6 +2546,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2378
2546
|
if (this.tab === "lobby" && this.host.masterBusy()) return [["enter", "steer"], ["esc", "stop the oracle"], [k("hide"), "hide"]];
|
|
2379
2547
|
if (this.tab === "plan" && this.lineTarget) return [["enter", "add the comment"], ["esc", "cancel"]];
|
|
2380
2548
|
if (this.tab === "knowledge" && this.kDraft) return [["enter", this.kDraft.kind === "edit" ? "save" : this.kDraft.kind === "comment" ? "save the note" : "add it"], ["shift+enter", "new line"], ["esc", "cancel"]];
|
|
2549
|
+
if (this.tab === "excalidraw" && this.xDraft) return [["enter", this.xDraft === "add" ? "add it" : this.xDraft === "new" ? "make it" : "save"], ["esc", "cancel"]];
|
|
2381
2550
|
if (this.tab === "git" && this.gitFocusDraft) return [["enter", "review with this focus"], ["shift+enter", "new line"], ["esc", "cancel"]];
|
|
2382
2551
|
const enter = this.tab === "plan" && session?.awaitingAnswers && !this.editor.getText().trim() ? "answer questions" : this.tab === "quickfix" ? "run it" : "send";
|
|
2383
2552
|
return [["enter", enter], ["esc", "browse"], [k("hide"), "hide"]];
|
|
@@ -2419,6 +2588,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
2419
2588
|
if (this.kFocus === "detail" || this.pickedEntry()) keys.push(["e", "edit"], ["c", "comment"], ["n", "new"], ["d d", "delete"]);
|
|
2420
2589
|
keys.push(["E", "edit file"]);
|
|
2421
2590
|
break;
|
|
2591
|
+
case "excalidraw": {
|
|
2592
|
+
const session = this.data.excalidraw[this.xSelected];
|
|
2593
|
+
keys.push(["a", "add a link"], ["n", "new room"]);
|
|
2594
|
+
if (session) keys.push(["↑↓", this.xFocus === "detail" ? "agent" : "session"], ["enter", this.xFocus === "detail" ? "assign" : "agents"], ["t", "check"], ["w", session.contribute ? "look only" : "let agents draw"], ["d d", "remove"]);
|
|
2595
|
+
break;
|
|
2596
|
+
}
|
|
2422
2597
|
case "git": {
|
|
2423
2598
|
const pull = this.selectedPull();
|
|
2424
2599
|
keys.push(["↑↓", "select"]);
|
|
@@ -2529,6 +2704,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
2529
2704
|
this.kOffset = this.kLayout.offset;
|
|
2530
2705
|
return lines;
|
|
2531
2706
|
}
|
|
2707
|
+
case "excalidraw": {
|
|
2708
|
+
this.xSelected = Math.min(this.xSelected, Math.max(0, this.data.excalidraw.length - 1));
|
|
2709
|
+
return renderExcalidraw({ sessions: this.data.excalidraw, selected: this.xSelected, focus: this.xFocus, agent: this.xAgent, checks: this.xChecks, checking: this.xChecking, tick: this.tick }, width, height, theme);
|
|
2710
|
+
}
|
|
2532
2711
|
case "git": {
|
|
2533
2712
|
const pulls = this.host.pulls;
|
|
2534
2713
|
this.gitSelected = Math.min(this.gitSelected, Math.max(0, pulls.pulls.length - 1));
|