@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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { shortDuration } from "../../text.ts";
|
|
10
10
|
import { tokens } from "../../pi/run-summary.ts";
|
|
11
|
-
import type { GroupBy, MetricGroup, MetricRecord, SortKey, TaskStats, TaskTimeGroup } from "../../state/metrics.ts";
|
|
11
|
+
import type { ClassifierSummary, GroupBy, MetricGroup, MetricRecord, SortKey, TaskStats, TaskTimeGroup } from "../../state/metrics.ts";
|
|
12
12
|
import { bar, beside, bold, box, fill, fit, meter, notePane, paint, selectRow, sparkline, stackedBar, windowStart, wrap, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
13
|
import { sourceColor } from "./home.ts";
|
|
14
14
|
|
|
@@ -25,6 +25,8 @@ export interface MetricsTabInput {
|
|
|
25
25
|
query?: string;
|
|
26
26
|
/** Filled with where the table landed, for the wheel. */
|
|
27
27
|
panes?: PaneLayout;
|
|
28
|
+
/** What the classifier did and spared, when it has run. */
|
|
29
|
+
classifier?: ClassifierSummary;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface Column {
|
|
@@ -230,6 +232,27 @@ export function timeShare(records: readonly MetricRecord[], taskTimes: readonly
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
/** The metrics dashboard: KPI tiles, the charts, and the full table filling what is left. */
|
|
235
|
+
/** Milliseconds as `180 ms` or `1.2s`: classifier calls are short. */
|
|
236
|
+
function millis(ms: number): string {
|
|
237
|
+
return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The classifier's box: its calls and speed, then what its decisions spared. */
|
|
241
|
+
export function classifierLines(summary: ClassifierSummary, width: number, theme?: LobbyTheme): string[] {
|
|
242
|
+
const inner = width - 4;
|
|
243
|
+
const purposes = Object.entries(summary.byPurpose).sort((a, b) => b[1] - a[1]).map(([purpose, count]) => `${purpose} ${count}`).join(" · ");
|
|
244
|
+
const calls = summary.calls > 0
|
|
245
|
+
? [`${summary.calls} call${summary.calls === 1 ? "" : "s"}`, `${percent(summary.ok, summary.calls)} ok`, `p50 ${millis(summary.p50Ms)}`, `p90 ${millis(summary.p90Ms)}`, summary.input > 0 ? `${tokens(summary.input)} tokens read` : "", purposes].filter(Boolean).join(paint(theme, "dim", " · "))
|
|
246
|
+
: paint(theme, "dim", "no calls recorded");
|
|
247
|
+
const spared = [
|
|
248
|
+
`${summary.seatRunsSkipped} seat run${summary.seatRunsSkipped === 1 ? "" : "s"} skipped`,
|
|
249
|
+
`${summary.questionsAnswered} question${summary.questionsAnswered === 1 ? "" : "s"} answered`,
|
|
250
|
+
`${summary.quickFixesHeld} quick fix${summary.quickFixesHeld === 1 ? "" : "es"} held`,
|
|
251
|
+
summary.routed > 0 ? `${summary.routed} run${summary.routed === 1 ? "" : "s"} routed down, ${percent(summary.routedOk, summary.routed)} ok` : "no runs routed",
|
|
252
|
+
].join(paint(theme, "dim", " · "));
|
|
253
|
+
return box(width, 4, [fit(calls, inner), fit(`${paint(theme, "success", "spared")} ${spared}`, inner)], { title: "Classifier (Jev)", theme });
|
|
254
|
+
}
|
|
255
|
+
|
|
233
256
|
export function renderMetrics(input: MetricsTabInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
234
257
|
if (height <= 0) return [];
|
|
235
258
|
if (input.records.length === 0) {
|
|
@@ -242,6 +265,10 @@ export function renderMetrics(input: MetricsTabInput, width: number, height: num
|
|
|
242
265
|
sections.push(...tileRow(input, width, theme));
|
|
243
266
|
room -= 4;
|
|
244
267
|
}
|
|
268
|
+
if (input.classifier && room >= 14) {
|
|
269
|
+
sections.push(...classifierLines(input.classifier, width, theme));
|
|
270
|
+
room -= 4;
|
|
271
|
+
}
|
|
245
272
|
const chartHeight = Math.min(input.groups.length + 2, 8);
|
|
246
273
|
if (room - chartHeight >= 9) {
|
|
247
274
|
if (width >= 100) {
|
package/src/lobby/tabs/plan.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* said the plan must respect.
|
|
8
8
|
*/
|
|
9
9
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
10
|
-
import { ORACLE_LABEL, type LineComment, type PanelNote, type PanelQuestion, type PlannerMessage, type PlannerReply, type PlannerSeed } from "../planner.ts";
|
|
10
|
+
import { ORACLE_LABEL, type LineComment, type PanelNote, type PanelQuestion, type PlannerMessage, type PlannerReply, type PlannerSeed, type RoundMode } from "../planner.ts";
|
|
11
11
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
12
12
|
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
13
|
import { sourceColor, speakerLine, tailWindow, youLines } from "./home.ts";
|
|
@@ -23,6 +23,10 @@ export interface SeatView {
|
|
|
23
23
|
ready: boolean;
|
|
24
24
|
/** `provider/model · thinking` the seat runs on. */
|
|
25
25
|
profile?: string;
|
|
26
|
+
/** The classifier left it out of the latest round: how likely it judged the seat needed. */
|
|
27
|
+
satOut?: number;
|
|
28
|
+
/** Seated with a key press: it sits every round, whatever the classifier says. */
|
|
29
|
+
pinned?: boolean;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface PlanView {
|
|
@@ -35,6 +39,10 @@ export interface PlanView {
|
|
|
35
39
|
step?: string;
|
|
36
40
|
error?: string;
|
|
37
41
|
turns: number;
|
|
42
|
+
/** Rounds before the oracle finalizes alone; 0 = unlimited. */
|
|
43
|
+
limit?: number;
|
|
44
|
+
/** How the next round will run under the limit. */
|
|
45
|
+
nextMode?: RoundMode;
|
|
38
46
|
seed?: PlannerSeed;
|
|
39
47
|
saved?: PlannedTask;
|
|
40
48
|
title?: string;
|
|
@@ -79,6 +87,8 @@ export interface PlanTabInput {
|
|
|
79
87
|
panes?: PaneLayout;
|
|
80
88
|
/** The key that saves the plan, as the status line names it. */
|
|
81
89
|
saveKey?: string;
|
|
90
|
+
/** The planning round limit before a session exists; 0 = unlimited. */
|
|
91
|
+
limit?: number;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
export const PLAN_COLUMNS_MIN = 100;
|
|
@@ -91,19 +101,28 @@ function intro(input: PlanTabInput, width: number, theme?: LobbyTheme): string[]
|
|
|
91
101
|
const model = seat.label === ORACLE_LABEL ? input.profile : seat.seated ? seat.profile ?? "" : "not seated";
|
|
92
102
|
return fit(`${name}${paint(theme, seat.seated ? "muted" : "dim", model)}`, width);
|
|
93
103
|
});
|
|
94
|
-
|
|
104
|
+
const bound = input.limit && input.limit > 0 ? ` (at most ${input.limit} rounds; the last one the oracle settles alone)` : "";
|
|
105
|
+
return [...wrap(bold(theme, `Describe a task below and the panel questions you until the plan is clear${bound}.`), width), "", ...rows];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `round 3/5`, `final round 5/5` or `round 6 · revising` under a limit; `round 3` without one. */
|
|
109
|
+
export function roundLabel(turns: number, limit = 0): string {
|
|
110
|
+
if (limit <= 0) return `round ${turns}`;
|
|
111
|
+
if (turns > limit) return `round ${turns} · past the limit, revising`;
|
|
112
|
+
return turns === limit ? `final round ${turns}/${limit}` : `round ${turns}/${limit}`;
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
function statusLine(view: PlanView, input: PlanTabInput, theme?: LobbyTheme): string {
|
|
98
116
|
const parts: string[] = [];
|
|
99
117
|
const count = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
100
|
-
if (view.busy) parts.push(`${paint(theme, "accent", spinner(input.tick))}
|
|
118
|
+
if (view.busy) parts.push(`${paint(theme, "accent", spinner(input.tick))} ${roundLabel(view.turns, view.limit)}`);
|
|
101
119
|
else if (view.error) parts.push(paint(theme, "error", `✗ ${view.error.split("\n")[0]} — r retries`));
|
|
102
120
|
else if (view.reply?.status === "ready") parts.push(paint(theme, "success", `✓ ready — ${input.saveKey ?? "Ctrl+S"} saves it`));
|
|
103
121
|
else if (view.awaitingAnswers) parts.push(paint(theme, "warning", `● ${count(view.questions.length, "question")} — enter ${view.answeredChunks > 0 ? "resumes" : "answers them"}`));
|
|
104
122
|
if (view.lineComments.length > 0) parts.push(paint(theme, "accent", `◆ ${count(view.lineComments.length, "comment")} to send`));
|
|
105
123
|
if (view.saved) parts.push(paint(theme, "success", `saved as ${view.saved.id}`));
|
|
106
|
-
if (!view.busy && view.turns > 0) parts.push(paint(theme, "dim",
|
|
124
|
+
if (!view.busy && view.turns > 0) parts.push(paint(theme, "dim", roundLabel(view.turns, view.limit)));
|
|
125
|
+
if (!view.busy && view.nextMode === "final") parts.push(paint(theme, "warning", "the next round is the last: the oracle settles the rest"));
|
|
107
126
|
return parts.join(paint(theme, "dim", " · "));
|
|
108
127
|
}
|
|
109
128
|
|
|
@@ -111,6 +130,7 @@ function statusLine(view: PlanView, input: PlanTabInput, theme?: LobbyTheme): st
|
|
|
111
130
|
function seatCell(seat: SeatView, tick: number, theme?: LobbyTheme): string {
|
|
112
131
|
const name = paint(theme, seat.seated ? sourceColor(seat.label) : "dim", seat.label);
|
|
113
132
|
if (!seat.seated) return `${name} ${paint(theme, "dim", "off")}`;
|
|
133
|
+
if (seat.satOut !== undefined && seat.status !== "thinking") return `${name} ${paint(theme, "dim", `sat out · ${seat.satOut.toFixed(2)}`)}`;
|
|
114
134
|
if (seat.status === "thinking") return `${name} ${paint(theme, "accent", spinner(tick))} ${paint(theme, "muted", seat.step ?? "thinking")}`;
|
|
115
135
|
if (seat.status === "failed") return `${name} ${paint(theme, "error", "✗ failed — r retries")}`;
|
|
116
136
|
if (seat.status === "idle") return `${name} ${paint(theme, "dim", "·")}`;
|
|
@@ -159,6 +179,9 @@ export function conversationLines(view: PlanView, width: number, theme?: LobbyTh
|
|
|
159
179
|
}
|
|
160
180
|
lines.push(speakerLine("panel", width, theme, time));
|
|
161
181
|
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) : wrap(message.text, width).map((line) => (line ? ` ${line}` : ""))));
|
|
182
|
+
for (const entry of message.decided ?? []) {
|
|
183
|
+
lines.push(...wrapHanging(` ${paint(theme, "success", "✓")} `, `${paint(theme, sourceColor(entry.from), `[${entry.from}]`)} ${entry.question} → ${bold(theme, entry.answer)} ${paint(theme, "dim", `· decided by the classifier (${entry.probability.toFixed(2)}); comment on the plan to overrule`)}`, width));
|
|
184
|
+
}
|
|
162
185
|
}
|
|
163
186
|
if (query && lines.length === 0) return wrap(paint(theme, "dim", `Nothing in the conversation matches "${query}".`), width);
|
|
164
187
|
return lines;
|
|
@@ -31,6 +31,7 @@ const STATUS_COLORS: Record<QuickFixJob["status"], "accent" | "success" | "error
|
|
|
31
31
|
failed: "error",
|
|
32
32
|
timeout: "error",
|
|
33
33
|
cancelled: "warning",
|
|
34
|
+
held: "warning",
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
function mark(job: QuickFixJob, tick: number, theme?: LobbyTheme): string {
|
|
@@ -38,6 +39,7 @@ function mark(job: QuickFixJob, tick: number, theme?: LobbyTheme): string {
|
|
|
38
39
|
if (job.status === "queued") return paint(theme, "muted", "…");
|
|
39
40
|
if (job.status === "success") return paint(theme, "success", "✓");
|
|
40
41
|
if (job.status === "cancelled") return paint(theme, "warning", "·");
|
|
42
|
+
if (job.status === "held") return paint(theme, "warning", "‖");
|
|
41
43
|
return paint(theme, "error", "✗");
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -60,8 +62,10 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
60
62
|
job.usage?.cost ? `$${job.usage.cost.toFixed(2)}` : "",
|
|
61
63
|
].filter(Boolean);
|
|
62
64
|
const lines = [paint(theme, "dim", job.id), facts.join(paint(theme, "dim", " · ")), "", ...wrap(bold(theme, job.prompt), width)];
|
|
65
|
+
if (job.route) lines.push("", ...wrap(paint(theme, "dim", `↓ routed ${job.route}`), width));
|
|
66
|
+
if (job.note) lines.push("", ...wrap(paint(theme, "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
63
67
|
lines.push("", rule(width, "Steps", theme));
|
|
64
|
-
if (job.steps.length === 0) lines.push(paint(theme, "dim", job.status === "queued" ? "Waiting for the quick fix ahead of it." : job.status === "running" ? `${spinner(tick)} starting…` : "No tool calls."));
|
|
68
|
+
if (job.steps.length === 0) lines.push(paint(theme, "dim", job.status === "queued" ? "Waiting for the quick fix ahead of it." : job.status === "running" ? `${spinner(tick)} starting…` : job.status === "held" ? "Not started." : "No tool calls."));
|
|
65
69
|
for (const step of job.steps) {
|
|
66
70
|
const lead = `${paint(theme, "dim", clock(step.at))} ${step.pending && job.status === "running" ? paint(theme, "accent", spinner(tick)) : paint(theme, "dim", "·")} `;
|
|
67
71
|
lines.push(...wrapHanging(lead, step.pending && job.status === "running" ? `${step.text}…` : paint(theme, "muted", step.text), width));
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -13,13 +13,14 @@ import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|
|
14
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
|
-
export type TaskSection = "mine" | "others" | "pending" | "recent";
|
|
16
|
+
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
17
17
|
|
|
18
18
|
/** How a row is ticked off: still to do (a plan, or a task under way), completed, or abandoned. */
|
|
19
19
|
export type CheckState = "open" | "done" | "dropped";
|
|
20
20
|
|
|
21
21
|
export interface TaskRow {
|
|
22
|
-
|
|
22
|
+
/** A task on the list, a saved plan, or a task in the archive. */
|
|
23
|
+
kind: "task" | "plan" | "archived";
|
|
23
24
|
id: string;
|
|
24
25
|
title: string;
|
|
25
26
|
section: TaskSection;
|
|
@@ -31,7 +32,7 @@ export interface TaskRow {
|
|
|
31
32
|
progress?: { done: number; total: number };
|
|
32
33
|
/** Who drives it, for tasks other sessions own. */
|
|
33
34
|
owner?: string;
|
|
34
|
-
/** How long ago it finished (finished tasks)
|
|
35
|
+
/** How long ago it finished (finished tasks), was saved (plans) or was archived, as `3h`. */
|
|
35
36
|
age?: string;
|
|
36
37
|
/** The GitHub issue a plan came from. */
|
|
37
38
|
issue?: number;
|
|
@@ -45,6 +46,10 @@ export interface RowContext {
|
|
|
45
46
|
names?: ReadonlyMap<string, string>;
|
|
46
47
|
/** Tasks with auto mode on. */
|
|
47
48
|
auto?: ReadonlySet<string>;
|
|
49
|
+
/** Sessions running now (from their heartbeats); a task whose owner is not among them says so. */
|
|
50
|
+
live?: ReadonlySet<string>;
|
|
51
|
+
/** Archived tasks, listed last when shown. */
|
|
52
|
+
archived?: readonly Task[];
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
export const SECTION_TITLES: Record<TaskSection, string> = {
|
|
@@ -52,6 +57,7 @@ export const SECTION_TITLES: Record<TaskSection, string> = {
|
|
|
52
57
|
others: "OTHER SESSIONS",
|
|
53
58
|
pending: "PENDING",
|
|
54
59
|
recent: "FINISHED",
|
|
60
|
+
archived: "ARCHIVED",
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
/** The box each row wears: empty while there is work to do, ticked when completed, crossed when abandoned. */
|
|
@@ -63,9 +69,6 @@ export function checkState(task: Task): CheckState {
|
|
|
63
69
|
return "open";
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
/** Finished tasks listed under RECENT. */
|
|
67
|
-
export const RECENT_LIMIT = 8;
|
|
68
|
-
|
|
69
72
|
/** Checklist progress as `3/7`, or undefined before a plan exists. */
|
|
70
73
|
export function taskProgress(task: Task): { done: number; total: number } | undefined {
|
|
71
74
|
if (!task.plan) return undefined;
|
|
@@ -74,13 +77,14 @@ export function taskProgress(task: Task): { done: number; total: number } | unde
|
|
|
74
77
|
return { done: steps.filter((step) => step.status === "done").length, total: steps.length };
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function ownerLabel(task: Task, sessionId: string | undefined, names?: ReadonlyMap<string, string>): string {
|
|
78
|
-
if (!task.ownerSessionId) return "
|
|
80
|
+
function ownerLabel(task: Task, sessionId: string | undefined, names?: ReadonlyMap<string, string>, live?: ReadonlySet<string>): string {
|
|
81
|
+
if (!task.ownerSessionId) return "not running";
|
|
79
82
|
if (task.ownerSessionId === sessionId) return "this session";
|
|
80
83
|
const name = names?.get(task.ownerSessionId);
|
|
81
84
|
// A background session is named after its task, so the row's title already says which one.
|
|
82
85
|
if (name) return name === task.title ? "background" : `background · ${name}`;
|
|
83
|
-
|
|
86
|
+
// An ended session's id tells nobody anything: say that nothing runs the task.
|
|
87
|
+
return live && !live.has(task.ownerSessionId) ? "not running" : `session ${task.ownerSessionId.slice(0, 8)}`;
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/** Rows in display order: this session's task, other sessions', pending plans, then recent finished tasks. */
|
|
@@ -98,7 +102,7 @@ export function taskRows(tasks: readonly Task[], plans: readonly PlannedTask[],
|
|
|
98
102
|
...(task.paused && section !== "recent" ? { paused: true } : {}),
|
|
99
103
|
check: checkState(task),
|
|
100
104
|
...(progress ? { progress } : {}),
|
|
101
|
-
...(section === "others" ? { owner: ownerLabel(task, sessionId, context.names) } : {}),
|
|
105
|
+
...(section === "others" ? { owner: ownerLabel(task, sessionId, context.names, context.live) } : {}),
|
|
102
106
|
...(section === "recent" ? { age: ago(now - Date.parse(task.updatedAt)) } : {}),
|
|
103
107
|
...(auto ? { auto: true } : {}),
|
|
104
108
|
};
|
|
@@ -116,7 +120,16 @@ export function taskRows(tasks: readonly Task[], plans: readonly PlannedTask[],
|
|
|
116
120
|
...(plan.createdAt ? { age: ago(now - Date.parse(plan.createdAt)) } : {}),
|
|
117
121
|
...(plan.issue ? { issue: plan.issue.number } : {}),
|
|
118
122
|
})),
|
|
119
|
-
...tasks.filter((task) => TERMINAL_STATES.includes(task.state)).
|
|
123
|
+
...tasks.filter((task) => TERMINAL_STATES.includes(task.state)).map((task) => row(task, "recent")),
|
|
124
|
+
...(context.archived ?? []).map((task): TaskRow => ({
|
|
125
|
+
kind: "archived",
|
|
126
|
+
id: task.id,
|
|
127
|
+
title: task.title,
|
|
128
|
+
section: "archived",
|
|
129
|
+
status: task.state,
|
|
130
|
+
check: checkState(task),
|
|
131
|
+
age: ago(now - Date.parse(task.archivedAt ?? task.updatedAt)),
|
|
132
|
+
})),
|
|
120
133
|
];
|
|
121
134
|
}
|
|
122
135
|
|
|
@@ -151,6 +164,7 @@ export function checkMark(row: Pick<TaskRow, "check" | "status" | "paused">, the
|
|
|
151
164
|
/** A title as its box says: struck through and dim once abandoned, bold while selected. */
|
|
152
165
|
function rowTitle(row: TaskRow, selected: boolean, theme?: LobbyTheme): string {
|
|
153
166
|
if (row.check === "dropped") return paint(theme, "dim", strike(theme, row.title));
|
|
167
|
+
if (row.kind === "archived") return selected ? bold(theme, row.title) : paint(theme, "dim", row.title);
|
|
154
168
|
if (row.check === "done") return selected ? bold(theme, row.title) : paint(theme, "muted", row.title);
|
|
155
169
|
return selected ? bold(theme, row.title) : row.title;
|
|
156
170
|
}
|
|
@@ -216,9 +230,11 @@ export function listLines(rows: readonly TaskRow[], selected: number, width: num
|
|
|
216
230
|
/** The list pane's right-hand count: what is open and what is finished, or how many rows match a search. */
|
|
217
231
|
export function listCount(rows: readonly TaskRow[], query?: string): string {
|
|
218
232
|
if (query) return `${rows.length} match${rows.length === 1 ? "" : "es"}`;
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
233
|
+
const listed = rows.filter((row) => row.kind !== "archived");
|
|
234
|
+
const open = listed.filter((row) => row.check === "open").length;
|
|
235
|
+
const finished = listed.length - open;
|
|
236
|
+
const archived = rows.length - listed.length;
|
|
237
|
+
return [open ? `${open} open` : "", finished ? `${finished} finished` : "", archived ? `${archived} archived` : ""].filter(Boolean).join(" · ") || "0";
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
const COMMENT_MARKS: Record<PlanComment["status"], string> = { open: "○", delivered: "◐", addressed: "✓" };
|