@a-t-h-i/bot-lobby 0.5.1 → 0.6.0
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 +145 -44
- package/package.json +1 -1
- package/prompts/panel.md +6 -2
- package/prompts/planner.md +29 -16
- package/src/execution/pi-runner.ts +1 -1
- package/src/index.ts +4 -2
- package/src/lobby/feed.ts +65 -9
- package/src/lobby/keys.ts +5 -1
- package/src/lobby/layout.ts +17 -2
- package/src/lobby/planner.ts +16 -5
- package/src/lobby/runtime.ts +195 -59
- package/src/lobby/session-files.ts +126 -0
- package/src/lobby/sessions.ts +288 -0
- package/src/lobby/tabs/home.ts +117 -29
- package/src/lobby/tabs/plan.ts +25 -30
- package/src/lobby/tabs/quickfix.ts +5 -8
- package/src/lobby/tabs/tasks.ts +177 -44
- package/src/lobby/view.ts +491 -86
- package/src/pi/commands.ts +34 -6
- package/src/pi/events.ts +5 -2
- package/src/pi/notify.ts +18 -0
- package/src/pi/owner.ts +216 -0
- package/src/pi/settings-ui.ts +6 -2
- package/src/pi/start-task.ts +33 -2
- package/src/pi/ui.ts +3 -3
- package/src/pi/zen-large.ts +9 -1
- package/src/pi/zen.ts +7 -3
- package/src/schemas/configuration.ts +8 -3
- package/src/schemas/task.ts +2 -0
- package/src/state/auto.ts +40 -0
- package/src/state/inbox.ts +90 -0
- package/src/workflow/workflow.ts +27 -3
package/src/lobby/view.ts
CHANGED
|
@@ -16,12 +16,13 @@ import type { PlannedTask } from "../state/backlog.ts";
|
|
|
16
16
|
import type { PlanComment } from "../state/comments.ts";
|
|
17
17
|
import { aggregateMetrics, collectMetrics, SORT_KEYS, sortGroups, taskStats, taskTimesByModel, type GroupBy, type MetricRecord, type SortKey } from "../state/metrics.ts";
|
|
18
18
|
import { PANEL_MEMBERS, type LobbyAgentKind, type LobbyPanel, type PanelMember } from "../schemas/configuration.ts";
|
|
19
|
-
import type { LobbyFeed } from "./feed.ts";
|
|
19
|
+
import type { ChatEntry, LobbyFeed } from "./feed.ts";
|
|
20
|
+
import type { BackgroundSession } from "./sessions.ts";
|
|
20
21
|
import type { QuickFixQueue } from "./quickfix.ts";
|
|
21
22
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
22
23
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
23
24
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
24
|
-
import { beside, bold, box, fit, highlight, paint, spinner, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
25
|
+
import { beside, bold, box, fit, highlight, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
25
26
|
import { HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
26
27
|
import { filterRows, planDetailLines, renderTasks, taskDetailLines, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
27
28
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
@@ -52,8 +53,24 @@ const PROMPT_FIRST: ReadonlySet<TabId> = new Set(["lobby", "plan", "quickfix"]);
|
|
|
52
53
|
export type LobbyMode = "type" | "browse";
|
|
53
54
|
|
|
54
55
|
/** What each pane toggle is called in notices and help. */
|
|
55
|
-
const PANEL_NAMES: Record<LobbyPanel, string> = {
|
|
56
|
-
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = {
|
|
56
|
+
const PANEL_NAMES: Record<LobbyPanel, string> = { animations: "oracle and agent animations", conversation: "conversation", activity: "activity log", thinking: "thinking" };
|
|
57
|
+
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = { animations: "toggleScene", conversation: "toggleConversation", activity: "toggleActivity", thinking: "toggleThinking" };
|
|
58
|
+
|
|
59
|
+
/** Which session the Lobby tab shows and talks to: this window, one it started in the background, or one in another terminal. */
|
|
60
|
+
export type SessionView = { kind: "here" } | { kind: "background"; key: string } | { kind: "other"; taskId: string };
|
|
61
|
+
|
|
62
|
+
/** One row of the session switcher. */
|
|
63
|
+
export interface SessionEntry {
|
|
64
|
+
view: SessionView;
|
|
65
|
+
name: string;
|
|
66
|
+
where: "this window" | "background" | "other terminal";
|
|
67
|
+
task?: Task;
|
|
68
|
+
/** What it is doing: working, idle, starting, ended, or its task's state. */
|
|
69
|
+
status: string;
|
|
70
|
+
auto: boolean;
|
|
71
|
+
/** Questions it waits on you for. */
|
|
72
|
+
waiting: number;
|
|
73
|
+
}
|
|
57
74
|
|
|
58
75
|
/** Everything the view needs from pi and bot-lobby. */
|
|
59
76
|
export interface LobbyHost {
|
|
@@ -62,8 +79,8 @@ export interface LobbyHost {
|
|
|
62
79
|
sessionId(): string | undefined;
|
|
63
80
|
/** This session's active task and its runs, as the zen widget sees them. */
|
|
64
81
|
zen(): { task?: Task; runs: readonly AgentRun[] };
|
|
65
|
-
/** Draw the zen scene into at most `height` lines. */
|
|
66
|
-
scene(width: number, height: number): string[];
|
|
82
|
+
/** Draw the zen scene into at most `height` lines: animated, or only the task's status when `animated` is false. */
|
|
83
|
+
scene(width: number, height: number, animated: boolean): string[];
|
|
67
84
|
/** Advance the scene's clock; returns the delay it wants until the next step. */
|
|
68
85
|
advanceScene(now: number): number;
|
|
69
86
|
feed: LobbyFeed;
|
|
@@ -99,6 +116,23 @@ export interface LobbyHost {
|
|
|
99
116
|
keys(): Readonly<Record<string, string>>;
|
|
100
117
|
/** Open bot-lobby's settings (or one agent's entry); the lobby steps aside and rereads the config after. */
|
|
101
118
|
openSettings(entry?: "quickfix" | "planner"): Promise<void>;
|
|
119
|
+
/** This window's pi session name, when it has one. */
|
|
120
|
+
sessionName(): string | undefined;
|
|
121
|
+
/** Background sessions this window started, oldest first. */
|
|
122
|
+
sessions(): readonly BackgroundSession[];
|
|
123
|
+
/** Start a task in a new background session, named after the task; the session, or why not. */
|
|
124
|
+
startSession(start: { request?: string; plan?: PlannedTask; auto?: boolean }): BackgroundSession | string;
|
|
125
|
+
/** Put the oldest question a background session waits on to the user in this window. */
|
|
126
|
+
answerDialog(session: BackgroundSession): Promise<void>;
|
|
127
|
+
/** Auto mode for any task, whichever session drives it. */
|
|
128
|
+
isAuto(taskId: string): boolean;
|
|
129
|
+
setAuto(taskId: string, on: boolean): void;
|
|
130
|
+
/** Leave a message for the oracle of a task another terminal's session drives; returns a notice. */
|
|
131
|
+
sendToTask(taskId: string, text: string): string;
|
|
132
|
+
/** Another session's conversation, read from its saved session file. */
|
|
133
|
+
sessionChat(sessionId: string): ChatEntry[];
|
|
134
|
+
/** A task's status without animations, for a session other than this window's. */
|
|
135
|
+
taskScene(task: Task, width: number, height: number): string[];
|
|
102
136
|
profileLabel(kind: LobbyAgentKind): string;
|
|
103
137
|
requestRender(): void;
|
|
104
138
|
now?(): number;
|
|
@@ -137,11 +171,10 @@ interface Notice {
|
|
|
137
171
|
kind: "info" | "warning";
|
|
138
172
|
}
|
|
139
173
|
|
|
140
|
-
/** A key and what it does, for the help screen
|
|
174
|
+
/** A key and what it does, for the help screen. */
|
|
141
175
|
interface KeyHelp {
|
|
142
176
|
key: string;
|
|
143
177
|
text: string;
|
|
144
|
-
hint?: string;
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
function isPrintable(data: string): string | undefined {
|
|
@@ -198,6 +231,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
198
231
|
private issuesOn: boolean;
|
|
199
232
|
/** The help screen covers the body. */
|
|
200
233
|
help = false;
|
|
234
|
+
/** The session switcher covers the body. */
|
|
235
|
+
picking = false;
|
|
236
|
+
private pickIndex = 0;
|
|
237
|
+
/** The session the Lobby tab shows and talks to. */
|
|
238
|
+
viewing: SessionView = { kind: "here" };
|
|
239
|
+
/** The Lobby prompt starts a task in a new session instead of talking to an oracle. */
|
|
240
|
+
newSession = false;
|
|
201
241
|
/** The search bar has the keys. */
|
|
202
242
|
searching = false;
|
|
203
243
|
/** Each tab's search, kept while you move between tabs. */
|
|
@@ -304,6 +344,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
304
344
|
return this.host.masterBusy()
|
|
305
345
|
|| Boolean(this.host.quickfix.running)
|
|
306
346
|
|| Boolean(this.host.planner()?.busy)
|
|
347
|
+
|| this.host.sessions().some((session) => session.busy)
|
|
307
348
|
|| (this.issuesOn && this.host.issues.loading)
|
|
308
349
|
|| zen.runs.some((run) => run.status === "running")
|
|
309
350
|
|| Boolean(zen.task && !zen.task.paused && !TERMINAL_STATES.includes(zen.task.state));
|
|
@@ -315,7 +356,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
315
356
|
const now = this.now();
|
|
316
357
|
this.tick += 1;
|
|
317
358
|
let delay = this.isLive() ? LIVE_MS : IDLE_MS;
|
|
318
|
-
if (this.tab === "lobby" && this.panels.
|
|
359
|
+
if (this.tab === "lobby" && this.panels.animations && this.host.zen().task) delay = Math.min(delay, this.host.advanceScene(now));
|
|
319
360
|
this.refreshData(false);
|
|
320
361
|
this.host.requestRender();
|
|
321
362
|
this.schedule(delay);
|
|
@@ -364,6 +405,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
364
405
|
this.issueDraft = false;
|
|
365
406
|
this.armed = undefined;
|
|
366
407
|
this.help = false;
|
|
408
|
+
this.picking = false;
|
|
409
|
+
this.newSession = false;
|
|
367
410
|
this.closeSearch();
|
|
368
411
|
this.setMode(PROMPT_FIRST.has(tab) ? "type" : "browse");
|
|
369
412
|
this.refreshData(true);
|
|
@@ -506,6 +549,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
506
549
|
// Esc, ?, q and the help key only close it; anything else also does its job.
|
|
507
550
|
if (action === "help" || matchesKey(data, Key.escape) || data === "?" || data === "q" || matchesKey(data, Key.enter)) return;
|
|
508
551
|
}
|
|
552
|
+
// The session switcher takes the keys while it is open (shortcuts other than its own still work).
|
|
553
|
+
if (this.picking && (!action || action === "sessions" || action === "newSession") && this.pickerKey(data, action)) return;
|
|
509
554
|
// While searching, the shortcuts still work; everything else edits the query.
|
|
510
555
|
if (this.searching) return action ? this.runAction(action) : this.searchKey(data);
|
|
511
556
|
if (action) return this.runAction(action);
|
|
@@ -530,6 +575,14 @@ export class LobbyView implements Component, Focusable {
|
|
|
530
575
|
return;
|
|
531
576
|
case "settings":
|
|
532
577
|
return this.openSettings();
|
|
578
|
+
case "savePlan":
|
|
579
|
+
return this.savePlan();
|
|
580
|
+
case "sessions":
|
|
581
|
+
return this.picking ? void (this.picking = false) : this.openPicker();
|
|
582
|
+
case "newSession":
|
|
583
|
+
return this.startNewSessionPrompt();
|
|
584
|
+
case "toggleAuto":
|
|
585
|
+
return this.toggleAuto();
|
|
533
586
|
case "search":
|
|
534
587
|
return this.searching ? this.closeSearch() : this.openSearch();
|
|
535
588
|
case "nextTab":
|
|
@@ -537,7 +590,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
537
590
|
case "prevTab":
|
|
538
591
|
return this.cycleTab(-1);
|
|
539
592
|
case "toggleScene":
|
|
540
|
-
return this.togglePanel("
|
|
593
|
+
return this.togglePanel("animations");
|
|
541
594
|
case "toggleConversation":
|
|
542
595
|
return this.togglePanel("conversation");
|
|
543
596
|
case "toggleActivity":
|
|
@@ -559,6 +612,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
559
612
|
}
|
|
560
613
|
this.commentTarget = undefined;
|
|
561
614
|
this.issueDraft = false;
|
|
615
|
+
this.newSession = false;
|
|
562
616
|
if (this.lineTarget) {
|
|
563
617
|
this.lineTarget = undefined;
|
|
564
618
|
this.planFocus = "draft";
|
|
@@ -847,19 +901,26 @@ export class LobbyView implements Component, Focusable {
|
|
|
847
901
|
const enter = matchesKey(data, Key.enter);
|
|
848
902
|
const escape = matchesKey(data, Key.escape);
|
|
849
903
|
switch (this.tab) {
|
|
850
|
-
case "lobby":
|
|
851
|
-
|
|
904
|
+
case "lobby": {
|
|
905
|
+
const background = this.viewedSession();
|
|
906
|
+
if (escape && background?.busy) {
|
|
907
|
+
background.abort();
|
|
908
|
+
this.say(`stopping ${background.name}'s oracle…`);
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
if (escape && this.viewing.kind === "here" && this.host.masterBusy()) {
|
|
852
912
|
this.host.abortMaster();
|
|
853
913
|
this.say("stopping the oracle…");
|
|
854
914
|
return true;
|
|
855
915
|
}
|
|
856
916
|
if (data === "c") {
|
|
857
|
-
const task = this.
|
|
858
|
-
if (task) this.commentOn(task.id);
|
|
917
|
+
const task = this.viewedEntry().task;
|
|
918
|
+
if (task && !TERMINAL_STATES.includes(task.state)) this.commentOn(task.id);
|
|
859
919
|
else this.say("no task in this session to comment on", "warning");
|
|
860
920
|
return true;
|
|
861
921
|
}
|
|
862
922
|
return enter ? (this.setMode("type"), true) : false;
|
|
923
|
+
}
|
|
863
924
|
case "tasks":
|
|
864
925
|
return this.tasksCommand(data, enter, escape);
|
|
865
926
|
case "plan":
|
|
@@ -889,6 +950,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
889
950
|
if (enter) return (this.tasksFocus = this.tasksFocus === "list" ? "detail" : "list"), true;
|
|
890
951
|
if (escape) return (this.tasksFocus = "list"), true;
|
|
891
952
|
if (data === "r") return this.refreshData(true), true;
|
|
953
|
+
if (data === "n") return this.startNewSessionPrompt(), true;
|
|
892
954
|
if (!row) return false;
|
|
893
955
|
if (data === "c") {
|
|
894
956
|
const task = this.data.tasks.find((entry) => entry.id === row.id);
|
|
@@ -901,11 +963,39 @@ export class LobbyView implements Component, Focusable {
|
|
|
901
963
|
return true;
|
|
902
964
|
}
|
|
903
965
|
const plan = row.kind === "plan" ? this.data.plans.find((entry) => entry.id === row.id) : undefined;
|
|
966
|
+
// A planned task starts in a new session of its own (s), or in this window (h).
|
|
904
967
|
if (data === "s" && plan) {
|
|
968
|
+
this.startInNewSession({ plan });
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
if (data === "h" && plan) {
|
|
905
972
|
this.say(this.host.startPlanned(plan));
|
|
906
973
|
this.refreshData(true);
|
|
907
974
|
return true;
|
|
908
975
|
}
|
|
976
|
+
const task = row.kind === "task" ? this.data.tasks.find((entry) => entry.id === row.id) : undefined;
|
|
977
|
+
if (data === "o" && task) {
|
|
978
|
+
const target = this.sessionEntries().find((entry) => entry.task?.id === task.id);
|
|
979
|
+
if (!target) this.say(`${task.id} is not running in any session now`, "warning");
|
|
980
|
+
else {
|
|
981
|
+
this.view(target.view);
|
|
982
|
+
this.say(target.view.kind === "here" ? "this window's task" : `showing ${target.name}`);
|
|
983
|
+
}
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
if (data === "x" && task) {
|
|
987
|
+
const session = this.host.sessions().find((entry) => entry.alive && entry.sessionId === task.ownerSessionId);
|
|
988
|
+
if (!session) return this.say("only a background session this window started can be stopped from here", "warning"), true;
|
|
989
|
+
if (this.armed !== `stop:${session.key}`) {
|
|
990
|
+
this.armed = `stop:${session.key}`;
|
|
991
|
+
this.say(`press x again to stop ${session.name} (its task keeps its state)`, "warning");
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
this.armed = undefined;
|
|
995
|
+
session.stop();
|
|
996
|
+
this.say(`stopping ${session.name}`);
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
909
999
|
if (data === "d" && plan) {
|
|
910
1000
|
if (this.armed !== `discard:${plan.id}`) {
|
|
911
1001
|
this.armed = `discard:${plan.id}`;
|
|
@@ -935,6 +1025,182 @@ export class LobbyView implements Component, Focusable {
|
|
|
935
1025
|
this.planCursor = 0;
|
|
936
1026
|
}
|
|
937
1027
|
|
|
1028
|
+
/** The Lobby prompt: a new task in its own session, or a message to the session in view. */
|
|
1029
|
+
private submitLobby(body: string): void {
|
|
1030
|
+
this.homeOffsets.conversation = 0;
|
|
1031
|
+
if (this.newSession) {
|
|
1032
|
+
this.newSession = false;
|
|
1033
|
+
return this.startInNewSession({ request: body });
|
|
1034
|
+
}
|
|
1035
|
+
const view = this.viewing;
|
|
1036
|
+
if (view.kind === "background") {
|
|
1037
|
+
const session = this.viewedSession();
|
|
1038
|
+
if (!session?.alive) {
|
|
1039
|
+
this.editor.setText(body);
|
|
1040
|
+
return this.say(`${session?.name ?? "that session"} has ended — alt+n starts a new one`, "warning");
|
|
1041
|
+
}
|
|
1042
|
+
session.send(body);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (view.kind === "other") return this.say(this.host.sendToTask(view.taskId, body));
|
|
1046
|
+
const notice = this.host.toOracle(body);
|
|
1047
|
+
if (notice) this.say(notice);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** Put the question the background session in view waits on to the user. */
|
|
1051
|
+
private answerSessionDialog(): void {
|
|
1052
|
+
const session = this.viewedSession();
|
|
1053
|
+
if (!session?.dialogs.length) return;
|
|
1054
|
+
void this.host.answerDialog(session).then(() => this.host.requestRender());
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/* ------------------------------------------------------------ sessions */
|
|
1058
|
+
|
|
1059
|
+
/** Every session the lobby can show: this window, the background sessions it started, then tasks other terminals drive. */
|
|
1060
|
+
sessionEntries(): SessionEntry[] {
|
|
1061
|
+
const me = this.host.sessionId();
|
|
1062
|
+
const zen = this.host.zen();
|
|
1063
|
+
const background = this.host.sessions();
|
|
1064
|
+
const started = new Set(background.map((session) => session.sessionId).filter((id): id is string => Boolean(id)));
|
|
1065
|
+
const auto = (task: Task | undefined) => Boolean(task && !TERMINAL_STATES.includes(task.state) && this.host.isAuto(task.id));
|
|
1066
|
+
const entries: SessionEntry[] = [{
|
|
1067
|
+
view: { kind: "here" },
|
|
1068
|
+
name: this.host.sessionName() ?? zen.task?.title ?? "unnamed session",
|
|
1069
|
+
where: "this window",
|
|
1070
|
+
...(zen.task ? { task: zen.task } : {}),
|
|
1071
|
+
status: this.host.masterBusy() ? "working" : zen.task?.state ?? "no task",
|
|
1072
|
+
auto: auto(zen.task),
|
|
1073
|
+
waiting: 0,
|
|
1074
|
+
}];
|
|
1075
|
+
for (const session of background) {
|
|
1076
|
+
const task = session.sessionId ? this.data.tasks.find((entry) => entry.ownerSessionId === session.sessionId) : undefined;
|
|
1077
|
+
const status = session.status === "exited" ? "ended" : session.status === "starting" ? "starting" : session.busy ? "working" : task?.state ?? "idle";
|
|
1078
|
+
entries.push({ view: { kind: "background", key: session.key }, name: session.name, where: "background", ...(task ? { task } : {}), status, auto: auto(task), waiting: session.dialogs.length });
|
|
1079
|
+
}
|
|
1080
|
+
for (const task of this.data.tasks) {
|
|
1081
|
+
if (TERMINAL_STATES.includes(task.state) || !task.ownerSessionId || task.ownerSessionId === me || started.has(task.ownerSessionId)) continue;
|
|
1082
|
+
entries.push({ view: { kind: "other", taskId: task.id }, name: task.title, where: "other terminal", task, status: task.state, auto: auto(task), waiting: 0 });
|
|
1083
|
+
}
|
|
1084
|
+
return entries;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** The entry the Lobby tab shows, falling back to this window when that session is gone. */
|
|
1088
|
+
viewedEntry(): SessionEntry {
|
|
1089
|
+
const entries = this.sessionEntries();
|
|
1090
|
+
const view = this.viewing;
|
|
1091
|
+
const found = view.kind === "here" ? undefined : entries.find((entry) => (entry.view.kind === "background" && view.kind === "background" && entry.view.key === view.key) || (entry.view.kind === "other" && view.kind === "other" && entry.view.taskId === view.taskId));
|
|
1092
|
+
if (!found && view.kind !== "here") this.viewing = { kind: "here" };
|
|
1093
|
+
return found ?? entries[0]!;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private viewedSession(): BackgroundSession | undefined {
|
|
1097
|
+
const view = this.viewing;
|
|
1098
|
+
return view.kind === "background" ? this.host.sessions().find((session) => session.key === view.key) : undefined;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Show a session in the Lobby tab, and talk to it from there. */
|
|
1102
|
+
view(target: SessionView): void {
|
|
1103
|
+
this.viewing = target;
|
|
1104
|
+
this.picking = false;
|
|
1105
|
+
this.newSession = false;
|
|
1106
|
+
for (const pane of HOME_PANES) this.homeOffsets[pane] = 0;
|
|
1107
|
+
if (this.tab !== "lobby") this.setTab("lobby");
|
|
1108
|
+
this.setMode("type");
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private openPicker(): void {
|
|
1112
|
+
this.help = false;
|
|
1113
|
+
this.picking = true;
|
|
1114
|
+
const entries = this.sessionEntries();
|
|
1115
|
+
const current = this.viewedEntry();
|
|
1116
|
+
this.pickIndex = Math.max(0, entries.findIndex((entry) => entry.name === current.name && entry.where === current.where));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Keys while the session switcher is open; true when the key was handled. */
|
|
1120
|
+
private pickerKey(data: string, action: LobbyAction | undefined): boolean {
|
|
1121
|
+
const entries = this.sessionEntries();
|
|
1122
|
+
this.pickIndex = Math.min(this.pickIndex, Math.max(0, entries.length - 1));
|
|
1123
|
+
if (matchesKey(data, Key.escape) || action === "sessions") {
|
|
1124
|
+
this.picking = false;
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
if (matchesKey(data, Key.up) || data === "k") return (this.pickIndex = Math.max(0, this.pickIndex - 1)), true;
|
|
1128
|
+
if (matchesKey(data, Key.down) || data === "j") return (this.pickIndex = Math.min(entries.length - 1, this.pickIndex + 1)), true;
|
|
1129
|
+
const entry = entries[this.pickIndex];
|
|
1130
|
+
if (matchesKey(data, Key.enter) && entry) {
|
|
1131
|
+
this.view(entry.view);
|
|
1132
|
+
this.say(entry.view.kind === "here" ? "back to this window" : `showing ${entry.name} — you talk to its oracle from here`);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (data === "n" || action === "newSession") {
|
|
1136
|
+
this.picking = false;
|
|
1137
|
+
this.startNewSessionPrompt();
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
if (data === "x" && entry?.view.kind === "background") {
|
|
1141
|
+
const key = entry.view.key;
|
|
1142
|
+
if (this.armed !== `stop:${key}`) {
|
|
1143
|
+
this.armed = `stop:${key}`;
|
|
1144
|
+
this.say(`press x again to stop ${entry.name} (its task keeps its state)`, "warning");
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
this.armed = undefined;
|
|
1148
|
+
this.host.sessions().find((session) => session.key === key)?.stop();
|
|
1149
|
+
this.say(`stopping ${entry.name}`);
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** Start typing a task that begins in its own new session. */
|
|
1156
|
+
startNewSessionPrompt(): void {
|
|
1157
|
+
this.picking = false;
|
|
1158
|
+
this.help = false;
|
|
1159
|
+
if (this.tab !== "lobby") this.setTab("lobby");
|
|
1160
|
+
this.newSession = true;
|
|
1161
|
+
this.setMode("type");
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/** Start a task in a new background session and show it. */
|
|
1165
|
+
startInNewSession(start: { request?: string; plan?: PlannedTask }): void {
|
|
1166
|
+
const session = this.host.startSession(start);
|
|
1167
|
+
if (typeof session === "string") return this.say(session, "warning");
|
|
1168
|
+
this.view({ kind: "background", key: session.key });
|
|
1169
|
+
this.say(`started ${session.name} in a new session — alt+o switches back`);
|
|
1170
|
+
this.refreshData(true);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** The task auto mode applies to: the selected row on the Tasks tab, the viewed session's task elsewhere. */
|
|
1174
|
+
private autoTarget(): Task | undefined {
|
|
1175
|
+
if (this.tab === "tasks") {
|
|
1176
|
+
const row = this.taskRowList()[this.tasksSelected];
|
|
1177
|
+
return row?.kind === "task" ? this.data.tasks.find((task) => task.id === row.id) : undefined;
|
|
1178
|
+
}
|
|
1179
|
+
return this.viewedEntry().task;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Switch auto mode for the task in view. */
|
|
1183
|
+
toggleAuto(): void {
|
|
1184
|
+
const task = this.autoTarget();
|
|
1185
|
+
if (!task || TERMINAL_STATES.includes(task.state)) return this.say("no active task here — auto mode applies to a task", "warning");
|
|
1186
|
+
const on = !this.host.isAuto(task.id);
|
|
1187
|
+
this.host.setAuto(task.id, on);
|
|
1188
|
+
this.say(on ? `auto mode on — the oracle drives ${task.id} to completion without asking` : `auto mode off — the oracle asks you again on ${task.id}`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** Save the planning session's draft to the pending tasks, from any tab and in either mode. */
|
|
1192
|
+
savePlan(): void {
|
|
1193
|
+
const session = this.host.planner();
|
|
1194
|
+
if (!session?.reply?.plan) return this.say(session?.busy ? "the first draft is still being written" : "no plan to save yet — describe a task in the Plan tab", "warning");
|
|
1195
|
+
try {
|
|
1196
|
+
const saved = session.save();
|
|
1197
|
+
this.say(`saved ${saved.id} to the pending tasks — start it from the Tasks tab${session.reply.status === "ready" ? "" : " (the panel had not agreed yet)"}`);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
this.say((error as Error).message, "warning");
|
|
1200
|
+
}
|
|
1201
|
+
this.refreshData(true);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
938
1204
|
/** The oracle puts the round's questions to the user through the questionnaire. */
|
|
939
1205
|
answerQuestions(): void {
|
|
940
1206
|
const session = this.host.planner();
|
|
@@ -1007,16 +1273,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
1007
1273
|
else this.say("nothing to retry", "warning");
|
|
1008
1274
|
return true;
|
|
1009
1275
|
}
|
|
1010
|
-
if (data === "s") {
|
|
1011
|
-
try {
|
|
1012
|
-
const saved = session?.save();
|
|
1013
|
-
this.say(saved ? `saved ${saved.id} — start it from the Tasks tab when you are ready` : "nothing to save yet", saved ? "info" : "warning");
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
this.say((error as Error).message, "warning");
|
|
1016
|
-
}
|
|
1017
|
-
this.refreshData(true);
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
1020
1276
|
if (data === "n") {
|
|
1021
1277
|
const unsaved = session && session.messages.length > 0 && !session.saved;
|
|
1022
1278
|
if (unsaved && this.armed !== "new-plan") {
|
|
@@ -1135,18 +1391,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
1135
1391
|
private submit(text: string): void {
|
|
1136
1392
|
const body = text.trim();
|
|
1137
1393
|
if (!body) {
|
|
1138
|
-
// An empty enter on the Plan tab opens the panel's questions.
|
|
1394
|
+
// An empty enter on the Plan tab opens the panel's questions; on the Lobby, a background session's question.
|
|
1139
1395
|
if (this.tab === "plan" && !this.lineTarget && this.host.planner()?.awaitingAnswers) this.answerQuestions();
|
|
1396
|
+
if (this.tab === "lobby" && !this.newSession) this.answerSessionDialog();
|
|
1140
1397
|
return;
|
|
1141
1398
|
}
|
|
1142
1399
|
this.editor.addToHistory(body);
|
|
1143
1400
|
switch (this.tab) {
|
|
1144
|
-
case "lobby":
|
|
1145
|
-
|
|
1146
|
-
if (notice) this.say(notice);
|
|
1147
|
-
this.homeOffsets.conversation = 0;
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1401
|
+
case "lobby":
|
|
1402
|
+
return this.submitLobby(body);
|
|
1150
1403
|
case "tasks":
|
|
1151
1404
|
if (!this.commentTarget) {
|
|
1152
1405
|
this.editor.setText(body);
|
|
@@ -1210,7 +1463,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
1210
1463
|
|
|
1211
1464
|
/** Task rows, narrowed by the Tasks tab's search. */
|
|
1212
1465
|
private taskRowList(): TaskRow[] {
|
|
1213
|
-
const
|
|
1466
|
+
const names = new Map<string, string>();
|
|
1467
|
+
for (const session of this.host.sessions()) if (session.sessionId && session.alive) names.set(session.sessionId, session.name);
|
|
1468
|
+
const auto = new Set(this.data.tasks.filter((task) => !TERMINAL_STATES.includes(task.state) && this.host.isAuto(task.id)).map((task) => task.id));
|
|
1469
|
+
const rows = taskRows(this.data.tasks, this.data.plans, this.host.sessionId(), this.now(), { names, auto });
|
|
1214
1470
|
return filterRows(rows, this.data.tasks, this.data.plans, this.queries.tasks);
|
|
1215
1471
|
}
|
|
1216
1472
|
|
|
@@ -1244,13 +1500,26 @@ export class LobbyView implements Component, Focusable {
|
|
|
1244
1500
|
}
|
|
1245
1501
|
this.tabSpans = spans;
|
|
1246
1502
|
const room = width - visibleWidth(left) - 1;
|
|
1247
|
-
const
|
|
1248
|
-
const state = zen.task ? paint(theme, "muted", zen.task.paused ? `${zen.task.state} (paused)` : zen.task.state) : "";
|
|
1503
|
+
const entry = this.viewedEntry();
|
|
1249
1504
|
const help = paint(theme, "dim", `${keyLabel(this.keys.help)} keys `);
|
|
1250
|
-
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1505
|
+
const auto = entry.auto ? ` ${bold(theme, paint(theme, "success", "⟳ AUTO"))}` : "";
|
|
1506
|
+
const waiting = this.host.sessions().reduce((count, session) => count + session.dialogs.length, 0);
|
|
1507
|
+
const asking = waiting > 0 ? ` ${paint(theme, "warning", `● ${waiting} waiting · ${keyLabel(this.keys.sessions)}`)}` : "";
|
|
1508
|
+
let choices: string[];
|
|
1509
|
+
if (entry.view.kind !== "here") {
|
|
1510
|
+
// Another session in view: its name and what it is doing lead, so it is never mistaken for this window.
|
|
1511
|
+
const working = entry.status === "working";
|
|
1512
|
+
const dot = paint(theme, working ? "accent" : "dim", working ? spinner(this.tick) : "◆");
|
|
1513
|
+
const where = paint(theme, "muted", `${entry.name} · ${entry.status}`);
|
|
1514
|
+
choices = [`${dot} ${where}${auto}${asking} ${help}`, `${dot} ${where}${auto}${asking} `, `${dot} ${where}${auto} `, `${dot}${auto} `];
|
|
1515
|
+
} else {
|
|
1516
|
+
const dot = paint(theme, this.host.masterBusy() ? "accent" : "dim", this.host.masterBusy() ? spinner(this.tick) : "●");
|
|
1517
|
+
const state = zen.task ? paint(theme, "muted", zen.task.paused ? `${zen.task.state} (paused)` : zen.task.state) : "";
|
|
1518
|
+
// Long task ids give way to the state, then to the dot alone.
|
|
1519
|
+
choices = zen.task
|
|
1520
|
+
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
1521
|
+
: [`${paint(theme, "dim", "no task in this session")}${asking} ${help}`, `${paint(theme, "dim", "no task in this session")}${asking} `, help, ""];
|
|
1522
|
+
}
|
|
1254
1523
|
const status = choices.find((choice) => visibleWidth(choice) <= room) ?? "";
|
|
1255
1524
|
const gap = width - visibleWidth(left) - visibleWidth(status);
|
|
1256
1525
|
return gap >= 1 ? `${left}${" ".repeat(gap)}${status}` : fit(left, width);
|
|
@@ -1260,23 +1529,33 @@ export class LobbyView implements Component, Focusable {
|
|
|
1260
1529
|
const zen = this.host.zen();
|
|
1261
1530
|
const browsing = this.mode === "browse";
|
|
1262
1531
|
switch (this.tab) {
|
|
1263
|
-
case "lobby":
|
|
1532
|
+
case "lobby": {
|
|
1533
|
+
if (this.newSession) return "new task — it starts in its own session, named after it";
|
|
1534
|
+
if (this.viewing.kind !== "here") {
|
|
1535
|
+
const entry = this.viewedEntry();
|
|
1536
|
+
const session = this.viewedSession();
|
|
1537
|
+
if (session && !session.alive) return `${entry.name} has ended`;
|
|
1538
|
+
if (session?.dialogs.length) return `press enter to answer ${entry.name}'s question, or type a message`;
|
|
1539
|
+
return entry.where === "other terminal" ? `message ${entry.name}'s oracle (its own session delivers it)` : `message ${entry.name}'s oracle`;
|
|
1540
|
+
}
|
|
1264
1541
|
if (!zen.task) return browsing ? "describe a task to start — i to type" : "describe a task to start · enter starts it";
|
|
1265
1542
|
return browsing ? "message the oracle — i to type" : this.host.masterBusy() ? "message the oracle · enter steers the running turn" : "message the oracle";
|
|
1543
|
+
}
|
|
1266
1544
|
case "tasks":
|
|
1267
1545
|
return this.commentTarget ? `comment on ${this.commentTarget}'s plan · enter sends it to the oracle` : "c comments on the selected task's plan";
|
|
1268
1546
|
case "plan": {
|
|
1269
|
-
if (this.lineTarget) return `comment on “${clip(this.lineTarget, 48)}
|
|
1547
|
+
if (this.lineTarget) return `comment on “${clip(this.lineTarget, 48)}”`;
|
|
1270
1548
|
const session = this.host.planner();
|
|
1271
|
-
if (session?.busy) return "the panel is thinking
|
|
1549
|
+
if (session?.busy) return "the panel is thinking";
|
|
1272
1550
|
if (session?.awaitingAnswers) {
|
|
1273
1551
|
const count = session.questions.length;
|
|
1274
|
-
return `enter
|
|
1552
|
+
return `press enter to answer ${count} question${count === 1 ? "" : "s"}, or type a reply`;
|
|
1275
1553
|
}
|
|
1276
|
-
|
|
1554
|
+
if (session?.reply?.status === "ready") return `the plan is ready — ${keyLabel(this.keys.savePlan)} saves it, or reply to refine it`;
|
|
1555
|
+
return session && session.messages.length > 0 ? "reply to the panel" : "describe the task to plan";
|
|
1277
1556
|
}
|
|
1278
1557
|
case "quickfix":
|
|
1279
|
-
return this.host.quickfix.running ? "describe a quick change
|
|
1558
|
+
return this.host.quickfix.running ? "describe a quick change; it runs after the current one" : "describe a quick change";
|
|
1280
1559
|
case "issues":
|
|
1281
1560
|
return this.issueDraft ? "new issue · first line is the title" : "n files a new issue";
|
|
1282
1561
|
case "metrics":
|
|
@@ -1310,61 +1589,64 @@ export class LobbyView implements Component, Focusable {
|
|
|
1310
1589
|
|
|
1311
1590
|
/** Every key the current tab understands while browsing. */
|
|
1312
1591
|
private tabKeys(): KeyHelp[] {
|
|
1313
|
-
const session = this.host.planner();
|
|
1314
1592
|
switch (this.tab) {
|
|
1315
1593
|
case "lobby":
|
|
1316
1594
|
return [
|
|
1317
|
-
{ key: "type", text: "talk to the oracle (starts a task when none is running)"
|
|
1318
|
-
{ key: "
|
|
1319
|
-
{ key: "
|
|
1595
|
+
{ key: "type", text: "talk to the oracle of the session in view (in this window, starts a task when none is running)" },
|
|
1596
|
+
{ key: "enter", text: "on an empty prompt: answer the question a background session waits on" },
|
|
1597
|
+
{ key: "↑ ↓", text: "scroll the focused pane (PageUp/PageDown a page)" },
|
|
1598
|
+
{ key: "← →", text: "move between the conversation, activity log and thinking" },
|
|
1320
1599
|
{ key: "Home End", text: "the oldest lines, or back to the newest" },
|
|
1321
|
-
{ key: "c", text: "comment on this task's plan"
|
|
1600
|
+
{ key: "c", text: "comment on this task's plan" },
|
|
1322
1601
|
{ key: "esc", text: "stop the oracle while it works" },
|
|
1323
1602
|
];
|
|
1324
1603
|
case "tasks":
|
|
1325
1604
|
return [
|
|
1326
|
-
{ key: "↑ ↓", text: "select a task or plan, or scroll the detail (PageUp/PageDown)"
|
|
1327
|
-
{ key: "enter / ← →", text: "move between the list and the detail"
|
|
1328
|
-
{ key: "c", text: "comment on the selected task's plan"
|
|
1329
|
-
{ key: "s", text: "start the selected planned task
|
|
1330
|
-
{ key: "
|
|
1605
|
+
{ key: "↑ ↓", text: "select a task or plan, or scroll the detail (PageUp/PageDown)" },
|
|
1606
|
+
{ key: "enter / ← →", text: "move between the list and the detail" },
|
|
1607
|
+
{ key: "c", text: "comment on the selected task's plan" },
|
|
1608
|
+
{ key: "s", text: "start the selected planned task in a new session, named after it" },
|
|
1609
|
+
{ key: "h", text: "start the selected planned task here, in this window" },
|
|
1610
|
+
{ key: "n", text: "type a new task that starts in its own session" },
|
|
1611
|
+
{ key: "o", text: "show the session driving the selected task in the Lobby tab" },
|
|
1612
|
+
{ key: "x x", text: "stop the background session driving the selected task" },
|
|
1613
|
+
{ key: "d d", text: "discard the selected planned task" },
|
|
1331
1614
|
{ key: "r", text: "reread tasks from disk" },
|
|
1332
1615
|
];
|
|
1333
1616
|
case "plan":
|
|
1334
1617
|
return [
|
|
1335
|
-
{ key: "a", text: "answer the panel's questions, one questionnaire at a time (enter on an empty prompt too)"
|
|
1336
|
-
{ key: "enter / ← →", text: "move between the conversation and the draft"
|
|
1337
|
-
{ key: "↑ ↓", text: "pick a draft line (draft) or scroll (conversation); PageUp/PageDown a page"
|
|
1338
|
-
{ key: "c / click", text: "comment on the picked draft line"
|
|
1339
|
-
{ key: "1-4", text: "seat or unseat DEV, DESIGN, QA, RESEARCH"
|
|
1340
|
-
{ key: "
|
|
1341
|
-
{ key: "n", text: "start a new plan", hint: "new" },
|
|
1618
|
+
{ key: "a", text: "answer the panel's questions, one questionnaire at a time (enter on an empty prompt too)" },
|
|
1619
|
+
{ key: "enter / ← →", text: "move between the conversation and the draft" },
|
|
1620
|
+
{ key: "↑ ↓", text: "pick a draft line (draft) or scroll (conversation); PageUp/PageDown a page" },
|
|
1621
|
+
{ key: "c / click", text: "comment on the picked draft line" },
|
|
1622
|
+
{ key: "1-4", text: "seat or unseat DEV, DESIGN, QA, RESEARCH" },
|
|
1623
|
+
{ key: "n", text: "start a new plan" },
|
|
1342
1624
|
{ key: "x", text: "stop the round" },
|
|
1343
1625
|
{ key: "r", text: "retry a failed round or seat" },
|
|
1344
1626
|
{ key: "m", text: "the oracle's model, thinking and time limit (Planner settings)" },
|
|
1345
1627
|
];
|
|
1346
1628
|
case "quickfix":
|
|
1347
1629
|
return [
|
|
1348
|
-
{ key: "type", text: "describe a quick change"
|
|
1349
|
-
{ key: "↑ ↓", text: "select a quick fix, or scroll the detail (PageUp/PageDown)"
|
|
1350
|
-
{ key: "enter / ← →", text: "move between the list and the detail"
|
|
1351
|
-
{ key: "x", text: "cancel the selected quick fix"
|
|
1352
|
-
{ key: "m", text: "the quick fix agent's model, thinking, time limit and instructions"
|
|
1630
|
+
{ key: "type", text: "describe a quick change" },
|
|
1631
|
+
{ key: "↑ ↓", text: "select a quick fix, or scroll the detail (PageUp/PageDown)" },
|
|
1632
|
+
{ key: "enter / ← →", text: "move between the list and the detail" },
|
|
1633
|
+
{ key: "x", text: "cancel the selected quick fix" },
|
|
1634
|
+
{ key: "m", text: "the quick fix agent's model, thinking, time limit and instructions" },
|
|
1353
1635
|
];
|
|
1354
1636
|
case "issues":
|
|
1355
1637
|
return [
|
|
1356
|
-
{ key: "↑ ↓", text: "select an issue"
|
|
1357
|
-
{ key: "enter", text: "read the selected issue"
|
|
1358
|
-
{ key: "p", text: "plan the selected issue"
|
|
1359
|
-
{ key: "n", text: "file a new issue"
|
|
1360
|
-
{ key: "r", text: "reload issues"
|
|
1638
|
+
{ key: "↑ ↓", text: "select an issue" },
|
|
1639
|
+
{ key: "enter", text: "read the selected issue" },
|
|
1640
|
+
{ key: "p", text: "plan the selected issue" },
|
|
1641
|
+
{ key: "n", text: "file a new issue" },
|
|
1642
|
+
{ key: "r", text: "reload issues" },
|
|
1361
1643
|
];
|
|
1362
1644
|
case "metrics":
|
|
1363
1645
|
return [
|
|
1364
|
-
{ key: "↑ ↓", text: "select a row of the table"
|
|
1365
|
-
{ key: "g", text: "group by model, or by model and agent"
|
|
1366
|
-
{ key: "s", text: "change the sort"
|
|
1367
|
-
{ key: "r", text: "reread the metrics log"
|
|
1646
|
+
{ key: "↑ ↓", text: "select a row of the table" },
|
|
1647
|
+
{ key: "g", text: "group by model, or by model and agent" },
|
|
1648
|
+
{ key: "s", text: "change the sort" },
|
|
1649
|
+
{ key: "r", text: "reread the metrics log" },
|
|
1368
1650
|
];
|
|
1369
1651
|
}
|
|
1370
1652
|
}
|
|
@@ -1383,14 +1665,62 @@ export class LobbyView implements Component, Focusable {
|
|
|
1383
1665
|
};
|
|
1384
1666
|
if (this.help) return fit(`${badge("KEYS", "accent")} ${this.chips([["any key", "closes"]], theme)}`, width);
|
|
1385
1667
|
if (this.searching) return fit(`${badge("SEARCH", "warning")} ${this.chips([["enter", "keep"], ["esc", "clear"], ["↑↓", "results"], [k("nextTab"), "next tab"]], theme)}`, width);
|
|
1668
|
+
const mode = this.mode === "type" ? badge("TYPE", "accent") : badge("BROWSE", "muted");
|
|
1669
|
+
return fit(`${mode} ${this.chips(this.hintChips(), theme)}`, width);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* The few keys that matter right now, most useful first: typing names what
|
|
1674
|
+
* enter does here; browsing names the tab's main commands, only those that
|
|
1675
|
+
* apply. Everything else is one `?` away.
|
|
1676
|
+
*/
|
|
1677
|
+
private hintChips(): Array<[string, string]> {
|
|
1678
|
+
const k = (action: LobbyAction) => keyLabel(this.keys[action]).toLowerCase();
|
|
1679
|
+
const session = this.host.planner();
|
|
1386
1680
|
if (this.mode === "type") {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1681
|
+
if (this.tab === "lobby" && this.newSession) return [["enter", "start in a new session"], ["esc", "cancel"]];
|
|
1682
|
+
if (this.tab === "lobby" && this.viewing.kind !== "here") {
|
|
1683
|
+
const background = this.viewedSession();
|
|
1684
|
+
if (background?.dialogs.length && !this.editor.getText().trim()) return [["enter", "answer its question"], [k("sessions"), "sessions"], [k("hide"), "hide"]];
|
|
1685
|
+
return [["enter", background?.busy ? "steer" : "send"], [k("sessions"), "sessions"], [k("toggleAuto"), "auto"]];
|
|
1686
|
+
}
|
|
1687
|
+
if (this.tab === "lobby" && this.host.masterBusy()) return [["enter", "steer"], ["esc", "stop the oracle"], [k("hide"), "hide"]];
|
|
1688
|
+
if (this.tab === "plan" && this.lineTarget) return [["enter", "add the comment"], ["esc", "cancel"]];
|
|
1689
|
+
const enter = this.tab === "plan" && session?.awaitingAnswers && !this.editor.getText().trim() ? "answer questions" : this.tab === "quickfix" ? "run it" : "send";
|
|
1690
|
+
return [["enter", enter], ["esc", "browse"], [k("hide"), "hide"]];
|
|
1691
|
+
}
|
|
1692
|
+
const keys: Array<[string, string]> = [];
|
|
1693
|
+
switch (this.tab) {
|
|
1694
|
+
case "lobby":
|
|
1695
|
+
keys.push(["←→", "pane"], ["↑↓", "scroll"], [k("sessions"), "sessions"], [k("newSession"), "new session"]);
|
|
1696
|
+
if (this.viewedEntry().task) keys.push([k("toggleAuto"), "auto"], ["c", "comment on plan"]);
|
|
1697
|
+
break;
|
|
1698
|
+
case "tasks": {
|
|
1699
|
+
const row = this.taskRowList()[this.tasksSelected];
|
|
1700
|
+
keys.push(["↑↓", "select"]);
|
|
1701
|
+
if (row?.kind === "task") keys.push(["c", "comment"], ["o", "open its session"], [k("toggleAuto"), "auto"]);
|
|
1702
|
+
if (row?.kind === "plan") keys.push(["s", "start in a new session"], ["h", "start here"], ["d", "discard"]);
|
|
1703
|
+
keys.push(["n", "new task"]);
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
case "plan":
|
|
1707
|
+
if (session?.awaitingAnswers) keys.push(["a", "answer"]);
|
|
1708
|
+
if (session?.reply?.plan) keys.push([k("savePlan"), "save"], ["c", "comment on a line"]);
|
|
1709
|
+
if (session?.busy) keys.push(["x", "stop"]);
|
|
1710
|
+
keys.push(["n", "new"], ["1-4", "seats"]);
|
|
1711
|
+
break;
|
|
1712
|
+
case "quickfix":
|
|
1713
|
+
if (this.host.quickfix.jobs.length > 0) keys.push(["↑↓", "select"], ["x", "cancel"]);
|
|
1714
|
+
keys.push(["m", "model"]);
|
|
1715
|
+
break;
|
|
1716
|
+
case "issues":
|
|
1717
|
+
keys.push(["↑↓", "select"], ["p", "plan it"], ["n", "new"]);
|
|
1718
|
+
break;
|
|
1719
|
+
case "metrics":
|
|
1720
|
+
keys.push(["↑↓", "select"], ["g", "group"], ["s", "sort"]);
|
|
1721
|
+
break;
|
|
1390
1722
|
}
|
|
1391
|
-
|
|
1392
|
-
const panes: Array<[string, string]> = this.tab === "lobby" ? [[k("toggleActivity"), "activity"], [k("toggleThinking"), "thinking"]] : [];
|
|
1393
|
-
return fit(`${badge("BROWSE", "muted")} ${this.chips([...tabHints, ...panes, ["/", "search"], ["?", "keys"], [k("hide"), "hide"]], theme)}`, width);
|
|
1723
|
+
return [...keys, ["?", "all keys"]];
|
|
1394
1724
|
}
|
|
1395
1725
|
|
|
1396
1726
|
/** The help screen: every shortcut, the typing and browsing keys, and this tab's commands. */
|
|
@@ -1459,6 +1789,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1459
1789
|
...(query ? { query } : {}),
|
|
1460
1790
|
layout: this.planLayout,
|
|
1461
1791
|
panes: this.panes,
|
|
1792
|
+
saveKey: keyLabel(this.keys.savePlan),
|
|
1462
1793
|
}, width, height, theme);
|
|
1463
1794
|
if (!session) Object.assign(this.planLayout, { draftRows: 0, draftStart: 0, draftText: [] });
|
|
1464
1795
|
this.planCursor = Math.min(this.planCursor, Math.max(0, this.planLayout.draftText.length - 1));
|
|
@@ -1509,7 +1840,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1509
1840
|
seated: seats.has(member),
|
|
1510
1841
|
status: state ? (state.status === "thinking" ? "thinking" : state.status) : "idle",
|
|
1511
1842
|
...(state?.step ? { step: state.step } : {}),
|
|
1512
|
-
|
|
1843
|
+
// The questions from this seat that reached the user this round.
|
|
1844
|
+
questions: session?.questions.filter((question) => question.from === MEMBER_LABELS[member]).length ?? 0,
|
|
1513
1845
|
ready: state?.reply?.status === "ready",
|
|
1514
1846
|
profile: this.host.seatLabel(member),
|
|
1515
1847
|
};
|
|
@@ -1537,14 +1869,36 @@ export class LobbyView implements Component, Focusable {
|
|
|
1537
1869
|
};
|
|
1538
1870
|
}
|
|
1539
1871
|
|
|
1872
|
+
/** The session switcher: every session this window can show, with what each is doing. */
|
|
1873
|
+
private pickerBody(width: number, height: number, theme: LobbyTheme): string[] {
|
|
1874
|
+
const entries = this.sessionEntries();
|
|
1875
|
+
this.pickIndex = Math.min(this.pickIndex, Math.max(0, entries.length - 1));
|
|
1876
|
+
const inner = width - 4;
|
|
1877
|
+
const nameWidth = Math.min(34, Math.max(14, Math.floor(inner * 0.36)));
|
|
1878
|
+
const marks = { "this window": "●", background: "◆", "other terminal": "◇" } as const;
|
|
1879
|
+
const lines = entries.map((entry, index) => {
|
|
1880
|
+
const selected = index === this.pickIndex;
|
|
1881
|
+
const mark = paint(theme, entry.where === "this window" ? "accent" : "toolTitle", marks[entry.where]);
|
|
1882
|
+
const name = fit(selected ? bold(theme, entry.name) : entry.name, nameWidth);
|
|
1883
|
+
const status = paint(theme, entry.status === "working" ? "accent" : entry.status === "ended" ? "dim" : "muted", entry.status.replace(/_/g, " "));
|
|
1884
|
+
const badges = [entry.auto ? paint(theme, "success", "⟳ auto") : "", entry.waiting > 0 ? paint(theme, "warning", `● ${entry.waiting} waiting`) : ""].filter(Boolean).join(" ");
|
|
1885
|
+
const where = paint(theme, "dim", entry.where);
|
|
1886
|
+
return selectRow(theme, `${selected ? paint(theme, "accent", "▸") : " "} ${mark} ${name} ${where} ${paint(theme, "dim", "·")} ${status}${badges ? ` ${badges}` : ""}`, inner, selected, true);
|
|
1887
|
+
});
|
|
1888
|
+
const help = paint(theme, "dim", "enter shows it here · n new task in a new session · x x stops a background session · esc closes");
|
|
1889
|
+
return box(width, height, [...lines, "", ...wrap(help, inner)], { title: "Sessions", right: `${entries.length}`, focused: true, theme });
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1540
1892
|
private homeBody(width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
1541
1893
|
const zen = this.host.zen();
|
|
1542
1894
|
const feed = this.host.feed;
|
|
1543
1895
|
const sessionId = this.host.sessionId();
|
|
1544
1896
|
const others = this.data.tasks.filter((task) => !TERMINAL_STATES.includes(task.state) && task.ownerSessionId !== sessionId).length;
|
|
1545
1897
|
const query = this.query();
|
|
1898
|
+
const entry = this.viewedEntry();
|
|
1899
|
+
if (entry.view.kind !== "here") return this.otherSessionBody(entry, width, height, theme, now);
|
|
1546
1900
|
return renderHome({
|
|
1547
|
-
...(zen.task ? { task: zen.task, scene: (w: number, h: number) => this.host.scene(w, h) } : {}),
|
|
1901
|
+
...(zen.task ? { task: zen.task, scene: (w: number, h: number, animated: boolean) => this.host.scene(w, h, animated) } : {}),
|
|
1548
1902
|
chat: feed.chat,
|
|
1549
1903
|
...(feed.reply ? { liveReply: feed.reply } : {}),
|
|
1550
1904
|
activity: feed.activity,
|
|
@@ -1560,7 +1914,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1560
1914
|
panels: this.panels,
|
|
1561
1915
|
...(query ? { query } : {}),
|
|
1562
1916
|
keys: {
|
|
1563
|
-
|
|
1917
|
+
animations: keyLabel(this.keys.toggleScene),
|
|
1564
1918
|
conversation: keyLabel(this.keys.toggleConversation),
|
|
1565
1919
|
activity: keyLabel(this.keys.toggleActivity),
|
|
1566
1920
|
thinking: keyLabel(this.keys.toggleThinking),
|
|
@@ -1568,6 +1922,57 @@ export class LobbyView implements Component, Focusable {
|
|
|
1568
1922
|
}, width, height, theme);
|
|
1569
1923
|
}
|
|
1570
1924
|
|
|
1925
|
+
/**
|
|
1926
|
+
* Another session on the Lobby tab: a background session's live feed, or an
|
|
1927
|
+
* other terminal's conversation read from its session file; the task's
|
|
1928
|
+
* status sits on top without animations.
|
|
1929
|
+
*/
|
|
1930
|
+
private otherSessionBody(entry: SessionEntry, width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
1931
|
+
const session = this.viewedSession();
|
|
1932
|
+
const task = entry.task;
|
|
1933
|
+
const query = this.query();
|
|
1934
|
+
const common = {
|
|
1935
|
+
...(task ? { task, scene: (w: number, h: number) => this.host.taskScene(task, w, h), stillScene: true } : {}),
|
|
1936
|
+
others: 0,
|
|
1937
|
+
pending: 0,
|
|
1938
|
+
offsets: this.homeOffsets,
|
|
1939
|
+
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
1940
|
+
panes: this.panes,
|
|
1941
|
+
tick: this.tick,
|
|
1942
|
+
now,
|
|
1943
|
+
panels: this.panels,
|
|
1944
|
+
...(query ? { query } : {}),
|
|
1945
|
+
title: entry.name,
|
|
1946
|
+
keys: {
|
|
1947
|
+
animations: keyLabel(this.keys.toggleScene),
|
|
1948
|
+
conversation: keyLabel(this.keys.toggleConversation),
|
|
1949
|
+
activity: keyLabel(this.keys.toggleActivity),
|
|
1950
|
+
thinking: keyLabel(this.keys.toggleThinking),
|
|
1951
|
+
},
|
|
1952
|
+
};
|
|
1953
|
+
if (session) {
|
|
1954
|
+
const emptyNote = session.status === "starting" ? `starting ${session.name}…` : !session.alive ? `${session.name} has ended.` : `${session.name} has not said anything yet.`;
|
|
1955
|
+
return renderHome({
|
|
1956
|
+
...common,
|
|
1957
|
+
chat: session.feed.chat,
|
|
1958
|
+
...(session.feed.reply ? { liveReply: session.feed.reply } : {}),
|
|
1959
|
+
activity: session.feed.activity,
|
|
1960
|
+
thoughts: session.feed.thoughts,
|
|
1961
|
+
busy: session.busy,
|
|
1962
|
+
emptyNote,
|
|
1963
|
+
}, width, height, theme);
|
|
1964
|
+
}
|
|
1965
|
+
return renderHome({
|
|
1966
|
+
...common,
|
|
1967
|
+
chat: task?.ownerSessionId ? this.host.sessionChat(task.ownerSessionId) : [],
|
|
1968
|
+
activity: [],
|
|
1969
|
+
thoughts: [],
|
|
1970
|
+
busy: false,
|
|
1971
|
+
emptyNote: "Nothing said in this session yet.",
|
|
1972
|
+
activityNote: "This session runs in another terminal: its conversation shows here and your messages reach its oracle, but live activity only streams from sessions started in this window.",
|
|
1973
|
+
}, width, height, theme);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1571
1976
|
private tasksBody(width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
1572
1977
|
const rows = this.taskRowList();
|
|
1573
1978
|
this.tasksSelected = Math.min(this.tasksSelected, Math.max(0, rows.length - 1));
|
|
@@ -1595,9 +2000,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
1595
2000
|
const bodyHeight = Math.max(0, rows - top.length - search.length - prompt.length - 1);
|
|
1596
2001
|
this.bodyTop = top.length;
|
|
1597
2002
|
this.promptTop = top.length + bodyHeight + search.length;
|
|
1598
|
-
let body = this.help ? this.helpBody(width, bodyHeight, theme) : this.body(width, bodyHeight, theme);
|
|
2003
|
+
let body = this.help ? this.helpBody(width, bodyHeight, theme) : this.picking ? this.pickerBody(width, bodyHeight, theme) : this.body(width, bodyHeight, theme);
|
|
1599
2004
|
// A pane scrolled back that gained lines moves with them, so it keeps showing what was being read.
|
|
1600
|
-
if (!this.help && this.clampScroll()) body = this.body(width, bodyHeight, theme);
|
|
2005
|
+
if (!this.help && !this.picking && this.clampScroll()) body = this.body(width, bodyHeight, theme);
|
|
1601
2006
|
const query = this.query();
|
|
1602
2007
|
if (query && !this.help) body = body.map((line) => highlight(line, query));
|
|
1603
2008
|
return [...top, ...body, ...search, ...prompt, hint].slice(0, rows).map((line) => fit(line, width));
|