@a-t-h-i/bot-lobby 0.6.0 → 0.6.2
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 +177 -799
- package/package.json +1 -1
- package/prompts/master.md +17 -0
- package/prompts/panel.md +3 -1
- package/prompts/planner.md +14 -0
- package/prompts/quickfix.md +3 -1
- package/prompts/scout.md +3 -0
- package/prompts/worker.md +3 -0
- package/src/classifier/answers.ts +110 -0
- package/src/classifier/classifier.ts +171 -0
- package/src/classifier/client.ts +239 -0
- package/src/classifier/effort.ts +143 -0
- package/src/classifier/files.ts +427 -0
- package/src/classifier/hosts.ts +157 -0
- package/src/classifier/instance.ts +98 -0
- package/src/classifier/limits.ts +37 -0
- package/src/classifier/seats.ts +102 -0
- package/src/classifier/tools.ts +58 -0
- package/src/classifier/triage.ts +203 -0
- package/src/execution/agent-runner.ts +5 -0
- package/src/index.ts +6 -0
- package/src/lobby/keys.ts +1 -1
- package/src/lobby/planner.ts +305 -27
- package/src/lobby/quickfix.ts +120 -8
- package/src/lobby/runtime.ts +136 -5
- package/src/lobby/session-files.ts +12 -0
- package/src/lobby/sessions.ts +28 -2
- package/src/lobby/tabs/home.ts +54 -26
- package/src/lobby/tabs/metrics.ts +28 -1
- package/src/lobby/tabs/plan.ts +27 -4
- package/src/lobby/tabs/quickfix.ts +5 -1
- package/src/lobby/tabs/tasks.ts +30 -14
- package/src/lobby/view.ts +334 -41
- package/src/master/master.ts +95 -29
- package/src/pi/commands.ts +24 -4
- package/src/pi/events.ts +11 -2
- package/src/pi/owner.ts +57 -8
- package/src/pi/run-summary.ts +3 -1
- package/src/pi/settings-ui.ts +138 -2
- package/src/pi/start-task.ts +4 -0
- package/src/pi/tools.ts +7 -2
- package/src/schemas/configuration.ts +125 -1
- package/src/schemas/findings.ts +4 -0
- package/src/schemas/task.ts +24 -0
- package/src/state/archive.ts +99 -0
- package/src/state/inbox.ts +49 -15
- package/src/state/metrics.ts +74 -1
- package/src/state/presence.ts +99 -0
- package/src/workflow/workflow.ts +49 -1
package/src/lobby/view.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
|
14
14
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
15
15
|
import type { PlannedTask } from "../state/backlog.ts";
|
|
16
16
|
import type { PlanComment } from "../state/comments.ts";
|
|
17
|
-
import { aggregateMetrics, collectMetrics, SORT_KEYS, sortGroups, taskStats, taskTimesByModel, type GroupBy, type MetricRecord, type SortKey } from "../state/metrics.ts";
|
|
17
|
+
import { aggregateMetrics, collectMetrics, SORT_KEYS, sortGroups, summarizeClassifier, 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
19
|
import type { ChatEntry, LobbyFeed } from "./feed.ts";
|
|
20
20
|
import type { BackgroundSession } from "./sessions.ts";
|
|
@@ -22,9 +22,9 @@ import type { QuickFixQueue } from "./quickfix.ts";
|
|
|
22
22
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
23
23
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
24
24
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
25
|
-
import { beside, bold, box, fit, highlight, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
-
import { HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
27
|
-
import { filterRows, planDetailLines, renderTasks, taskDetailLines, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
25
|
+
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
+
import { chatLines, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
27
|
+
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
28
28
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
29
29
|
import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
30
30
|
import { renderIssues } from "./tabs/issues.ts";
|
|
@@ -56,22 +56,66 @@ export type LobbyMode = "type" | "browse";
|
|
|
56
56
|
const PANEL_NAMES: Record<LobbyPanel, string> = { animations: "oracle and agent animations", conversation: "conversation", activity: "activity log", thinking: "thinking" };
|
|
57
57
|
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = { animations: "toggleScene", conversation: "toggleConversation", activity: "toggleActivity", thinking: "toggleThinking" };
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Which session the Lobby tab shows and talks to: this window, one it started
|
|
61
|
+
* in the background, one running in another terminal, or a task whose
|
|
62
|
+
* session is not running at all.
|
|
63
|
+
*/
|
|
64
|
+
export type SessionView =
|
|
65
|
+
| { kind: "here" }
|
|
66
|
+
| { kind: "background"; key: string }
|
|
67
|
+
| { kind: "other"; sessionId: string }
|
|
68
|
+
| { kind: "idle"; taskId: string };
|
|
69
|
+
|
|
70
|
+
export type SessionWhere = "this window" | "background" | "other terminal" | "not running";
|
|
61
71
|
|
|
62
|
-
/** One row of the session
|
|
72
|
+
/** One row of the session browser. */
|
|
63
73
|
export interface SessionEntry {
|
|
64
74
|
view: SessionView;
|
|
65
75
|
name: string;
|
|
66
|
-
where:
|
|
76
|
+
where: SessionWhere;
|
|
67
77
|
task?: Task;
|
|
68
78
|
/** What it is doing: working, idle, starting, ended, or its task's state. */
|
|
69
79
|
status: string;
|
|
70
80
|
auto: boolean;
|
|
71
81
|
/** Questions it waits on you for. */
|
|
72
82
|
waiting: number;
|
|
83
|
+
/** The pi session it is (absent for a task no session ever owned). */
|
|
84
|
+
sessionId?: string;
|
|
85
|
+
/** The process running it, for sessions in other terminals. */
|
|
86
|
+
pid?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A session running in this project, as its heartbeat tells (see state/presence.ts). */
|
|
90
|
+
export interface LiveSession {
|
|
91
|
+
sessionId: string;
|
|
92
|
+
pid: number;
|
|
93
|
+
name?: string;
|
|
94
|
+
taskId?: string;
|
|
95
|
+
mode: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What `switchTo` needs: the session to run in this window, and the background process to stop first. */
|
|
99
|
+
export interface SwitchTarget {
|
|
100
|
+
name: string;
|
|
101
|
+
sessionId?: string;
|
|
102
|
+
background?: BackgroundSession;
|
|
103
|
+
/** A task no session owns: this window claims it instead. */
|
|
104
|
+
claimTaskId?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Two views show the same session. */
|
|
108
|
+
function sameView(a: SessionView, b: SessionView): boolean {
|
|
109
|
+
if (a.kind !== b.kind) return false;
|
|
110
|
+
if (a.kind === "background" && b.kind === "background") return a.key === b.key;
|
|
111
|
+
if (a.kind === "other" && b.kind === "other") return a.sessionId === b.sessionId;
|
|
112
|
+
if (a.kind === "idle" && b.kind === "idle") return a.taskId === b.taskId;
|
|
113
|
+
return true;
|
|
73
114
|
}
|
|
74
115
|
|
|
116
|
+
const SECTION_OF: Record<SessionWhere, string> = { "this window": "THIS WINDOW", background: "BACKGROUND", "other terminal": "OTHER TERMINALS", "not running": "NOT RUNNING" };
|
|
117
|
+
const WHERE_MARKS: Record<SessionWhere, string> = { "this window": "●", background: "◆", "other terminal": "◇", "not running": "○" };
|
|
118
|
+
|
|
75
119
|
/** Everything the view needs from pi and bot-lobby. */
|
|
76
120
|
export interface LobbyHost {
|
|
77
121
|
rows(): number;
|
|
@@ -89,6 +133,8 @@ export interface LobbyHost {
|
|
|
89
133
|
plans(): PlannedTask[];
|
|
90
134
|
comments(taskId: string): PlanComment[];
|
|
91
135
|
metrics(): MetricRecord[];
|
|
136
|
+
/** Classifier calls (kind `classifier`), kept out of the agent tables. */
|
|
137
|
+
classifierMetrics?(): MetricRecord[];
|
|
92
138
|
/** Send text to the oracle, or start a task when none is active; returns a notice. */
|
|
93
139
|
toOracle(text: string): string | undefined;
|
|
94
140
|
comment(taskId: string, text: string): string;
|
|
@@ -104,6 +150,8 @@ export interface LobbyHost {
|
|
|
104
150
|
answerPanel(): Promise<string>;
|
|
105
151
|
/** The seats a new session starts with, from settings. */
|
|
106
152
|
defaultPanel(): readonly PanelMember[];
|
|
153
|
+
/** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
|
|
154
|
+
planningRounds?(): number;
|
|
107
155
|
/** `model · thinking` a panel seat runs on. */
|
|
108
156
|
seatLabel(member: PanelMember): string;
|
|
109
157
|
issues: IssuesState;
|
|
@@ -127,8 +175,20 @@ export interface LobbyHost {
|
|
|
127
175
|
/** Auto mode for any task, whichever session drives it. */
|
|
128
176
|
isAuto(taskId: string): boolean;
|
|
129
177
|
setAuto(taskId: string, on: boolean): void;
|
|
130
|
-
/** Leave a message for
|
|
178
|
+
/** Leave a message for a task's oracle, delivered by whichever session drives it (now or once one resumes it); returns a notice. */
|
|
131
179
|
sendToTask(taskId: string, text: string): string;
|
|
180
|
+
/** Leave a message for a session running in another terminal; it delivers it within seconds. Returns a notice. */
|
|
181
|
+
sendToSession(sessionId: string, text: string): string;
|
|
182
|
+
/** Sessions running in this project, other than this window's, from their heartbeats. */
|
|
183
|
+
liveSessions(): readonly LiveSession[];
|
|
184
|
+
/** Run another session in this window (stopping its background process first); resolves to a notice. */
|
|
185
|
+
switchTo(target: SwitchTarget): Promise<string>;
|
|
186
|
+
/** Archived tasks, most recently archived first. */
|
|
187
|
+
archivedTasks(): Task[];
|
|
188
|
+
/** Archive, restore or delete a task; each returns a notice. */
|
|
189
|
+
archiveTask(taskId: string): string;
|
|
190
|
+
restoreTask(taskId: string): string;
|
|
191
|
+
deleteTask(taskId: string, where: "list" | "archive"): string;
|
|
132
192
|
/** Another session's conversation, read from its saved session file. */
|
|
133
193
|
sessionChat(sessionId: string): ChatEntry[];
|
|
134
194
|
/** A task's status without animations, for a session other than this window's. */
|
|
@@ -278,11 +338,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
278
338
|
private metricsBy: GroupBy = "model";
|
|
279
339
|
private metricsSort: SortKey = "runs";
|
|
280
340
|
|
|
281
|
-
|
|
341
|
+
/** The Tasks tab lists archived tasks too (v). */
|
|
342
|
+
showArchived = false;
|
|
343
|
+
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; at: number } = {
|
|
282
344
|
tasks: [],
|
|
283
345
|
plans: [],
|
|
346
|
+
archived: [],
|
|
284
347
|
comments: new Map(),
|
|
285
348
|
metrics: [],
|
|
349
|
+
classifierMetrics: [],
|
|
286
350
|
at: 0,
|
|
287
351
|
};
|
|
288
352
|
|
|
@@ -369,8 +433,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
369
433
|
this.data = {
|
|
370
434
|
tasks: this.host.tasks(),
|
|
371
435
|
plans: this.host.plans(),
|
|
436
|
+
archived: this.showArchived ? this.host.archivedTasks() : [],
|
|
372
437
|
comments: new Map(),
|
|
373
438
|
metrics: this.tab === "metrics" ? this.host.metrics() : this.data.metrics,
|
|
439
|
+
classifierMetrics: this.tab === "metrics" ? this.host.classifierMetrics?.() ?? [] : this.data.classifierMetrics,
|
|
374
440
|
at: now,
|
|
375
441
|
};
|
|
376
442
|
if (this.tab === "tasks") {
|
|
@@ -919,6 +985,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
919
985
|
else this.say("no task in this session to comment on", "warning");
|
|
920
986
|
return true;
|
|
921
987
|
}
|
|
988
|
+
if (data === "s" && this.viewing.kind !== "here") {
|
|
989
|
+
this.switchTo(this.viewedEntry());
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
922
992
|
return enter ? (this.setMode("type"), true) : false;
|
|
923
993
|
}
|
|
924
994
|
case "tasks":
|
|
@@ -934,6 +1004,17 @@ export class LobbyView implements Component, Focusable {
|
|
|
934
1004
|
return true;
|
|
935
1005
|
}
|
|
936
1006
|
if (data === "m") return this.openSettings("quickfix"), true;
|
|
1007
|
+
if (data === "r" || data === "t") {
|
|
1008
|
+
const job = this.fixJobs()[this.fixSelected];
|
|
1009
|
+
// Only a held job takes r and t; otherwise the key resumes typing like any other.
|
|
1010
|
+
if (!job || job.status !== "held") return false;
|
|
1011
|
+
if (data === "r" && this.host.quickfix.runAnyway(job.id)) this.say(`running ${job.id} anyway`);
|
|
1012
|
+
if (data === "t") {
|
|
1013
|
+
this.startInNewSession({ request: job.prompt });
|
|
1014
|
+
this.host.quickfix.movedToTask(job.id);
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
937
1018
|
return false;
|
|
938
1019
|
case "issues":
|
|
939
1020
|
return this.issuesCommand(data, enter, escape);
|
|
@@ -951,7 +1032,17 @@ export class LobbyView implements Component, Focusable {
|
|
|
951
1032
|
if (escape) return (this.tasksFocus = "list"), true;
|
|
952
1033
|
if (data === "r") return this.refreshData(true), true;
|
|
953
1034
|
if (data === "n") return this.startNewSessionPrompt(), true;
|
|
1035
|
+
if (data === "v") {
|
|
1036
|
+
this.showArchived = !this.showArchived;
|
|
1037
|
+
this.refreshData(true);
|
|
1038
|
+
const count = this.data.archived.length;
|
|
1039
|
+
this.say(this.showArchived ? (count > 0 ? `showing ${count} archived task${count === 1 ? "" : "s"} — a restores one, d d deletes it` : "the archive is empty") : "archived tasks hidden");
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
if (data === "A") return this.archiveFinished(), true;
|
|
954
1043
|
if (!row) return false;
|
|
1044
|
+
if (row.kind === "archived") return this.archivedCommand(row, data);
|
|
1045
|
+
if ((data === "a" || data === "d") && row.kind === "task") return this.archiveOrDelete(row, data === "a" ? "archive" : "delete"), true;
|
|
955
1046
|
if (data === "c") {
|
|
956
1047
|
const task = this.data.tasks.find((entry) => entry.id === row.id);
|
|
957
1048
|
if (row.kind !== "task" || !task) this.say("pick a task to comment on its plan", "warning");
|
|
@@ -1011,6 +1102,65 @@ export class LobbyView implements Component, Focusable {
|
|
|
1011
1102
|
return false;
|
|
1012
1103
|
}
|
|
1013
1104
|
|
|
1105
|
+
/**
|
|
1106
|
+
* Archive (a) or delete (d d) a task on the list. Deleting always asks for
|
|
1107
|
+
* the key twice; archiving a task still under way does too, since it is
|
|
1108
|
+
* abandoned first.
|
|
1109
|
+
*/
|
|
1110
|
+
private archiveOrDelete(row: TaskRow, action: "archive" | "delete"): void {
|
|
1111
|
+
const task = this.data.tasks.find((entry) => entry.id === row.id);
|
|
1112
|
+
if (!task) return;
|
|
1113
|
+
const open = !TERMINAL_STATES.includes(task.state);
|
|
1114
|
+
const armed = `${action}:${task.id}`;
|
|
1115
|
+
if ((action === "delete" || open) && this.armed !== armed) {
|
|
1116
|
+
this.armed = armed;
|
|
1117
|
+
const key = action === "delete" ? "d" : "a";
|
|
1118
|
+
this.say(action === "delete" ? `press ${key} again to delete ${task.id} for good` : `${task.id} is ${task.state.replace(/_/g, " ")} — press a again to abandon and archive it`, "warning");
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
this.armed = undefined;
|
|
1122
|
+
const notice = action === "archive" ? this.host.archiveTask(task.id) : this.host.deleteTask(task.id, "list");
|
|
1123
|
+
this.say(notice, /^(archived|deleted)/.test(notice) ? "info" : "warning");
|
|
1124
|
+
this.refreshData(true);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Keys on an archived task: a restores it, d d deletes it. */
|
|
1128
|
+
private archivedCommand(row: TaskRow, data: string): boolean {
|
|
1129
|
+
if (data === "a") {
|
|
1130
|
+
const notice = this.host.restoreTask(row.id);
|
|
1131
|
+
this.say(notice, notice.startsWith("restored") ? "info" : "warning");
|
|
1132
|
+
this.refreshData(true);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
if (data === "d") {
|
|
1136
|
+
if (this.armed !== `purge:${row.id}`) {
|
|
1137
|
+
this.armed = `purge:${row.id}`;
|
|
1138
|
+
this.say(`press d again to delete ${row.id} for good`, "warning");
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
this.armed = undefined;
|
|
1142
|
+
const notice = this.host.deleteTask(row.id, "archive");
|
|
1143
|
+
this.say(notice, notice.startsWith("deleted") ? "info" : "warning");
|
|
1144
|
+
this.refreshData(true);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Archive every finished task at once (A twice). */
|
|
1151
|
+
private archiveFinished(): void {
|
|
1152
|
+
const finished = this.data.tasks.filter((task) => TERMINAL_STATES.includes(task.state));
|
|
1153
|
+
if (finished.length === 0) return this.say("no finished tasks to archive", "warning");
|
|
1154
|
+
if (this.armed !== "archive-all") {
|
|
1155
|
+
this.armed = "archive-all";
|
|
1156
|
+
return this.say(`press A again to archive all ${finished.length} finished task${finished.length === 1 ? "" : "s"}`, "warning");
|
|
1157
|
+
}
|
|
1158
|
+
this.armed = undefined;
|
|
1159
|
+
const failed = finished.map((task) => this.host.archiveTask(task.id)).filter((notice) => !notice.startsWith("archived"));
|
|
1160
|
+
this.refreshData(true);
|
|
1161
|
+
this.say(failed.length === 0 ? `archived ${finished.length} finished task${finished.length === 1 ? "" : "s"} — v shows them` : `archived ${finished.length - failed.length}; ${failed[0]}`, failed.length === 0 ? "info" : "warning");
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1014
1164
|
/** Seats for the next round: the session's, or the ones chosen before it started. */
|
|
1015
1165
|
private seatSet(): Set<PanelMember> {
|
|
1016
1166
|
const session = this.host.planner();
|
|
@@ -1042,7 +1192,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1042
1192
|
session.send(body);
|
|
1043
1193
|
return;
|
|
1044
1194
|
}
|
|
1045
|
-
if (view.kind === "other") return this.say(this.host.
|
|
1195
|
+
if (view.kind === "other") return this.say(this.host.sendToSession(view.sessionId, body));
|
|
1196
|
+
if (view.kind === "idle") return this.say(this.host.sendToTask(view.taskId, body));
|
|
1046
1197
|
const notice = this.host.toOracle(body);
|
|
1047
1198
|
if (notice) this.say(notice);
|
|
1048
1199
|
}
|
|
@@ -1072,24 +1223,40 @@ export class LobbyView implements Component, Focusable {
|
|
|
1072
1223
|
auto: auto(zen.task),
|
|
1073
1224
|
waiting: 0,
|
|
1074
1225
|
}];
|
|
1226
|
+
const active = (sessionId: string | undefined) => (sessionId ? this.data.tasks.find((task) => task.ownerSessionId === sessionId && !TERMINAL_STATES.includes(task.state)) : undefined);
|
|
1075
1227
|
for (const session of background) {
|
|
1076
|
-
const task =
|
|
1228
|
+
const task = active(session.sessionId);
|
|
1077
1229
|
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 });
|
|
1230
|
+
entries.push({ view: { kind: "background", key: session.key }, name: session.name, where: "background", ...(task ? { task } : {}), status, auto: auto(task), waiting: session.dialogs.length, ...(session.sessionId ? { sessionId: session.sessionId } : {}) });
|
|
1231
|
+
}
|
|
1232
|
+
const running = new Set<string>([...(me ? [me] : []), ...started]);
|
|
1233
|
+
for (const live of this.host.liveSessions()) {
|
|
1234
|
+
if (running.has(live.sessionId)) continue;
|
|
1235
|
+
running.add(live.sessionId);
|
|
1236
|
+
const task = active(live.sessionId);
|
|
1237
|
+
entries.push({ view: { kind: "other", sessionId: live.sessionId }, name: live.name ?? task?.title ?? "unnamed session", where: "other terminal", ...(task ? { task } : {}), status: task?.state ?? "no task", auto: auto(task), waiting: 0, sessionId: live.sessionId, pid: live.pid });
|
|
1079
1238
|
}
|
|
1239
|
+
// Tasks under way whose session is not running (it ended, or none ever owned them): they can be resumed here.
|
|
1080
1240
|
for (const task of this.data.tasks) {
|
|
1081
|
-
if (TERMINAL_STATES.includes(task.state) ||
|
|
1082
|
-
entries.push({ view: { kind: "
|
|
1241
|
+
if (TERMINAL_STATES.includes(task.state) || (task.ownerSessionId && running.has(task.ownerSessionId))) continue;
|
|
1242
|
+
entries.push({ view: { kind: "idle", taskId: task.id }, name: task.title, where: "not running", task, status: task.state, auto: auto(task), waiting: 0, ...(task.ownerSessionId ? { sessionId: task.ownerSessionId } : {}) });
|
|
1083
1243
|
}
|
|
1084
1244
|
return entries;
|
|
1085
1245
|
}
|
|
1086
1246
|
|
|
1087
|
-
/**
|
|
1247
|
+
/**
|
|
1248
|
+
* The entry the Lobby tab shows. A task that was not running follows its
|
|
1249
|
+
* session once one picks it up; a session that is gone falls back to this
|
|
1250
|
+
* window.
|
|
1251
|
+
*/
|
|
1088
1252
|
viewedEntry(): SessionEntry {
|
|
1089
1253
|
const entries = this.sessionEntries();
|
|
1090
1254
|
const view = this.viewing;
|
|
1091
|
-
|
|
1092
|
-
|
|
1255
|
+
if (view.kind === "here") return entries[0]!;
|
|
1256
|
+
let found = entries.find((entry) => sameView(entry.view, view));
|
|
1257
|
+
if (!found && view.kind === "idle") found = entries.find((entry) => entry.task?.id === view.taskId);
|
|
1258
|
+
if (!found) this.viewing = { kind: "here" };
|
|
1259
|
+
else this.viewing = found.view;
|
|
1093
1260
|
return found ?? entries[0]!;
|
|
1094
1261
|
}
|
|
1095
1262
|
|
|
@@ -1113,7 +1280,32 @@ export class LobbyView implements Component, Focusable {
|
|
|
1113
1280
|
this.picking = true;
|
|
1114
1281
|
const entries = this.sessionEntries();
|
|
1115
1282
|
const current = this.viewedEntry();
|
|
1116
|
-
this.pickIndex = Math.max(0, entries.findIndex((entry) => entry.
|
|
1283
|
+
this.pickIndex = Math.max(0, entries.findIndex((entry) => sameView(entry.view, current.view)));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Run a session in this window: a background one is stopped and resumed
|
|
1288
|
+
* here, a task nobody runs is resumed (or claimed) here. A session running
|
|
1289
|
+
* in another terminal stays there — two pi processes must not write one
|
|
1290
|
+
* session.
|
|
1291
|
+
*/
|
|
1292
|
+
switchTo(entry: SessionEntry): void {
|
|
1293
|
+
if (entry.view.kind === "here") return this.say("this is this window's session");
|
|
1294
|
+
if (entry.where === "other terminal") return this.say(`${entry.name} is running in another terminal${entry.pid ? ` (pid ${entry.pid})` : ""} — switch there, or close it first and resume it here`, "warning");
|
|
1295
|
+
if (this.host.masterBusy()) return this.say("this window's oracle is working — esc stops it, then switch", "warning");
|
|
1296
|
+
const background = entry.view.kind === "background" ? this.host.sessions().find((session) => entry.view.kind === "background" && session.key === entry.view.key) : undefined;
|
|
1297
|
+
const target: SwitchTarget = {
|
|
1298
|
+
name: entry.name,
|
|
1299
|
+
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
|
|
1300
|
+
...(background ? { background } : {}),
|
|
1301
|
+
...(!entry.sessionId && entry.task ? { claimTaskId: entry.task.id } : {}),
|
|
1302
|
+
};
|
|
1303
|
+
this.picking = false;
|
|
1304
|
+
this.say(background ? `stopping ${entry.name}'s background process to resume it here…` : `switching this window to ${entry.name}…`);
|
|
1305
|
+
void this.host.switchTo(target).then((notice) => {
|
|
1306
|
+
this.say(notice);
|
|
1307
|
+
this.host.requestRender();
|
|
1308
|
+
});
|
|
1117
1309
|
}
|
|
1118
1310
|
|
|
1119
1311
|
/** Keys while the session switcher is open; true when the key was handled. */
|
|
@@ -1132,6 +1324,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
1132
1324
|
this.say(entry.view.kind === "here" ? "back to this window" : `showing ${entry.name} — you talk to its oracle from here`);
|
|
1133
1325
|
return true;
|
|
1134
1326
|
}
|
|
1327
|
+
if (data === "s" && entry) {
|
|
1328
|
+
this.switchTo(entry);
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1135
1331
|
if (data === "n" || action === "newSession") {
|
|
1136
1332
|
this.picking = false;
|
|
1137
1333
|
this.startNewSessionPrompt();
|
|
@@ -1254,7 +1450,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1254
1450
|
seated = !seats.delete(seat);
|
|
1255
1451
|
if (seated) seats.add(seat);
|
|
1256
1452
|
}
|
|
1257
|
-
this.say(`${MEMBER_LABELS[seat]} ${seated ? "joins" : "leaves
|
|
1453
|
+
this.say(`${MEMBER_LABELS[seat]} ${seated ? "joins the panel from the next round and sits every round" : "leaves the panel from the next round"}`);
|
|
1258
1454
|
return true;
|
|
1259
1455
|
}
|
|
1260
1456
|
if (data === "x" || (escape && session?.busy)) {
|
|
@@ -1466,7 +1662,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1466
1662
|
const names = new Map<string, string>();
|
|
1467
1663
|
for (const session of this.host.sessions()) if (session.sessionId && session.alive) names.set(session.sessionId, session.name);
|
|
1468
1664
|
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
|
|
1665
|
+
const live = new Set(this.host.liveSessions().map((session) => session.sessionId));
|
|
1666
|
+
const rows = taskRows(this.data.tasks, this.data.plans, this.host.sessionId(), this.now(), { names, auto, live, archived: this.data.archived });
|
|
1470
1667
|
return filterRows(rows, this.data.tasks, this.data.plans, this.queries.tasks);
|
|
1471
1668
|
}
|
|
1472
1669
|
|
|
@@ -1536,6 +1733,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1536
1733
|
const session = this.viewedSession();
|
|
1537
1734
|
if (session && !session.alive) return `${entry.name} has ended`;
|
|
1538
1735
|
if (session?.dialogs.length) return `press enter to answer ${entry.name}'s question, or type a message`;
|
|
1736
|
+
if (entry.where === "not running") return `leave ${entry.name} a message for when it resumes · esc, s resumes it here`;
|
|
1539
1737
|
return entry.where === "other terminal" ? `message ${entry.name}'s oracle (its own session delivers it)` : `message ${entry.name}'s oracle`;
|
|
1540
1738
|
}
|
|
1541
1739
|
if (!zen.task) return browsing ? "describe a task to start — i to type" : "describe a task to start · enter starts it";
|
|
@@ -1594,6 +1792,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1594
1792
|
return [
|
|
1595
1793
|
{ key: "type", text: "talk to the oracle of the session in view (in this window, starts a task when none is running)" },
|
|
1596
1794
|
{ key: "enter", text: "on an empty prompt: answer the question a background session waits on" },
|
|
1795
|
+
{ key: "s", text: "run the session in view in this window: a background one moves here, a task nobody runs resumes here" },
|
|
1597
1796
|
{ key: "↑ ↓", text: "scroll the focused pane (PageUp/PageDown a page)" },
|
|
1598
1797
|
{ key: "← →", text: "move between the conversation, activity log and thinking" },
|
|
1599
1798
|
{ key: "Home End", text: "the oldest lines, or back to the newest" },
|
|
@@ -1610,7 +1809,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
1610
1809
|
{ key: "n", text: "type a new task that starts in its own session" },
|
|
1611
1810
|
{ key: "o", text: "show the session driving the selected task in the Lobby tab" },
|
|
1612
1811
|
{ key: "x x", text: "stop the background session driving the selected task" },
|
|
1613
|
-
{ key: "d d", text: "discard the selected planned task" },
|
|
1812
|
+
{ key: "d d", text: "discard the selected planned task, or delete the selected task for good" },
|
|
1813
|
+
{ key: "a", text: "archive the selected task (a task under way is abandoned first: a twice), or restore an archived one" },
|
|
1814
|
+
{ key: "A A", text: "archive every finished task" },
|
|
1815
|
+
{ key: "v", text: "show or hide archived tasks" },
|
|
1614
1816
|
{ key: "r", text: "reread tasks from disk" },
|
|
1615
1817
|
];
|
|
1616
1818
|
case "plan":
|
|
@@ -1677,6 +1879,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1677
1879
|
private hintChips(): Array<[string, string]> {
|
|
1678
1880
|
const k = (action: LobbyAction) => keyLabel(this.keys[action]).toLowerCase();
|
|
1679
1881
|
const session = this.host.planner();
|
|
1882
|
+
if (this.picking) return [["↑↓", "browse"], ["enter", "view"], ["s", "switch here"], ["x x", "stop"], ["n", "new"], ["esc", "close"]];
|
|
1680
1883
|
if (this.mode === "type") {
|
|
1681
1884
|
if (this.tab === "lobby" && this.newSession) return [["enter", "start in a new session"], ["esc", "cancel"]];
|
|
1682
1885
|
if (this.tab === "lobby" && this.viewing.kind !== "here") {
|
|
@@ -1693,14 +1896,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
1693
1896
|
switch (this.tab) {
|
|
1694
1897
|
case "lobby":
|
|
1695
1898
|
keys.push(["←→", "pane"], ["↑↓", "scroll"], [k("sessions"), "sessions"], [k("newSession"), "new session"]);
|
|
1899
|
+
if (this.viewing.kind === "background" || this.viewing.kind === "idle") keys.push(["s", "switch here"]);
|
|
1696
1900
|
if (this.viewedEntry().task) keys.push([k("toggleAuto"), "auto"], ["c", "comment on plan"]);
|
|
1697
1901
|
break;
|
|
1698
1902
|
case "tasks": {
|
|
1699
1903
|
const row = this.taskRowList()[this.tasksSelected];
|
|
1700
1904
|
keys.push(["↑↓", "select"]);
|
|
1701
|
-
|
|
1905
|
+
const finished = row?.kind === "task" && TERMINAL_STATES.includes(row.status as Task["state"]);
|
|
1906
|
+
if (row?.kind === "task" && !finished) keys.push(["c", "comment"], ["o", "open its session"], [k("toggleAuto"), "auto"]);
|
|
1907
|
+
if (row?.kind === "task") keys.push(["a", "archive"], ["d d", "delete"]);
|
|
1908
|
+
if (row?.kind === "archived") keys.push(["a", "restore"], ["d d", "delete"]);
|
|
1702
1909
|
if (row?.kind === "plan") keys.push(["s", "start in a new session"], ["h", "start here"], ["d", "discard"]);
|
|
1703
|
-
keys.push(["n", "new task"]);
|
|
1910
|
+
keys.push(["v", this.showArchived ? "hide archived" : "archived"], ["n", "new task"]);
|
|
1704
1911
|
break;
|
|
1705
1912
|
}
|
|
1706
1913
|
case "plan":
|
|
@@ -1711,6 +1918,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1711
1918
|
break;
|
|
1712
1919
|
case "quickfix":
|
|
1713
1920
|
if (this.host.quickfix.jobs.length > 0) keys.push(["↑↓", "select"], ["x", "cancel"]);
|
|
1921
|
+
if (this.fixJobs()[this.fixSelected]?.status === "held") keys.push(["r", "run anyway"], ["t", "as a task"]);
|
|
1714
1922
|
keys.push(["m", "model"]);
|
|
1715
1923
|
break;
|
|
1716
1924
|
case "issues":
|
|
@@ -1781,6 +1989,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1781
1989
|
...(session ? { session: this.planView(session) } : {}),
|
|
1782
1990
|
profile: this.host.profileLabel("planner"),
|
|
1783
1991
|
seats: this.seatViews(undefined),
|
|
1992
|
+
limit: this.host.planningRounds?.() ?? 0,
|
|
1784
1993
|
offset: this.planOffset,
|
|
1785
1994
|
focus: this.planFocus,
|
|
1786
1995
|
draftOffset: this.planDraftOffset,
|
|
@@ -1813,7 +2022,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1813
2022
|
const groups = sortGroups(aggregateMetrics(records, this.metricsBy), this.metricsSort);
|
|
1814
2023
|
this.metricsSelected = Math.min(this.metricsSelected, Math.max(0, groups.length - 1));
|
|
1815
2024
|
const taskTimes = taskTimesByModel(this.data.tasks, records);
|
|
1816
|
-
|
|
2025
|
+
const jev = summarizeClassifier(this.data.classifierMetrics, records);
|
|
2026
|
+
return renderMetrics({ groups, taskTimes, records, stats: taskStats(this.data.tasks), by: this.metricsBy, sort: this.metricsSort, selected: this.metricsSelected, ...(query ? { query } : {}), panes: this.panes, ...(jev ? { classifier: jev } : {}) }, width, height, theme);
|
|
1817
2027
|
}
|
|
1818
2028
|
}
|
|
1819
2029
|
}
|
|
@@ -1844,6 +2054,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1844
2054
|
questions: session?.questions.filter((question) => question.from === MEMBER_LABELS[member]).length ?? 0,
|
|
1845
2055
|
ready: state?.reply?.status === "ready",
|
|
1846
2056
|
profile: this.host.seatLabel(member),
|
|
2057
|
+
...(!state && session?.satOut.has(member) ? { satOut: session.satOut.get(member)! } : {}),
|
|
2058
|
+
...(session?.pins.has(member) ? { pinned: true } : {}),
|
|
1847
2059
|
};
|
|
1848
2060
|
});
|
|
1849
2061
|
return [oracle, ...members];
|
|
@@ -1860,6 +2072,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1860
2072
|
...(session.step ? { step: session.step } : {}),
|
|
1861
2073
|
...(session.error ? { error: session.error } : {}),
|
|
1862
2074
|
turns: session.turns,
|
|
2075
|
+
limit: session.limit,
|
|
2076
|
+
nextMode: session.nextMode,
|
|
1863
2077
|
...(session.seed ? { seed: session.seed } : {}),
|
|
1864
2078
|
...(session.saved ? { saved: session.saved } : {}),
|
|
1865
2079
|
...(session.title ? { title: session.title } : {}),
|
|
@@ -1869,24 +2083,88 @@ export class LobbyView implements Component, Focusable {
|
|
|
1869
2083
|
};
|
|
1870
2084
|
}
|
|
1871
2085
|
|
|
1872
|
-
/**
|
|
2086
|
+
/**
|
|
2087
|
+
* The session browser: every session this window can show, grouped by
|
|
2088
|
+
* where it runs, with a preview of the one picked — its task, what it is
|
|
2089
|
+
* doing and the last of its conversation.
|
|
2090
|
+
*/
|
|
1873
2091
|
private pickerBody(width: number, height: number, theme: LobbyTheme): string[] {
|
|
1874
2092
|
const entries = this.sessionEntries();
|
|
1875
2093
|
this.pickIndex = Math.min(this.pickIndex, Math.max(0, entries.length - 1));
|
|
1876
|
-
const
|
|
1877
|
-
const
|
|
1878
|
-
const
|
|
1879
|
-
const lines =
|
|
2094
|
+
const wide = width >= 90;
|
|
2095
|
+
const listWidth = wide ? Math.max(38, Math.round((width - 1) * 0.42)) : width;
|
|
2096
|
+
const inner = listWidth - 4;
|
|
2097
|
+
const lines: string[] = [];
|
|
2098
|
+
let selectedLine = 0;
|
|
2099
|
+
let section: SessionWhere | undefined;
|
|
2100
|
+
const counts = new Map<SessionWhere, number>();
|
|
2101
|
+
for (const entry of entries) counts.set(entry.where, (counts.get(entry.where) ?? 0) + 1);
|
|
2102
|
+
entries.forEach((entry, index) => {
|
|
2103
|
+
if (entry.where !== section) {
|
|
2104
|
+
section = entry.where;
|
|
2105
|
+
if (lines.length > 0) lines.push("");
|
|
2106
|
+
lines.push(rule(inner, SECTION_OF[section], theme, String(counts.get(section) ?? 0)));
|
|
2107
|
+
}
|
|
1880
2108
|
const selected = index === this.pickIndex;
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
const
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1886
|
-
|
|
2109
|
+
if (selected) selectedLine = lines.length;
|
|
2110
|
+
const mark = paint(theme, entry.where === "this window" ? "accent" : entry.where === "not running" ? "dim" : "toolTitle", WHERE_MARKS[entry.where]);
|
|
2111
|
+
const name = selected ? bold(theme, entry.name) : entry.where === "not running" ? paint(theme, "muted", entry.name) : entry.name;
|
|
2112
|
+
const status = paint(theme, entry.status === "working" ? "accent" : entry.status === "ended" || entry.status === "no task" ? "dim" : "muted", entry.status.replace(/_/g, " "));
|
|
2113
|
+
const badges = [entry.auto ? paint(theme, "success", "⟳") : "", entry.waiting > 0 ? paint(theme, "warning", `● ${entry.waiting}`) : ""].filter(Boolean).join(" ");
|
|
2114
|
+
const head = `${selected ? paint(theme, "accent", "▸") : " "} ${mark} ${name}`;
|
|
2115
|
+
lines.push(selectRow(theme, spread(head, `${status}${badges ? ` ${badges}` : ""}`, inner), inner, selected, true));
|
|
1887
2116
|
});
|
|
1888
|
-
const
|
|
1889
|
-
|
|
2117
|
+
const rows = Math.max(0, height - 2);
|
|
2118
|
+
const start = windowStart(selectedLine, lines.length, rows);
|
|
2119
|
+
const list = box(listWidth, height, lines.slice(start), { title: "Sessions", right: `${entries.length}`, focused: true, scroll: { total: lines.length, start }, theme });
|
|
2120
|
+
if (!wide) return list;
|
|
2121
|
+
const previewWidth = width - 1 - listWidth;
|
|
2122
|
+
const picked = entries[this.pickIndex];
|
|
2123
|
+
const preview = picked ? this.sessionPreview(picked, previewWidth - 4, rows, theme) : [];
|
|
2124
|
+
return beside([list, box(previewWidth, height, preview, { title: "Preview", theme })]);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/** What the browser shows of one session: who and where, its task, what `enter` and `s` do, then the end of its conversation. */
|
|
2128
|
+
private sessionPreview(entry: SessionEntry, width: number, height: number, theme: LobbyTheme): string[] {
|
|
2129
|
+
const dot = paint(theme, "dim", " · ");
|
|
2130
|
+
const task = entry.task;
|
|
2131
|
+
const lines = [
|
|
2132
|
+
...wrapHanging(`${paint(theme, entry.where === "this window" ? "accent" : "toolTitle", WHERE_MARKS[entry.where])} `, bold(theme, entry.name), width),
|
|
2133
|
+
...wrapHanging(" ", [paint(theme, "muted", entry.where), paint(theme, entry.status === "working" ? "accent" : "muted", entry.status.replace(/_/g, " ")), entry.pid ? paint(theme, "dim", `pid ${entry.pid}`) : ""].filter(Boolean).join(dot), width),
|
|
2134
|
+
];
|
|
2135
|
+
if (task) {
|
|
2136
|
+
const progress = taskProgress(task);
|
|
2137
|
+
lines.push(...wrapHanging(" ", [paint(theme, "dim", task.id), progress ? `${pips(progress.done, progress.total, Math.min(progress.total, 8), theme)} ${paint(theme, "muted", `${progress.done}/${progress.total}`)}` : ""].filter(Boolean).join(dot), width));
|
|
2138
|
+
}
|
|
2139
|
+
const flags = [entry.auto ? paint(theme, "success", "⟳ auto mode") : "", entry.waiting > 0 ? paint(theme, "warning", `● ${entry.waiting} question${entry.waiting === 1 ? "" : "s"} waiting for you`) : ""].filter(Boolean);
|
|
2140
|
+
if (flags.length > 0) lines.push(...wrapHanging(" ", flags.join(dot), width));
|
|
2141
|
+
const key = (text: string) => paint(theme, "accent", text);
|
|
2142
|
+
const does: Record<SessionWhere, string> = {
|
|
2143
|
+
"this window": `${key("enter")} ${paint(theme, "muted", "back to this window")}`,
|
|
2144
|
+
background: `${key("enter")} ${paint(theme, "muted", "view it")} ${key("s")} ${paint(theme, "muted", "move it into this window")} ${key("x x")} ${paint(theme, "muted", "stop it")}`,
|
|
2145
|
+
"other terminal": `${key("enter")} ${paint(theme, "muted", "view it and message it")} ${paint(theme, "dim", "switch in its own terminal")}`,
|
|
2146
|
+
"not running": `${key("enter")} ${paint(theme, "muted", "view it")} ${key("s")} ${paint(theme, "muted", entry.sessionId ? "resume it in this window" : "take it over in this window")}`,
|
|
2147
|
+
};
|
|
2148
|
+
lines.push("", ...wrapHanging(" ", does[entry.where], width));
|
|
2149
|
+
const chat = this.previewChat(entry);
|
|
2150
|
+
const room = height - lines.length - 2;
|
|
2151
|
+
if (room > 2) {
|
|
2152
|
+
lines.push("", rule(width, "Conversation", theme));
|
|
2153
|
+
const talk = chat.length > 0 ? chatLines(chat, width, theme) : [paint(theme, "dim", "Nothing said yet.")];
|
|
2154
|
+
lines.push(...talk.slice(-(room - 1)));
|
|
2155
|
+
}
|
|
2156
|
+
return lines;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/** The conversation behind an entry: this window's feed, a background session's, or a saved session file. */
|
|
2160
|
+
private previewChat(entry: SessionEntry): readonly ChatEntry[] {
|
|
2161
|
+
if (entry.view.kind === "here") return this.host.feed.chat;
|
|
2162
|
+
if (entry.view.kind === "background") return this.viewedSessionFor(entry.view.key)?.feed.chat ?? [];
|
|
2163
|
+
return entry.sessionId ? this.host.sessionChat(entry.sessionId) : [];
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
private viewedSessionFor(key: string): BackgroundSession | undefined {
|
|
2167
|
+
return this.host.sessions().find((session) => session.key === key);
|
|
1890
2168
|
}
|
|
1891
2169
|
|
|
1892
2170
|
private homeBody(width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
@@ -1962,14 +2240,17 @@ export class LobbyView implements Component, Focusable {
|
|
|
1962
2240
|
emptyNote,
|
|
1963
2241
|
}, width, height, theme);
|
|
1964
2242
|
}
|
|
2243
|
+
const running = entry.where === "other terminal";
|
|
1965
2244
|
return renderHome({
|
|
1966
2245
|
...common,
|
|
1967
|
-
chat:
|
|
2246
|
+
chat: entry.sessionId ? this.host.sessionChat(entry.sessionId) : [],
|
|
1968
2247
|
activity: [],
|
|
1969
2248
|
thoughts: [],
|
|
1970
2249
|
busy: false,
|
|
1971
|
-
emptyNote: "Nothing said in this session yet.",
|
|
1972
|
-
activityNote:
|
|
2250
|
+
emptyNote: running ? "Nothing said in this session yet." : entry.sessionId ? "Nothing was said in its session." : "No session has driven this task yet.",
|
|
2251
|
+
activityNote: running
|
|
2252
|
+
? "This session runs in another terminal: its conversation shows here and your messages reach its oracle within seconds, but live activity only streams from sessions started in this window."
|
|
2253
|
+
: `No session is running this task. Press esc, then s to ${entry.sessionId ? "resume it" : "take it over"} in this window; a message you send now waits until a session picks it up.`,
|
|
1973
2254
|
}, width, height, theme);
|
|
1974
2255
|
}
|
|
1975
2256
|
|
|
@@ -1985,6 +2266,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
1985
2266
|
} else if (row?.kind === "plan") {
|
|
1986
2267
|
const plan = this.data.plans.find((entry) => entry.id === row.id);
|
|
1987
2268
|
if (plan) detail = planDetailLines(plan, detailWidth, now, theme);
|
|
2269
|
+
} else if (row?.kind === "archived") {
|
|
2270
|
+
const task = this.data.archived.find((entry) => entry.id === row.id);
|
|
2271
|
+
const key = (text: string) => paint(theme, "accent", text);
|
|
2272
|
+
if (task) {
|
|
2273
|
+
detail = [
|
|
2274
|
+
...taskDetailLines(task, [], this.host.sessionId(), detailWidth, now, theme).slice(0, 3),
|
|
2275
|
+
...wrapHanging(" ", paint(theme, "warning", `archived ${row.age === "now" ? "just now" : `${row.age} ago`}`), detailWidth),
|
|
2276
|
+
"",
|
|
2277
|
+
...wrapHanging(" ", `${key("a")} ${paint(theme, "muted", "restore it to the list")} ${key("d d")} ${paint(theme, "muted", "delete it for good")}`, detailWidth),
|
|
2278
|
+
...taskDetailLines(task, [], this.host.sessionId(), detailWidth, now, theme).slice(3),
|
|
2279
|
+
];
|
|
2280
|
+
}
|
|
1988
2281
|
}
|
|
1989
2282
|
const query = this.query();
|
|
1990
2283
|
return renderTasks({ rows, selected: this.tasksSelected, detail, focus: this.tasksFocus, detailOffset: this.tasksDetailOffset, ...(query ? { query } : {}), panes: this.panes }, width, height, theme);
|