@a-t-h-i/bot-lobby 0.6.6 → 0.6.8
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 +183 -11
- package/package.json +1 -1
- package/prompts/master.md +21 -0
- package/prompts/panel.md +4 -0
- package/prompts/planner.md +5 -0
- package/prompts/pr-review.md +62 -0
- package/prompts/splitter.md +58 -0
- package/src/ask/dialog.ts +23 -7
- package/src/ask/state.ts +18 -3
- package/src/ask/tool.ts +9 -2
- package/src/ask/view.ts +8 -4
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -0
- package/src/execution/agent-runner.ts +23 -1
- package/src/execution/fallback.ts +75 -0
- package/src/execution/workspace.ts +185 -0
- package/src/knowledge/edit.ts +109 -0
- package/src/knowledge/notes.ts +143 -0
- package/src/knowledge/selector.ts +1 -1
- package/src/knowledge/store.ts +16 -2
- package/src/lobby/ask.ts +42 -21
- package/src/lobby/feed.ts +7 -0
- package/src/lobby/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +99 -1
- package/src/lobby/mini.ts +159 -0
- package/src/lobby/planner.ts +209 -10
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/quickfix.ts +17 -3
- package/src/lobby/runtime.ts +142 -17
- package/src/lobby/split.ts +230 -0
- package/src/lobby/tabs/git.ts +162 -0
- package/src/lobby/tabs/knowledge.ts +135 -0
- package/src/lobby/tabs/plan.ts +3 -1
- package/src/lobby/tabs/tasks.ts +11 -2
- package/src/lobby/view.ts +450 -35
- package/src/master/master.ts +40 -14
- package/src/master/research.ts +1 -0
- package/src/pi/commands.ts +12 -7
- package/src/pi/events.ts +2 -0
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +27 -3
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +104 -11
- package/src/pi/start-flags.ts +14 -0
- package/src/pi/start-task.ts +56 -5
- package/src/pi/tools.ts +4 -2
- package/src/schemas/configuration.ts +74 -11
- package/src/schemas/findings.ts +2 -0
- package/src/schemas/task.ts +15 -0
- package/src/state/backlog.ts +27 -4
- package/src/state/metrics.ts +2 -2
- package/src/state/persistence.ts +5 -5
- package/src/text.ts +38 -0
- package/src/workflow/workflow.ts +23 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Git tab: the repository's open pull requests through `gh`, the selected
|
|
3
|
+
* one with its checks, files, description and discussion, and — the point of
|
|
4
|
+
* the tab — a review of it by a read-only agent (v, or f with a focus), or a
|
|
5
|
+
* quick read by Jev (t), shown right under its facts.
|
|
6
|
+
*/
|
|
7
|
+
import { textWidth } from "../../width.ts";
|
|
8
|
+
import type { PullDetail, PullSummary } from "../pulls.ts";
|
|
9
|
+
import { isStale, type PullReadState, type PullReview, type Verdict } from "../pr-review.ts";
|
|
10
|
+
import { BRANCH_GLYPH, bold, columns, fill, markdownLines, paint, rule, selectRow, since, spinner, split, spread, windowStart, wrap, wrapHanging, type LobbyTheme } from "../layout.ts";
|
|
11
|
+
|
|
12
|
+
export interface GitTabInput {
|
|
13
|
+
pulls: readonly PullSummary[];
|
|
14
|
+
selected: number;
|
|
15
|
+
detail?: PullDetail;
|
|
16
|
+
/** The agent's review of the selected pull request, and Jev's read of it. */
|
|
17
|
+
review?: PullReview;
|
|
18
|
+
read?: PullReadState;
|
|
19
|
+
focus: "list" | "detail";
|
|
20
|
+
detailOffset: number;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
loaded: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
tick: number;
|
|
25
|
+
now: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GIT_COLUMNS_MIN = 90;
|
|
29
|
+
|
|
30
|
+
const VERDICTS: Record<Verdict, { words: string; color: "success" | "error" | "warning" }> = {
|
|
31
|
+
approve: { words: "approve", color: "success" },
|
|
32
|
+
changes: { words: "request changes", color: "error" },
|
|
33
|
+
comment: { words: "comment", color: "warning" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** `✓` passing, `✗` failing, `●` still running, blank without checks. */
|
|
37
|
+
function checkMark(pull: PullSummary, theme?: LobbyTheme): string {
|
|
38
|
+
if (pull.checks === "passing") return paint(theme, "success", "✓");
|
|
39
|
+
if (pull.checks === "failing") return paint(theme, "error", "✗");
|
|
40
|
+
if (pull.checks === "pending") return paint(theme, "warning", "●");
|
|
41
|
+
return " ";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The size of a change, `+12 −3`, added lines green and removed ones red. */
|
|
45
|
+
export function changeSize(additions: number, deletions: number, theme?: LobbyTheme): string {
|
|
46
|
+
return `${paint(theme, "success", `+${additions}`)} ${paint(theme, "error", `−${deletions}`)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function decisionWords(decision: string | undefined): { words: string; color: "success" | "error" | "warning" } | undefined {
|
|
50
|
+
if (decision === "APPROVED") return { words: "approved", color: "success" };
|
|
51
|
+
if (decision === "CHANGES_REQUESTED") return { words: "changes requested", color: "error" };
|
|
52
|
+
if (decision === "REVIEW_REQUIRED") return { words: "review required", color: "warning" };
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** How long a review took, `2m 05s`. */
|
|
57
|
+
function took(review: PullReview, now: number): string {
|
|
58
|
+
const ms = Math.max(0, (review.finishedAt ?? now) - review.startedAt);
|
|
59
|
+
const seconds = Math.round(ms / 1000);
|
|
60
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The review of the selected pull request: running steps, or the finished review, or why it failed. */
|
|
64
|
+
export function reviewLines(review: PullReview, detail: PullDetail | undefined, width: number, tick: number, now: number, theme?: LobbyTheme): string[] {
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
const verdict = review.verdict ? VERDICTS[review.verdict] : undefined;
|
|
67
|
+
// Short enough to fit beside the title in a narrow pane; the model goes on a line of its own.
|
|
68
|
+
const right = review.status === "running"
|
|
69
|
+
? `${spinner(tick)} ${took(review, now)}`
|
|
70
|
+
: [verdict ? paint(theme, verdict.color, verdict.words) : "", review.status === "done" ? took(review, now) : review.status].filter(Boolean).join(paint(theme, "dim", " · "));
|
|
71
|
+
lines.push(rule(width, "Review", theme, right));
|
|
72
|
+
if (review.model) lines.push(paint(theme, "dim", [review.model, review.thinking].filter(Boolean).join(" · ")));
|
|
73
|
+
if (review.focus) lines.push(...wrapHanging(paint(theme, "dim", "focus: "), review.focus, width));
|
|
74
|
+
if (review.saved) lines.push(paint(theme, "dim", `kept from ${since(now - review.startedAt)}`));
|
|
75
|
+
if (isStale(review, detail)) lines.push(paint(theme, "warning", "the pull request has new commits since this review — v reviews it again"));
|
|
76
|
+
if (review.status === "running") {
|
|
77
|
+
for (const step of review.steps.slice(-6)) lines.push(...wrapHanging(paint(theme, "dim", "· "), paint(theme, "muted", step), width));
|
|
78
|
+
lines.push(paint(theme, "dim", "x stops it"));
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
if (review.status === "cancelled") lines.push(paint(theme, "warning", "stopped"));
|
|
82
|
+
if (review.status === "failed" || review.status === "timeout") lines.push(...wrap(paint(theme, "error", `✗ ${review.error ?? review.status}`), width));
|
|
83
|
+
if (review.text) lines.push(...markdownLines(review.text, width, theme));
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Jev's read: one line, or the reason there is none. */
|
|
88
|
+
export function readLines(read: PullReadState, width: number, tick: number, theme?: LobbyTheme): string[] {
|
|
89
|
+
if (read.status === "running") return [rule(width, "Jev's read", theme, spinner(tick)), paint(theme, "dim", "reading the diff…")];
|
|
90
|
+
if (read.status === "failed") return [rule(width, "Jev's read", theme), ...wrap(paint(theme, "warning", read.error ?? "Jev could not read it"), width)];
|
|
91
|
+
return [rule(width, "Jev's read", theme, read.read ? `${read.read.model} · ${read.read.ms} ms` : ""), ...wrap(read.line ?? "", width)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function pullDetailLines(pull: PullDetail, review: PullReview | undefined, read: PullReadState | undefined, width: number, tick: number, now: number, theme?: LobbyTheme): string[] {
|
|
95
|
+
const dot = paint(theme, "dim", " · ");
|
|
96
|
+
const decision = decisionWords(pull.decision);
|
|
97
|
+
const facts = [
|
|
98
|
+
pull.state ? paint(theme, pull.state === "OPEN" ? "success" : "dim", pull.draft ? "draft" : pull.state.toLowerCase()) : pull.draft ? paint(theme, "dim", "draft") : "",
|
|
99
|
+
pull.author ? `by ${pull.author}` : "",
|
|
100
|
+
pull.updatedAt ? `updated ${since(now - Date.parse(pull.updatedAt))}` : "",
|
|
101
|
+
].filter(Boolean);
|
|
102
|
+
const checks = pull.checks ? `checks ${paint(theme, pull.checks === "passing" ? "success" : pull.checks === "failing" ? "error" : "warning", pull.checks)} (${pull.checkCount})` : "";
|
|
103
|
+
const merge = pull.mergeable ? (pull.mergeable === "CONFLICTING" ? paint(theme, "error", "conflicts") : pull.mergeable === "MERGEABLE" ? "mergeable" : "") : "";
|
|
104
|
+
const lines = [
|
|
105
|
+
...wrap(bold(theme, `#${pull.number} ${pull.title}`), width),
|
|
106
|
+
facts.join(dot),
|
|
107
|
+
`${paint(theme, "accent", `${BRANCH_GLYPH} ${pull.headRef || "?"}`)} → ${pull.baseRef || "?"}${dot}${changeSize(pull.additions, pull.deletions, theme)}${dot}${pull.changedFiles} file${pull.changedFiles === 1 ? "" : "s"}`,
|
|
108
|
+
[checks, decision ? paint(theme, decision.color, decision.words) : "", merge, pull.labels.length > 0 ? paint(theme, "warning", pull.labels.join(", ")) : ""].filter(Boolean).join(dot),
|
|
109
|
+
].filter((line) => line.length > 0);
|
|
110
|
+
if (pull.url) lines.push(paint(theme, "dim", pull.url));
|
|
111
|
+
lines.push("", ...wrap(paint(theme, "dim", "v reviews it with an agent · f with a focus you type · t is Jev's quick read"), width));
|
|
112
|
+
if (read) lines.push("", ...readLines(read, width, tick, theme));
|
|
113
|
+
if (review) lines.push("", ...reviewLines(review, pull, width, tick, now, theme));
|
|
114
|
+
if (pull.files.length > 0) {
|
|
115
|
+
lines.push("", rule(width, "Files", theme, String(pull.files.length)));
|
|
116
|
+
const numbers = pull.files.map((file) => textWidth(changeSize(file.additions, file.deletions)));
|
|
117
|
+
const room = Math.max(...numbers);
|
|
118
|
+
for (const file of pull.files) lines.push(spread(file.path, `${" ".repeat(room - textWidth(changeSize(file.additions, file.deletions)))}${changeSize(file.additions, file.deletions, theme)}`, width));
|
|
119
|
+
}
|
|
120
|
+
lines.push("", rule(width, "Description", theme), ...(pull.body.trim() ? markdownLines(pull.body.trim(), width, theme) : [paint(theme, "dim", "(no description)")]));
|
|
121
|
+
for (const note of pull.notes) {
|
|
122
|
+
const verdict = note.state && note.state !== "COMMENTED" ? paint(theme, note.state === "APPROVED" ? "success" : note.state === "CHANGES_REQUESTED" ? "error" : "warning", note.state.toLowerCase().replace(/_/g, " ")) : "";
|
|
123
|
+
lines.push("", rule(width, note.author ?? "comment", theme, [verdict, note.at ? since(now - Date.parse(note.at)) : ""].filter(Boolean).join(" · ")), ...(note.body.trim() ? markdownLines(note.body.trim(), width, theme) : []));
|
|
124
|
+
}
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function empty(input: GitTabInput, width: number, theme?: LobbyTheme): string[] {
|
|
129
|
+
if (input.loading) return [`${paint(theme, "accent", spinner(input.tick))} loading pull requests from GitHub…`];
|
|
130
|
+
if (input.error) return [];
|
|
131
|
+
if (!input.loaded) return wrap(paint(theme, "dim", "Press r to load open pull requests with the GitHub CLI (gh)."), width);
|
|
132
|
+
return wrap(paint(theme, "dim", "No open pull requests."), width);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One row of the list: number, checks, title, and the review's outcome when there is one. */
|
|
136
|
+
function row(pull: PullSummary, width: number, selected: boolean, focused: boolean, review: PullReview | undefined, theme?: LobbyTheme): string {
|
|
137
|
+
const verdict = review?.status === "done" && review.verdict ? paint(theme, VERDICTS[review.verdict].color, review.verdict === "approve" ? "✓ reviewed" : review.verdict === "changes" ? "✗ reviewed" : "◆ reviewed") : review?.status === "running" ? paint(theme, "accent", "reviewing") : "";
|
|
138
|
+
const title = selected ? bold(theme, pull.title) : pull.title;
|
|
139
|
+
const draft = pull.draft ? `${paint(theme, "dim", "draft")} ` : "";
|
|
140
|
+
const head = `${paint(theme, "dim", `#${pull.number}`)} ${checkMark(pull, theme)} ${draft}${title}`;
|
|
141
|
+
return selectRow(theme, spread(head, [verdict, changeSize(pull.additions, pull.deletions, theme)].filter(Boolean).join(" "), width), width, selected, focused);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderGit(input: GitTabInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
145
|
+
if (height <= 0) return [];
|
|
146
|
+
const banner = input.error ? wrap(paint(theme, "error", `✗ ${input.error}`), width) : [];
|
|
147
|
+
const bodyHeight = Math.max(0, height - banner.length);
|
|
148
|
+
if (input.pulls.length === 0) return fill([...banner, rule(width, "Pull requests", theme), ...empty(input, width, theme)], height, width);
|
|
149
|
+
const selected = Math.min(Math.max(0, input.selected), input.pulls.length - 1);
|
|
150
|
+
const wide = width >= GIT_COLUMNS_MIN;
|
|
151
|
+
const [listWidth, detailWidth] = wide ? split(width, 0.4, 3, 40) : [width, width];
|
|
152
|
+
const rows = input.pulls.map((pull, index) => row(pull, listWidth, index === selected, input.focus === "list", pull.number === input.review?.number ? input.review : undefined, theme));
|
|
153
|
+
const right = input.loading ? `${spinner(input.tick)} refreshing` : `${input.pulls.length} open`;
|
|
154
|
+
const listPane = fill([rule(listWidth, "Pull requests", theme, right), ...rows.slice(windowStart(selected, rows.length, bodyHeight - 1))], bodyHeight);
|
|
155
|
+
const current = input.pulls[selected]!;
|
|
156
|
+
const detail = input.detail && input.detail.number === current.number
|
|
157
|
+
? pullDetailLines(input.detail, input.review, input.read, detailWidth, input.tick, input.now, theme)
|
|
158
|
+
: [`${paint(theme, "accent", spinner(input.tick))} loading #${current.number}…`];
|
|
159
|
+
const detailPane = fill([rule(detailWidth, input.focus === "detail" ? "Pull request ◂" : "Pull request", theme), ...detail.slice(Math.max(0, Math.min(input.detailOffset, detail.length - 1)))], bodyHeight);
|
|
160
|
+
if (!wide) return fill([...banner, ...(input.focus === "detail" ? detailPane : listPane)], height, width);
|
|
161
|
+
return fill([...banner, ...columns(listPane, detailPane, listWidth, detailWidth, " │ ", theme)], height, width);
|
|
162
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Knowledge tab: everything each agent knows about the project — its
|
|
3
|
+
* knowledge, standards, decisions and completed tasks — with the file on the
|
|
4
|
+
* left and its entries on the right, one of them picked. The picked entry can
|
|
5
|
+
* be edited, deleted or commented on; the notes sit right under the entries
|
|
6
|
+
* they are about, as every agent reads them.
|
|
7
|
+
*/
|
|
8
|
+
import type { KnowledgeNote } from "../../knowledge/notes.ts";
|
|
9
|
+
import { AGENT_DIR_NAMES, type KnowledgeAgent } from "../../knowledge/paths.ts";
|
|
10
|
+
import { AGENT_LABELS, type KnowledgeFileInfo, type KnowledgeView } from "../knowledge.ts";
|
|
11
|
+
import { bold, columns, fill, markdownLines, paint, rule, selectRow, spread, split, windowStart, wrap, wrapHanging, type LobbyTheme } from "../layout.ts";
|
|
12
|
+
|
|
13
|
+
export interface KnowledgeLayout {
|
|
14
|
+
/** The first line of the entries shown, as the last frame drew them. */
|
|
15
|
+
offset: number;
|
|
16
|
+
/** How many lines the entries take, and how many the pane shows. */
|
|
17
|
+
total: number;
|
|
18
|
+
rows: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface KnowledgeTabInput {
|
|
22
|
+
files: readonly KnowledgeFileInfo[];
|
|
23
|
+
selected: number;
|
|
24
|
+
/** The selected file, read. */
|
|
25
|
+
view?: KnowledgeView;
|
|
26
|
+
/** The picked entry, an index into the file's entries. */
|
|
27
|
+
cursor: number;
|
|
28
|
+
focus: "list" | "detail";
|
|
29
|
+
/** The first line to show, unless the picked entry needs another. */
|
|
30
|
+
offset: number;
|
|
31
|
+
layout: KnowledgeLayout;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const KNOWLEDGE_COLUMNS_MIN = 90;
|
|
35
|
+
|
|
36
|
+
/** `812`, `1.2k`, `24k`. */
|
|
37
|
+
export function sizeWords(chars: number): string {
|
|
38
|
+
if (chars < 1000) return String(chars);
|
|
39
|
+
const thousands = chars / 1000;
|
|
40
|
+
return `${thousands >= 10 ? Math.round(thousands) : thousands.toFixed(1)}k`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fileRow(info: KnowledgeFileInfo, width: number, selected: boolean, focused: boolean, theme?: LobbyTheme): string {
|
|
44
|
+
const facts = [
|
|
45
|
+
info.over ? paint(theme, "warning", `${sizeWords(info.chars)} · over`) : paint(theme, "dim", sizeWords(info.chars)),
|
|
46
|
+
info.notes > 0 ? paint(theme, "accent", `✎ ${info.notes}`) : "",
|
|
47
|
+
].filter(Boolean).join(" ");
|
|
48
|
+
return selectRow(theme, spread(selected ? bold(theme, info.label) : info.label, facts, width - 2), width, selected, focused);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The files, grouped by agent, the selected one in view. */
|
|
52
|
+
export function listLines(files: readonly KnowledgeFileInfo[], selected: number, width: number, height: number, focused: boolean, theme?: LobbyTheme): string[] {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
let at = 0;
|
|
55
|
+
let previous: KnowledgeAgent | undefined;
|
|
56
|
+
files.forEach((info, index) => {
|
|
57
|
+
if (info.agent !== previous) {
|
|
58
|
+
previous = info.agent;
|
|
59
|
+
if (lines.length > 0) lines.push("");
|
|
60
|
+
const notes = files.filter((file) => file.agent === info.agent).reduce((total, file) => total + file.notes, 0);
|
|
61
|
+
lines.push(rule(width, AGENT_LABELS[info.agent], theme, notes > 0 ? `✎ ${notes}` : ""));
|
|
62
|
+
}
|
|
63
|
+
if (index === selected) at = lines.length;
|
|
64
|
+
lines.push(fileRow(info, width, index === selected, focused, theme));
|
|
65
|
+
});
|
|
66
|
+
return lines.slice(windowStart(at, lines.length, height));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One note under its entry. */
|
|
70
|
+
function noteLines(note: KnowledgeNote, width: number, theme?: LobbyTheme, about?: string): string[] {
|
|
71
|
+
const lead = paint(theme, "accent", " ✎ ");
|
|
72
|
+
const text = about ? `about “${about.replace(/\s+/g, " ").trim().slice(0, 50)}”: ${note.text}` : note.text;
|
|
73
|
+
return wrapHanging(lead, paint(theme, "muted", text), width);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The entries of the open file, each drawn once, with where the picked one sits. */
|
|
77
|
+
export function entryLines(view: KnowledgeView, cursor: number, width: number, focused: boolean, theme?: LobbyTheme): { lines: string[]; from: number; to: number } {
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
let from = 0;
|
|
80
|
+
let to = 0;
|
|
81
|
+
let previousEnd = 0;
|
|
82
|
+
view.entries.forEach((entry, index) => {
|
|
83
|
+
if (index > 0 && entry.start > previousEnd) lines.push("");
|
|
84
|
+
previousEnd = entry.end;
|
|
85
|
+
const picked = index === cursor;
|
|
86
|
+
const body = markdownLines(entry.text, Math.max(8, width - 2), theme);
|
|
87
|
+
if (picked) from = lines.length;
|
|
88
|
+
body.forEach((line, row) => {
|
|
89
|
+
const mark = picked ? paint(theme, "accent", row === 0 ? "▸ " : " ") : " ";
|
|
90
|
+
lines.push(picked ? selectRow(theme, `${mark}${line}`, width, true, focused) : `${mark}${line}`);
|
|
91
|
+
});
|
|
92
|
+
for (const note of view.attached.filter((candidate) => candidate.entry === entry.text)) lines.push(...noteLines(note, width, theme));
|
|
93
|
+
if (picked) to = lines.length;
|
|
94
|
+
});
|
|
95
|
+
if (view.detached.length > 0) {
|
|
96
|
+
lines.push("", rule(width, "Notes on entries that changed", theme, String(view.detached.length)));
|
|
97
|
+
for (const note of view.detached) lines.push(...noteLines(note, width, theme, note.entry));
|
|
98
|
+
}
|
|
99
|
+
return { lines, from, to };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderKnowledge(input: KnowledgeTabInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
103
|
+
if (height <= 0) return [];
|
|
104
|
+
if (input.files.length === 0) return fill([rule(width, "Knowledge", theme), ...wrap(paint(theme, "dim", "No knowledge files yet."), width)], height, width);
|
|
105
|
+
const selected = Math.min(Math.max(0, input.selected), input.files.length - 1);
|
|
106
|
+
const wide = width >= KNOWLEDGE_COLUMNS_MIN;
|
|
107
|
+
const [listWidth, detailWidth] = wide ? split(width, 0.34, 3, 44) : [width, width];
|
|
108
|
+
const total = input.files.reduce((sum, info) => sum + info.notes, 0);
|
|
109
|
+
const listPane = fill([rule(listWidth, "Knowledge", theme, total > 0 ? `✎ ${total}` : ""), ...listLines(input.files, selected, listWidth, height - 1, input.focus === "list", theme)], height);
|
|
110
|
+
const info = input.files[selected]!;
|
|
111
|
+
const view = input.view && input.view.agent === info.agent && input.view.file === info.file ? input.view : undefined;
|
|
112
|
+
const title = `${AGENT_DIR_NAMES[info.agent]} · ${info.file}${input.focus === "detail" ? " ◂" : ""}`;
|
|
113
|
+
const head: string[] = [rule(detailWidth, title, theme, view ? `${view.entries.length} entr${view.entries.length === 1 ? "y" : "ies"} · ${sizeWords(view.chars)} chars` : "")];
|
|
114
|
+
if (view?.over) head.push(...wrap(paint(theme, "warning", `over the compaction threshold — ask the oracle to compact ${info.file}`), detailWidth));
|
|
115
|
+
const rows = Math.max(1, height - head.length);
|
|
116
|
+
let body: string[];
|
|
117
|
+
if (!view) body = [paint(theme, "dim", "reading…")];
|
|
118
|
+
else if (view.entries.length === 0 && view.detached.length === 0) body = wrap(paint(theme, "dim", "Nothing here yet. n adds the first entry."), detailWidth);
|
|
119
|
+
else {
|
|
120
|
+
const drawn = entryLines(view, Math.min(input.cursor, Math.max(0, view.entries.length - 1)), detailWidth, input.focus === "detail", theme);
|
|
121
|
+
let start = Math.max(0, Math.min(input.offset, Math.max(0, drawn.lines.length - rows)));
|
|
122
|
+
// The picked entry stays in view (an entry taller than the pane shows its first lines).
|
|
123
|
+
if (input.focus === "detail" && view.entries.length > 0) {
|
|
124
|
+
if (drawn.from < start) start = drawn.from;
|
|
125
|
+
else if (drawn.to > start + rows) start = Math.min(drawn.from, drawn.to - rows);
|
|
126
|
+
}
|
|
127
|
+
input.layout.offset = start;
|
|
128
|
+
input.layout.total = drawn.lines.length;
|
|
129
|
+
input.layout.rows = rows;
|
|
130
|
+
body = drawn.lines.slice(start);
|
|
131
|
+
}
|
|
132
|
+
const detailPane = fill([...head, ...body], height);
|
|
133
|
+
if (!wide) return fill(input.focus === "detail" ? detailPane : listPane, height, width);
|
|
134
|
+
return fill(columns(listPane, detailPane, listWidth, detailWidth, " │ ", theme), height, width);
|
|
135
|
+
}
|
package/src/lobby/tabs/plan.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface PlanView {
|
|
|
45
45
|
nextMode?: RoundMode;
|
|
46
46
|
seed?: PlannerSeed;
|
|
47
47
|
saved?: PlannedTask;
|
|
48
|
+
/** How many tasks the plan was saved as, when it was split. */
|
|
49
|
+
savedParts?: number;
|
|
48
50
|
title?: string;
|
|
49
51
|
/** The round's questions wait for answers. */
|
|
50
52
|
awaitingAnswers: boolean;
|
|
@@ -120,7 +122,7 @@ function statusLine(view: PlanView, input: PlanTabInput, theme?: LobbyTheme): st
|
|
|
120
122
|
else if (view.reply?.status === "ready") parts.push(paint(theme, "success", `✓ ready — ${input.saveKey ?? "Ctrl+S"} saves it`));
|
|
121
123
|
else if (view.awaitingAnswers) parts.push(paint(theme, "warning", `● ${count(view.questions.length, "question")} — enter ${view.answeredChunks > 0 ? "resumes" : "answers them"}`));
|
|
122
124
|
if (view.lineComments.length > 0) parts.push(paint(theme, "accent", `◆ ${count(view.lineComments.length, "comment")} to send`));
|
|
123
|
-
if (view.saved) parts.push(paint(theme, "success", `saved as ${view.saved.id}`));
|
|
125
|
+
if (view.saved) parts.push(paint(theme, "success", view.savedParts && view.savedParts > 1 ? `saved as ${view.savedParts} tasks, from ${view.saved.id}` : `saved as ${view.saved.id}`));
|
|
124
126
|
if (!view.busy && view.turns > 0) parts.push(paint(theme, "dim", roundLabel(view.turns, view.limit)));
|
|
125
127
|
if (!view.busy && view.nextMode === "final") parts.push(paint(theme, "warning", "the next round is the last: the oracle settles the rest"));
|
|
126
128
|
return parts.join(paint(theme, "dim", " · "));
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -12,7 +12,7 @@ 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
14
|
import { textWidth } from "../../width.ts";
|
|
15
|
-
import { ago, beside, bold, box, detailWindow, fill, markdownHanging, markdownLines, notePane, paint, position, rule, selectRow, since, spread, strike, windowStart, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
15
|
+
import { ago, beside, BRANCH_GLYPH, bold, box, detailWindow, fill, markdownHanging, markdownLines, notePane, paint, position, rule, selectRow, since, spread, strike, windowStart, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
16
16
|
|
|
17
17
|
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
18
18
|
|
|
@@ -114,7 +114,7 @@ export function taskRows(tasks: readonly Task[], plans: readonly PlannedTask[],
|
|
|
114
114
|
...plans.filter((plan) => plan.status === "pending").map((plan): TaskRow => ({
|
|
115
115
|
kind: "plan",
|
|
116
116
|
id: plan.id,
|
|
117
|
-
title: plan.title,
|
|
117
|
+
title: plan.split ? `${plan.title} (${plan.split.part}/${plan.split.of})` : plan.title,
|
|
118
118
|
section: "pending",
|
|
119
119
|
status: "pending",
|
|
120
120
|
check: "open",
|
|
@@ -270,6 +270,10 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
270
270
|
lines.push(...wrapHanging(" ", facts.join(dot), width));
|
|
271
271
|
const track = task.track ? (task.track.path === "fast" ? `fast track (${task.track.size})` : `full workflow (${task.track.size})`) : "";
|
|
272
272
|
lines.push(...wrapHanging(" ", paint(theme, "dim", [task.id, track, task.domains.length > 0 ? task.domains.join(", ") : ""].filter(Boolean).join(" · ")), width));
|
|
273
|
+
if (task.git) {
|
|
274
|
+
const where = task.git.mode === "worktree" && task.git.path ? ` · worktree ${task.git.path}` : "";
|
|
275
|
+
lines.push(...wrapHanging(" ", paint(theme, "dim", `${BRANCH_GLYPH} ${task.git.branch}${task.git.from ? ` · from ${task.git.from}` : ""}${where}`), width));
|
|
276
|
+
}
|
|
273
277
|
const steps = task.plan ? planChecklist(task.plan, persistedRuns(task)) : [];
|
|
274
278
|
const done = steps.filter((step) => step.status === "done").length;
|
|
275
279
|
if (steps.length > 0) {
|
|
@@ -331,6 +335,11 @@ export function planDetailLines(plan: PlannedTask, width: number, now: number, t
|
|
|
331
335
|
...wrapHanging(" ", paint(theme, "dim", plan.id), width),
|
|
332
336
|
];
|
|
333
337
|
if (plan.issue) lines.push(...wrapHanging(" ", `from issue #${plan.issue.number} — ${plan.issue.title}${plan.issue.url ? ` ${paint(theme, "dim", plan.issue.url)}` : ""}`, width));
|
|
338
|
+
if (plan.split) {
|
|
339
|
+
const { part, of, titles, after } = plan.split;
|
|
340
|
+
lines.push(...wrapHanging(" ", `${paint(theme, "accent", `part ${part} of ${of}`)} of one plan that was split into tasks${after.length > 0 ? `, ${paint(theme, "warning", `after ${after.map((number) => `part ${number}`).join(" and ")}`)}` : ""}`, width));
|
|
341
|
+
lines.push(...wrapHanging(" ", titles.map((title, index) => (index + 1 === part ? bold(theme, `${index + 1}. ${title}`) : paint(theme, "muted", `${index + 1}. ${title}`))).join(paint(theme, "dim", " · ")), width));
|
|
342
|
+
}
|
|
334
343
|
const key = (text: string) => paint(theme, "accent", text);
|
|
335
344
|
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));
|
|
336
345
|
lines.push(...section("Agreed plan", width, theme), ...markdownLines(plan.brief, width, theme));
|