@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
|
@@ -73,16 +73,13 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
73
73
|
return lines;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Before the first fix: one sentence, then the agent and the model it runs on. */
|
|
76
77
|
function intro(input: QuickFixTabInput, width: number, theme?: LobbyTheme): string[] {
|
|
77
78
|
return [
|
|
78
|
-
bold(theme, "
|
|
79
|
+
...wrap(bold(theme, "Describe a small change below and one agent makes it now, beside any running task."), width),
|
|
79
80
|
"",
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
"Keep them small: a rename, a typo, a one-function fix. If it turns out bigger, the agent stops and says so, and you can plan it as a task.",
|
|
83
|
-
"",
|
|
84
|
-
paint(theme, "dim", `quick fix: ${input.profile} — esc, then m changes the model, thinking, time limit and instructions (alt+s: all settings)`),
|
|
85
|
-
].flatMap((line) => wrap(line, width));
|
|
81
|
+
`${paint(theme, "mdCode", "QUICK FIX".padEnd(11))}${paint(theme, "muted", input.profile)}`,
|
|
82
|
+
];
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
/** Jobs whose prompt, steps or report mention `query`. */
|
|
@@ -104,7 +101,7 @@ export function renderQuickFix(input: QuickFixTabInput, width: number, height: n
|
|
|
104
101
|
const jobs = newestFirst(filterJobs(input.jobs, input.query));
|
|
105
102
|
if (jobs.length === 0) {
|
|
106
103
|
const content = input.query && input.jobs.length > 0 ? [paint(theme, "dim", `No quick fix mentions "${input.query}".`)] : intro(input, width - 4, theme);
|
|
107
|
-
return box(width, height, content, { title: "Quick fix", right: input.profile, theme });
|
|
104
|
+
return box(width, height, content, { title: "Quick fix", ...(input.query ? { right: input.profile } : {}), theme });
|
|
108
105
|
}
|
|
109
106
|
const selected = Math.min(Math.max(0, input.selected), jobs.length - 1);
|
|
110
107
|
const { list: listWidth, detail: detailWidth, wide } = quickFixWidths(width);
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -11,10 +11,13 @@ import { planChecklist } from "../../pi/zen.ts";
|
|
|
11
11
|
import { persistedRuns } from "../../pi/ui.ts";
|
|
12
12
|
import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|
|
14
|
-
import { ago, beside, bold, box, detailWindow, fill, markdownLines, notePane, paint, position, rule, selectRow, since, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
14
|
+
import { ago, beside, bold, box, detailWindow, fill, markdownLines, notePane, paint, position, rule, selectRow, since, spread, strike, windowStart, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
15
15
|
|
|
16
16
|
export type TaskSection = "mine" | "others" | "pending" | "recent";
|
|
17
17
|
|
|
18
|
+
/** How a row is ticked off: still to do (a plan, or a task under way), completed, or abandoned. */
|
|
19
|
+
export type CheckState = "open" | "done" | "dropped";
|
|
20
|
+
|
|
18
21
|
export interface TaskRow {
|
|
19
22
|
kind: "task" | "plan";
|
|
20
23
|
id: string;
|
|
@@ -22,17 +25,44 @@ export interface TaskRow {
|
|
|
22
25
|
section: TaskSection;
|
|
23
26
|
/** Task state, or `pending` for a saved plan. */
|
|
24
27
|
status: string;
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
paused?: boolean;
|
|
29
|
+
check: CheckState;
|
|
30
|
+
/** Plan steps done, once the task has a plan. */
|
|
31
|
+
progress?: { done: number; total: number };
|
|
32
|
+
/** Who drives it, for tasks other sessions own. */
|
|
33
|
+
owner?: string;
|
|
34
|
+
/** How long ago it finished (finished tasks) or was saved (plans), as `3h`. */
|
|
35
|
+
age?: string;
|
|
36
|
+
/** The GitHub issue a plan came from. */
|
|
37
|
+
issue?: number;
|
|
38
|
+
/** Auto mode is on: the oracle drives it without asking. */
|
|
39
|
+
auto?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What the lobby knows about sessions beyond this one: background sessions' names, tasks in auto mode. */
|
|
43
|
+
export interface RowContext {
|
|
44
|
+
/** Background sessions this window started, by pi session id. */
|
|
45
|
+
names?: ReadonlyMap<string, string>;
|
|
46
|
+
/** Tasks with auto mode on. */
|
|
47
|
+
auto?: ReadonlySet<string>;
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
export const SECTION_TITLES: Record<TaskSection, string> = {
|
|
30
51
|
mine: "THIS SESSION",
|
|
31
52
|
others: "OTHER SESSIONS",
|
|
32
|
-
pending: "PENDING
|
|
33
|
-
recent: "
|
|
53
|
+
pending: "PENDING",
|
|
54
|
+
recent: "FINISHED",
|
|
34
55
|
};
|
|
35
56
|
|
|
57
|
+
/** The box each row wears: empty while there is work to do, ticked when completed, crossed when abandoned. */
|
|
58
|
+
export const CHECK_MARKS: Record<CheckState, string> = { open: "☐", done: "☑", dropped: "☒" };
|
|
59
|
+
|
|
60
|
+
export function checkState(task: Task): CheckState {
|
|
61
|
+
if (task.state === "completed") return "done";
|
|
62
|
+
if (task.state === "abandoned") return "dropped";
|
|
63
|
+
return "open";
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
/** Finished tasks listed under RECENT. */
|
|
37
67
|
export const RECENT_LIMIT = 8;
|
|
38
68
|
|
|
@@ -44,65 +74,153 @@ export function taskProgress(task: Task): { done: number; total: number } | unde
|
|
|
44
74
|
return { done: steps.filter((step) => step.status === "done").length, total: steps.length };
|
|
45
75
|
}
|
|
46
76
|
|
|
47
|
-
function ownerLabel(task: Task, sessionId: string | undefined): string {
|
|
77
|
+
function ownerLabel(task: Task, sessionId: string | undefined, names?: ReadonlyMap<string, string>): string {
|
|
48
78
|
if (!task.ownerSessionId) return "no owner";
|
|
49
79
|
if (task.ownerSessionId === sessionId) return "this session";
|
|
80
|
+
const name = names?.get(task.ownerSessionId);
|
|
81
|
+
// A background session is named after its task, so the row's title already says which one.
|
|
82
|
+
if (name) return name === task.title ? "background" : `background · ${name}`;
|
|
50
83
|
return `session ${task.ownerSessionId.slice(0, 8)}`;
|
|
51
84
|
}
|
|
52
85
|
|
|
53
86
|
/** Rows in display order: this session's task, other sessions', pending plans, then recent finished tasks. */
|
|
54
|
-
export function taskRows(tasks: readonly Task[], plans: readonly PlannedTask[], sessionId: string | undefined, now: number): TaskRow[] {
|
|
87
|
+
export function taskRows(tasks: readonly Task[], plans: readonly PlannedTask[], sessionId: string | undefined, now: number, context: RowContext = {}): TaskRow[] {
|
|
55
88
|
const active = tasks.filter((task) => !TERMINAL_STATES.includes(task.state));
|
|
56
89
|
const row = (task: Task, section: TaskSection): TaskRow => {
|
|
57
|
-
const progress = taskProgress(task);
|
|
58
|
-
const
|
|
59
|
-
return {
|
|
90
|
+
const progress = section === "recent" ? undefined : taskProgress(task);
|
|
91
|
+
const auto = section !== "recent" && context.auto?.has(task.id);
|
|
92
|
+
return {
|
|
93
|
+
kind: "task",
|
|
94
|
+
id: task.id,
|
|
95
|
+
title: task.title,
|
|
96
|
+
section,
|
|
97
|
+
status: task.state,
|
|
98
|
+
...(task.paused && section !== "recent" ? { paused: true } : {}),
|
|
99
|
+
check: checkState(task),
|
|
100
|
+
...(progress ? { progress } : {}),
|
|
101
|
+
...(section === "others" ? { owner: ownerLabel(task, sessionId, context.names) } : {}),
|
|
102
|
+
...(section === "recent" ? { age: ago(now - Date.parse(task.updatedAt)) } : {}),
|
|
103
|
+
...(auto ? { auto: true } : {}),
|
|
104
|
+
};
|
|
60
105
|
};
|
|
61
106
|
return [
|
|
62
107
|
...active.filter((task) => sessionId && task.ownerSessionId === sessionId).map((task) => row(task, "mine")),
|
|
63
108
|
...active.filter((task) => !sessionId || task.ownerSessionId !== sessionId).map((task) => row(task, "others")),
|
|
64
109
|
...plans.filter((plan) => plan.status === "pending").map((plan): TaskRow => ({
|
|
65
|
-
kind: "plan",
|
|
110
|
+
kind: "plan",
|
|
111
|
+
id: plan.id,
|
|
112
|
+
title: plan.title,
|
|
113
|
+
section: "pending",
|
|
114
|
+
status: "pending",
|
|
115
|
+
check: "open",
|
|
116
|
+
...(plan.createdAt ? { age: ago(now - Date.parse(plan.createdAt)) } : {}),
|
|
117
|
+
...(plan.issue ? { issue: plan.issue.number } : {}),
|
|
66
118
|
})),
|
|
67
119
|
...tasks.filter((task) => TERMINAL_STATES.includes(task.state)).slice(0, RECENT_LIMIT).map((task) => row(task, "recent")),
|
|
68
120
|
];
|
|
69
121
|
}
|
|
70
122
|
|
|
71
|
-
|
|
123
|
+
type StateColor = Extract<LobbyColor, "success" | "warning" | "error" | "accent" | "muted" | "dim">;
|
|
124
|
+
|
|
125
|
+
const STATE_COLORS: Record<string, StateColor> = {
|
|
72
126
|
completed: "success",
|
|
73
127
|
abandoned: "dim",
|
|
74
128
|
blocked: "error",
|
|
75
129
|
awaiting_approval: "warning",
|
|
130
|
+
planning: "accent",
|
|
76
131
|
implementing: "accent",
|
|
77
132
|
reviewing: "accent",
|
|
78
133
|
pending: "muted",
|
|
79
134
|
};
|
|
80
135
|
|
|
81
|
-
function statusColor(status: string):
|
|
82
|
-
return STATE_COLORS[status
|
|
136
|
+
function statusColor(status: string): StateColor {
|
|
137
|
+
return STATE_COLORS[status] ?? "muted";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A task's state in words: `awaiting approval`, `implementing · paused`. */
|
|
141
|
+
function stateWords(status: string, paused?: boolean): string {
|
|
142
|
+
return `${status.replace(/_/g, " ")}${paused ? " · paused" : ""}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The row's box, coloured by what it says: the state's colour while open, green when done, dim when dropped. */
|
|
146
|
+
export function checkMark(row: Pick<TaskRow, "check" | "status" | "paused">, theme?: LobbyTheme): string {
|
|
147
|
+
const color: StateColor = row.check === "done" ? "success" : row.check === "dropped" ? "dim" : row.paused ? "warning" : statusColor(row.status);
|
|
148
|
+
return paint(theme, color, CHECK_MARKS[row.check]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A title as its box says: struck through and dim once abandoned, bold while selected. */
|
|
152
|
+
function rowTitle(row: TaskRow, selected: boolean, theme?: LobbyTheme): string {
|
|
153
|
+
if (row.check === "dropped") return paint(theme, "dim", strike(theme, row.title));
|
|
154
|
+
if (row.check === "done") return selected ? bold(theme, row.title) : paint(theme, "muted", row.title);
|
|
155
|
+
return selected ? bold(theme, row.title) : row.title;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Progress as pips, `cells` wide: filled for the share done, hollow for the rest. */
|
|
159
|
+
export function pips(done: number, total: number, cells: number, theme?: LobbyTheme): string {
|
|
160
|
+
if (cells <= 0 || total <= 0) return "";
|
|
161
|
+
const filled = Math.min(cells, Math.round((Math.max(0, done) / total) * cells));
|
|
162
|
+
return `${paint(theme, done >= total ? "success" : "accent", "▰".repeat(filled))}${paint(theme, "borderMuted", "▱".repeat(cells - filled))}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Plan progress on a row: a pip per step (up to eight) and `3/5`. */
|
|
166
|
+
function progressText(progress: TaskRow["progress"], theme?: LobbyTheme): string {
|
|
167
|
+
if (!progress) return "";
|
|
168
|
+
return `${pips(progress.done, progress.total, Math.min(progress.total, 8), theme)} ${paint(theme, "muted", `${progress.done}/${progress.total}`)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The line under an open row's title: its state, who drives it and whether auto mode is on; a plan's age and issue. */
|
|
172
|
+
function detailsLine(row: TaskRow, theme?: LobbyTheme): string {
|
|
173
|
+
const dot = paint(theme, "dim", " · ");
|
|
174
|
+
if (row.kind === "plan") {
|
|
175
|
+
return [paint(theme, "muted", "planned"), row.age ? paint(theme, "dim", row.age === "now" ? "saved just now" : `saved ${row.age} ago`) : "", row.issue ? paint(theme, "dim", `#${row.issue}`) : ""].filter(Boolean).join(dot);
|
|
176
|
+
}
|
|
177
|
+
return [
|
|
178
|
+
paint(theme, row.paused ? "warning" : statusColor(row.status), stateWords(row.status, row.paused)),
|
|
179
|
+
row.auto ? paint(theme, "success", "⟳ auto") : "",
|
|
180
|
+
row.owner ? paint(theme, "dim", row.owner) : "",
|
|
181
|
+
].filter(Boolean).join(dot);
|
|
83
182
|
}
|
|
84
183
|
|
|
85
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* List lines with a rule over each section, plus which line holds the
|
|
186
|
+
* selected row. A task or plan still to do takes two lines — its box and
|
|
187
|
+
* title (with plan progress on the right), then its state, owner and auto
|
|
188
|
+
* mode; a finished one takes one, ticked or struck through, with its age.
|
|
189
|
+
*/
|
|
86
190
|
export function listLines(rows: readonly TaskRow[], selected: number, width: number, focused: boolean, theme?: LobbyTheme): { lines: string[]; selectedLine: number } {
|
|
87
191
|
const lines: string[] = [];
|
|
88
192
|
let selectedLine = 0;
|
|
89
193
|
let section: TaskSection | undefined;
|
|
194
|
+
const counts = new Map<TaskSection, number>();
|
|
195
|
+
for (const row of rows) counts.set(row.section, (counts.get(row.section) ?? 0) + 1);
|
|
90
196
|
rows.forEach((row, index) => {
|
|
91
197
|
if (row.section !== section) {
|
|
92
198
|
section = row.section;
|
|
93
199
|
if (lines.length > 0) lines.push("");
|
|
94
|
-
lines.push(
|
|
200
|
+
lines.push(rule(width, SECTION_TITLES[section], theme, String(counts.get(section) ?? 0)));
|
|
201
|
+
}
|
|
202
|
+
const chosen = index === selected;
|
|
203
|
+
const marker = chosen ? paint(theme, "accent", "▸") : " ";
|
|
204
|
+
const head = `${marker} ${checkMark(row, theme)} ${rowTitle(row, chosen, theme)}`;
|
|
205
|
+
if (chosen) selectedLine = lines.length;
|
|
206
|
+
if (row.check !== "open") {
|
|
207
|
+
lines.push(selectRow(theme, spread(head, row.age ? paint(theme, "dim", row.age) : "", width), width, chosen, focused));
|
|
208
|
+
return;
|
|
95
209
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const meta = row.meta ? ` ${paint(theme, "dim", row.meta)}` : "";
|
|
99
|
-
const title = index === selected ? bold(theme, row.title) : row.title;
|
|
100
|
-
if (index === selected) selectedLine = lines.length;
|
|
101
|
-
lines.push(selectRow(theme, `${marker}${title} ${paint(theme, "dim", "·")} ${status}${meta}`, width, index === selected, focused));
|
|
210
|
+
lines.push(selectRow(theme, spread(head, progressText(row.progress, theme), width), width, chosen, focused));
|
|
211
|
+
lines.push(selectRow(theme, ` ${detailsLine(row, theme)}`, width, chosen, focused));
|
|
102
212
|
});
|
|
103
213
|
return { lines, selectedLine };
|
|
104
214
|
}
|
|
105
215
|
|
|
216
|
+
/** The list pane's right-hand count: what is open and what is finished, or how many rows match a search. */
|
|
217
|
+
export function listCount(rows: readonly TaskRow[], query?: string): string {
|
|
218
|
+
if (query) return `${rows.length} match${rows.length === 1 ? "" : "es"}`;
|
|
219
|
+
const open = rows.filter((row) => row.check === "open").length;
|
|
220
|
+
const finished = rows.length - open;
|
|
221
|
+
return [open ? `${open} open` : "", finished ? `${finished} finished` : ""].filter(Boolean).join(" · ") || "0";
|
|
222
|
+
}
|
|
223
|
+
|
|
106
224
|
const COMMENT_MARKS: Record<PlanComment["status"], string> = { open: "○", delivered: "◐", addressed: "✓" };
|
|
107
225
|
const COMMENT_WORDS: Record<PlanComment["status"], string> = { open: "waiting for the owning session", delivered: "sent to the oracle", addressed: "plan amended" };
|
|
108
226
|
|
|
@@ -113,31 +231,44 @@ function section(title: string, width: number, theme?: LobbyTheme, right = ""):
|
|
|
113
231
|
/** Detail lines for a task: header, plan with progress, comments, amendments, waits and runs. */
|
|
114
232
|
export function taskDetailLines(task: Task, comments: readonly PlanComment[], sessionId: string | undefined, width: number, now: number, theme?: LobbyTheme): string[] {
|
|
115
233
|
const lines: string[] = [];
|
|
116
|
-
|
|
234
|
+
const check = checkState(task);
|
|
235
|
+
const row = { check, status: task.state, ...(task.paused ? { paused: true } : {}) };
|
|
236
|
+
const title = check === "dropped" ? paint(theme, "dim", strike(theme, task.title)) : bold(theme, task.title);
|
|
237
|
+
lines.push(...wrapHanging(`${checkMark(row, theme)} `, title, width));
|
|
238
|
+
const dot = paint(theme, "dim", " · ");
|
|
239
|
+
const finished = TERMINAL_STATES.includes(task.state);
|
|
117
240
|
const facts = [
|
|
118
|
-
paint(theme, statusColor(task.state),
|
|
119
|
-
ownerLabel(task, sessionId),
|
|
120
|
-
`started ${since(now - Date.parse(task.createdAt))}
|
|
121
|
-
task.
|
|
241
|
+
bold(theme, paint(theme, task.paused ? "warning" : statusColor(task.state), stateWords(task.state, task.paused))),
|
|
242
|
+
finished ? "" : paint(theme, "muted", ownerLabel(task, sessionId)),
|
|
243
|
+
paint(theme, "muted", `started ${since(now - Date.parse(task.createdAt))}`),
|
|
244
|
+
finished ? paint(theme, "muted", `${task.state === "abandoned" ? "dropped" : "done"} ${since(now - Date.parse(task.updatedAt))}`) : "",
|
|
122
245
|
].filter(Boolean);
|
|
123
|
-
lines.push(
|
|
246
|
+
lines.push(...wrapHanging(" ", facts.join(dot), width));
|
|
247
|
+
lines.push(...wrapHanging(" ", paint(theme, "dim", [task.id, task.domains.length > 0 ? task.domains.join(", ") : ""].filter(Boolean).join(" · ")), width));
|
|
248
|
+
const steps = task.plan ? planChecklist(task.plan, persistedRuns(task)) : [];
|
|
249
|
+
const done = steps.filter((step) => step.status === "done").length;
|
|
250
|
+
if (steps.length > 0) {
|
|
251
|
+
const cells = Math.max(4, Math.min(24, width - 20));
|
|
252
|
+
lines.push("", ` ${pips(done, steps.length, cells, theme)} ${paint(theme, "muted", `${done} of ${steps.length} steps`)}`);
|
|
253
|
+
}
|
|
124
254
|
const request = taskRequest(task);
|
|
125
255
|
if (request && request !== task.title) lines.push(...section("Request", width, theme), ...wrap(paint(theme, "muted", request), width));
|
|
126
256
|
if (task.plan) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
257
|
+
if (steps.length > 0) {
|
|
258
|
+
lines.push(...section("Progress", width, theme, `${done}/${steps.length} steps`));
|
|
259
|
+
for (const [index, step] of steps.entries()) {
|
|
260
|
+
const mark = step.status === "done" ? paint(theme, "success", CHECK_MARKS.done) : paint(theme, step.status === "current" ? "accent" : "dim", CHECK_MARKS.open);
|
|
261
|
+
const text = step.status === "done" ? paint(theme, "muted", step.text) : step.status === "current" ? bold(theme, step.text) : paint(theme, "muted", step.text);
|
|
262
|
+
const current = step.status === "current" ? ` ${paint(theme, "accent", "◂ now")}` : "";
|
|
263
|
+
lines.push(...wrapHanging(`${mark} ${paint(theme, "dim", `${index + 1}.`)} `, `${text}${current}`, width));
|
|
264
|
+
}
|
|
134
265
|
}
|
|
135
|
-
|
|
266
|
+
lines.push(...section("Approved plan", width, theme));
|
|
136
267
|
lines.push(...markdownLines(task.plan, width, theme));
|
|
137
268
|
} else if (task.proposal) {
|
|
138
|
-
lines.push(...section("Proposal (no plan yet)", width, theme), ...markdownLines(task.proposal, width, theme));
|
|
269
|
+
lines.push(...section(TERMINAL_STATES.includes(task.state) ? "Proposal" : "Proposal (no plan yet)", width, theme), ...markdownLines(task.proposal, width, theme));
|
|
139
270
|
} else {
|
|
140
|
-
lines.push(...section("Plan", width, theme), paint(theme, "dim", "No proposal or plan yet — the oracle is still clarifying or scouting."));
|
|
271
|
+
lines.push(...section("Plan", width, theme), paint(theme, "dim", TERMINAL_STATES.includes(task.state) ? "It ended before a plan was made." : "No proposal or plan yet — the oracle is still clarifying or scouting."));
|
|
141
272
|
}
|
|
142
273
|
const open = comments.filter((comment) => comment.status !== "addressed").length;
|
|
143
274
|
lines.push(...section("Comments", width, theme, open > 0 ? `${open} open` : ""));
|
|
@@ -166,13 +297,15 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
166
297
|
|
|
167
298
|
/** Detail lines for a saved plan waiting to start. */
|
|
168
299
|
export function planDetailLines(plan: PlannedTask, width: number, now: number, theme?: LobbyTheme): string[] {
|
|
300
|
+
const dot = paint(theme, "dim", " · ");
|
|
169
301
|
const lines = [
|
|
170
|
-
...
|
|
171
|
-
paint(theme, "
|
|
172
|
-
|
|
302
|
+
...wrapHanging(`${checkMark({ check: "open", status: "pending" }, theme)} `, bold(theme, plan.title), width),
|
|
303
|
+
...wrapHanging(" ", [bold(theme, paint(theme, "muted", "pending")), paint(theme, "muted", `saved ${since(now - Date.parse(plan.createdAt))}`)].join(dot), width),
|
|
304
|
+
...wrapHanging(" ", paint(theme, "dim", plan.id), width),
|
|
173
305
|
];
|
|
174
|
-
if (plan.issue) lines.push(...
|
|
175
|
-
|
|
306
|
+
if (plan.issue) lines.push(...wrapHanging(" ", `from issue #${plan.issue.number} — ${plan.issue.title}${plan.issue.url ? ` ${paint(theme, "dim", plan.issue.url)}` : ""}`, width));
|
|
307
|
+
const key = (text: string) => paint(theme, "accent", text);
|
|
308
|
+
lines.push("", ...wrapHanging(" ", [`${key("s")} ${paint(theme, "muted", "start it in a new session")}`, `${key("h")} ${paint(theme, "muted", "start it here")}`, `${key("d d")} ${paint(theme, "muted", "discard it")}`].join(paint(theme, "dim", " ")), width));
|
|
176
309
|
lines.push(...section("Agreed plan", width, theme), ...markdownLines(plan.brief, width, theme));
|
|
177
310
|
return lines;
|
|
178
311
|
}
|
|
@@ -230,7 +363,7 @@ export function renderTasks(input: TasksInput, width: number, height: number, th
|
|
|
230
363
|
const list = listLines(input.rows, input.selected, listWidth - 4, input.focus === "list", theme);
|
|
231
364
|
const rows = Math.max(0, bodyHeight - 2);
|
|
232
365
|
const listStart = windowStart(list.selectedLine, list.lines.length, rows);
|
|
233
|
-
const count = input.
|
|
366
|
+
const count = listCount(input.rows, input.query);
|
|
234
367
|
const listPane = box(listWidth, bodyHeight, list.lines.slice(listStart), { title: "Tasks", right: count, focused: input.focus === "list", scroll: { total: list.lines.length, start: listStart }, theme });
|
|
235
368
|
const detailStart = detailWindow(input.detail.length, rows, input.detailOffset);
|
|
236
369
|
const detailPane = box(detailWidth, bodyHeight, input.detail.slice(detailStart), { title: "Detail", ...(input.detail.length > rows ? { right: position(detailStart, rows, input.detail.length) } : {}), focused: input.focus === "detail", scroll: { total: input.detail.length, start: detailStart }, theme });
|