@a-t-h-i/bot-lobby 0.6.7 → 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 +132 -15
- package/package.json +1 -1
- package/prompts/master.md +15 -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 +19 -5
- package/src/ask/state.ts +8 -2
- package/src/ask/tool.ts +3 -1
- package/src/ask/view.ts +6 -3
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -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/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +95 -18
- package/src/lobby/planner.ts +199 -7
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/runtime.ts +115 -13
- 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 +432 -32
- package/src/master/master.ts +39 -13
- package/src/pi/commands.ts +12 -7
- package/src/pi/model-support.ts +9 -0
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +41 -1
- 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 +32 -3
- 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 +11 -0
package/src/lobby/view.ts
CHANGED
|
@@ -24,15 +24,24 @@ import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } f
|
|
|
24
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
25
25
|
import { agentsIndicator, workingAgents } from "./mini.ts";
|
|
26
26
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
27
|
-
import { beside, bold, box, fit, highlight, pagerButton, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
27
|
+
import { beside, BRANCH_GLYPH, bold, box, fit, highlight, pageStep, pagerButton, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
28
28
|
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
29
29
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
30
30
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
31
31
|
import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
32
32
|
import { renderIssues } from "./tabs/issues.ts";
|
|
33
|
+
import { renderGit } from "./tabs/git.ts";
|
|
34
|
+
import { renderKnowledge, type KnowledgeLayout } from "./tabs/knowledge.ts";
|
|
35
|
+
import type { EntryRef, KnowledgeBook, KnowledgeFileInfo, KnowledgeView } from "./knowledge.ts";
|
|
36
|
+
import type { PullsState } from "./pulls.ts";
|
|
37
|
+
import type { PullReviews } from "./pr-review.ts";
|
|
38
|
+
import type { WorkspaceInfo } from "../execution/workspace.ts";
|
|
33
39
|
import { filterRecords, renderMetrics } from "./tabs/metrics.ts";
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
/** The mark before a branch name in the title (shared with the task details). */
|
|
42
|
+
export { BRANCH_GLYPH };
|
|
43
|
+
|
|
44
|
+
export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "metrics", "git", "knowledge"] as const;
|
|
36
45
|
export type TabId = (typeof TAB_IDS)[number];
|
|
37
46
|
|
|
38
47
|
export const TAB_LABELS: Record<TabId, string> = {
|
|
@@ -42,6 +51,8 @@ export const TAB_LABELS: Record<TabId, string> = {
|
|
|
42
51
|
quickfix: "Quick fix",
|
|
43
52
|
issues: "Issues",
|
|
44
53
|
metrics: "Metrics",
|
|
54
|
+
git: "Git",
|
|
55
|
+
knowledge: "Knowledge",
|
|
45
56
|
};
|
|
46
57
|
|
|
47
58
|
/** The tabs on show: Issues only while `lobby.issues` switches it on. */
|
|
@@ -149,6 +160,8 @@ export interface LobbyHost {
|
|
|
149
160
|
newPlanner(seed?: PlannerSeed, seats?: readonly PanelMember[]): PlanningSession;
|
|
150
161
|
/** The oracle puts the panel's open questions to the user, one questionnaire at a time; returns a notice. */
|
|
151
162
|
answerPanel(): Promise<string>;
|
|
163
|
+
/** Save the plan as a pending task, offering to split a long one into several first; returns a notice. */
|
|
164
|
+
savePlan(): Promise<string>;
|
|
152
165
|
/** The seats a new session starts with, from settings. */
|
|
153
166
|
defaultPanel(): readonly PanelMember[];
|
|
154
167
|
/** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
|
|
@@ -156,6 +169,14 @@ export interface LobbyHost {
|
|
|
156
169
|
/** `model · thinking` a panel seat runs on. */
|
|
157
170
|
seatLabel(member: PanelMember): string;
|
|
158
171
|
issues: IssuesState;
|
|
172
|
+
/** The Git tab's pull requests, read through `gh`. */
|
|
173
|
+
pulls: PullsState;
|
|
174
|
+
/** Reviews of pull requests by an agent, and Jev's reads of them. */
|
|
175
|
+
reviews: PullReviews;
|
|
176
|
+
/** Every agent's knowledge files: read, edited, commented on. */
|
|
177
|
+
knowledge: KnowledgeBook;
|
|
178
|
+
/** Edit a text in pi's multi-line editor (the lobby steps aside meanwhile); the new text, or undefined when cancelled. */
|
|
179
|
+
editText(title: string, text: string): Promise<string | undefined>;
|
|
159
180
|
/** The Issues tab is switched on (`lobby.issues`). */
|
|
160
181
|
issuesEnabled(): boolean;
|
|
161
182
|
/** Which panes show (`lobby.panels`), and remembering a change. */
|
|
@@ -197,6 +218,10 @@ export interface LobbyHost {
|
|
|
197
218
|
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
198
219
|
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
199
220
|
profileLabel(kind: LobbyAgentKind): string;
|
|
221
|
+
/** The repository (or folder) and branch the title shows; the folder's name alone until git has answered. */
|
|
222
|
+
workspace?(): WorkspaceInfo;
|
|
223
|
+
/** Ask for the workspace to be read again (the branch may have changed); it arrives through `workspace()`. */
|
|
224
|
+
refreshWorkspace?(): void;
|
|
200
225
|
requestRender(): void;
|
|
201
226
|
now?(): number;
|
|
202
227
|
}
|
|
@@ -209,6 +234,9 @@ export const DATA_REFRESH_MS = 2000;
|
|
|
209
234
|
const NOTICE_MS = 6000;
|
|
210
235
|
/** Lines one wheel notch scrolls. */
|
|
211
236
|
const WHEEL_LINES = 3;
|
|
237
|
+
/** The longest repository and branch names the title shows whole. */
|
|
238
|
+
const TITLE_NAME_MAX = 28;
|
|
239
|
+
const TITLE_BRANCH_MAX = 36;
|
|
212
240
|
|
|
213
241
|
/** The prompt editor, with its target written into the top border. */
|
|
214
242
|
class LobbyEditor extends Editor {
|
|
@@ -339,19 +367,33 @@ export class LobbyView implements Component, Focusable {
|
|
|
339
367
|
private issueFocus: "list" | "detail" = "list";
|
|
340
368
|
private issueDetailOffset = 0;
|
|
341
369
|
private issueDraft = false;
|
|
370
|
+
private gitSelected = 0;
|
|
371
|
+
private gitFocus: "list" | "detail" = "list";
|
|
372
|
+
private gitDetailOffset = 0;
|
|
373
|
+
/** The prompt is taking what the review of the selected pull request should look at first. */
|
|
374
|
+
private gitFocusDraft = false;
|
|
375
|
+
private kSelected = 0;
|
|
376
|
+
private kFocus: "list" | "detail" = "list";
|
|
377
|
+
/** The picked entry of the open knowledge file (an index into its entries), and how far its pane is scrolled. */
|
|
378
|
+
private kCursor = 0;
|
|
379
|
+
private kOffset = 0;
|
|
380
|
+
private readonly kLayout: KnowledgeLayout = { offset: 0, total: 0, rows: 0 };
|
|
381
|
+
/** The prompt is writing an edit of the picked entry, a note on it, or a new entry after it. */
|
|
382
|
+
private kDraft: { kind: "edit" | "comment" | "add"; entry?: EntryRef } | undefined;
|
|
342
383
|
private metricsSelected = 0;
|
|
343
384
|
private metricsBy: GroupBy = "model";
|
|
344
385
|
private metricsSort: SortKey = "runs";
|
|
345
386
|
|
|
346
387
|
/** The Tasks tab lists archived tasks too (v). */
|
|
347
388
|
showArchived = false;
|
|
348
|
-
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; at: number } = {
|
|
389
|
+
private data: { tasks: Task[]; plans: PlannedTask[]; archived: Task[]; comments: Map<string, PlanComment[]>; metrics: MetricRecord[]; classifierMetrics: MetricRecord[]; knowledge: { files: KnowledgeFileInfo[]; view?: KnowledgeView }; at: number } = {
|
|
349
390
|
tasks: [],
|
|
350
391
|
plans: [],
|
|
351
392
|
archived: [],
|
|
352
393
|
comments: new Map(),
|
|
353
394
|
metrics: [],
|
|
354
395
|
classifierMetrics: [],
|
|
396
|
+
knowledge: { files: [] },
|
|
355
397
|
at: 0,
|
|
356
398
|
};
|
|
357
399
|
|
|
@@ -415,6 +457,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
415
457
|
|| Boolean(this.host.planner()?.busy)
|
|
416
458
|
|| this.host.sessions().some((session) => session.busy)
|
|
417
459
|
|| (this.issuesOn && this.host.issues.loading)
|
|
460
|
+
|| (this.tab === "git" && this.host.pulls.loading)
|
|
461
|
+
|| this.host.reviews.busy
|
|
418
462
|
|| zen.runs.some((run) => run.status === "running")
|
|
419
463
|
|| Boolean(zen.task && !zen.task.paused && !TERMINAL_STATES.includes(zen.task.state));
|
|
420
464
|
}
|
|
@@ -433,6 +477,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
433
477
|
const now = this.now();
|
|
434
478
|
if (!force && now - this.data.at < DATA_REFRESH_MS) return;
|
|
435
479
|
this.dataVersion += 1;
|
|
480
|
+
this.host.refreshWorkspace?.();
|
|
436
481
|
this.data = {
|
|
437
482
|
tasks: this.host.tasks(),
|
|
438
483
|
plans: this.host.plans(),
|
|
@@ -440,6 +485,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
440
485
|
comments: new Map(),
|
|
441
486
|
metrics: this.tab === "metrics" ? this.host.metrics() : this.data.metrics,
|
|
442
487
|
classifierMetrics: this.tab === "metrics" ? this.host.classifierMetrics?.() ?? [] : this.data.classifierMetrics,
|
|
488
|
+
knowledge: this.tab === "knowledge" ? this.readKnowledge() : this.data.knowledge,
|
|
443
489
|
at: now,
|
|
444
490
|
};
|
|
445
491
|
if (this.tab === "tasks") {
|
|
@@ -448,6 +494,19 @@ export class LobbyView implements Component, Focusable {
|
|
|
448
494
|
}
|
|
449
495
|
}
|
|
450
496
|
|
|
497
|
+
/** Every knowledge file, and the selected one read as entries. */
|
|
498
|
+
private readKnowledge(): { files: KnowledgeFileInfo[]; view?: KnowledgeView } {
|
|
499
|
+
const files = this.host.knowledge.files();
|
|
500
|
+
this.kSelected = Math.max(0, Math.min(this.kSelected, files.length - 1));
|
|
501
|
+
const info = files[this.kSelected];
|
|
502
|
+
return { files, ...(info ? { view: this.host.knowledge.open(info.agent, info.file) } : {}) };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Read the open knowledge file again (after an edit, or another file was picked). */
|
|
506
|
+
private reloadKnowledge(): void {
|
|
507
|
+
this.data.knowledge = this.readKnowledge();
|
|
508
|
+
}
|
|
509
|
+
|
|
451
510
|
private say(text: string, kind: Notice["kind"] = "info"): void {
|
|
452
511
|
this.notice = { text, at: this.now(), kind };
|
|
453
512
|
}
|
|
@@ -475,12 +534,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
475
534
|
setTab(tab: TabId): void {
|
|
476
535
|
if (tab === this.tab) return;
|
|
477
536
|
if (!this.tabs().includes(tab)) return this.say(`the ${TAB_LABELS[tab]} tab is off — set lobby.${tab} to true in the config to bring it back`, "warning");
|
|
537
|
+
// An entry being edited is not a draft to come back to: its text was only lent to the prompt.
|
|
538
|
+
if (this.kDraft) {
|
|
539
|
+
this.kDraft = undefined;
|
|
540
|
+
this.editor.setText("");
|
|
541
|
+
}
|
|
478
542
|
this.drafts[this.tab] = this.editor.getText();
|
|
479
543
|
this.tab = tab;
|
|
480
544
|
this.editor.setText(this.drafts[tab] ?? "");
|
|
481
545
|
this.commentTarget = undefined;
|
|
482
546
|
this.lineTarget = undefined;
|
|
483
547
|
this.issueDraft = false;
|
|
548
|
+
this.gitFocusDraft = false;
|
|
484
549
|
this.armed = undefined;
|
|
485
550
|
this.help = false;
|
|
486
551
|
this.picking = false;
|
|
@@ -489,6 +554,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
489
554
|
this.setMode(PROMPT_FIRST.has(tab) ? "type" : "browse");
|
|
490
555
|
this.refreshData(true);
|
|
491
556
|
if (tab === "issues" && !this.host.issues.loaded && !this.host.issues.loading) void this.host.issues.refresh().then(() => this.loadIssueDetail());
|
|
557
|
+
if (tab === "git" && !this.host.pulls.loaded && !this.host.pulls.loading) void this.host.pulls.refresh().then(() => this.loadPullDetail());
|
|
558
|
+
else if (tab === "git") void this.loadPullDetail();
|
|
492
559
|
this.host.requestRender();
|
|
493
560
|
}
|
|
494
561
|
|
|
@@ -688,6 +755,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
688
755
|
}
|
|
689
756
|
this.commentTarget = undefined;
|
|
690
757
|
this.issueDraft = false;
|
|
758
|
+
this.gitFocusDraft = false;
|
|
759
|
+
if (this.kDraft) {
|
|
760
|
+
this.kDraft = undefined;
|
|
761
|
+
this.editor.setText("");
|
|
762
|
+
}
|
|
691
763
|
this.newSession = false;
|
|
692
764
|
if (this.lineTarget) {
|
|
693
765
|
this.lineTarget = undefined;
|
|
@@ -714,7 +786,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
714
786
|
if (matchesKey(data, Key.home) || matchesKey(data, Key.end)) return this.scrollToEdge(matchesKey(data, Key.home));
|
|
715
787
|
const tabs = this.tabs();
|
|
716
788
|
if (!PROMPT_FIRST.has(this.tab) && /^[1-9]$/.test(data) && Number(data) <= tabs.length) return this.setTab(tabs[Number(data) - 1]!);
|
|
717
|
-
if (data === "i" && this.tab !== "metrics" && this.tab !== "tasks" && this.tab !== "issues") return this.setMode("type");
|
|
789
|
+
if (data === "i" && this.tab !== "metrics" && this.tab !== "tasks" && this.tab !== "issues" && this.tab !== "git" && this.tab !== "knowledge") return this.setMode("type");
|
|
718
790
|
if (this.tabCommand(data)) return;
|
|
719
791
|
const printable = isPrintable(data);
|
|
720
792
|
if (printable && PROMPT_FIRST.has(this.tab)) {
|
|
@@ -736,6 +808,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
736
808
|
return this.fixFocus;
|
|
737
809
|
case "issues":
|
|
738
810
|
return this.issueFocus;
|
|
811
|
+
case "git":
|
|
812
|
+
return this.gitFocus;
|
|
813
|
+
case "knowledge":
|
|
814
|
+
return this.kFocus;
|
|
739
815
|
case "metrics":
|
|
740
816
|
return "table";
|
|
741
817
|
}
|
|
@@ -782,6 +858,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
782
858
|
case "issues":
|
|
783
859
|
this.issueFocus = direction < 0 ? "list" : "detail";
|
|
784
860
|
return;
|
|
861
|
+
case "git":
|
|
862
|
+
this.gitFocus = direction < 0 ? "list" : "detail";
|
|
863
|
+
return;
|
|
864
|
+
case "knowledge":
|
|
865
|
+
this.kFocus = direction < 0 ? "list" : "detail";
|
|
866
|
+
return;
|
|
785
867
|
case "metrics":
|
|
786
868
|
return;
|
|
787
869
|
}
|
|
@@ -807,11 +889,38 @@ export class LobbyView implements Component, Focusable {
|
|
|
807
889
|
void this.loadIssueDetail();
|
|
808
890
|
}
|
|
809
891
|
return;
|
|
892
|
+
case "git":
|
|
893
|
+
if (this.gitFocus === "detail" || Math.abs(delta) > 1) this.gitDetailOffset = Math.max(0, this.gitDetailOffset + delta);
|
|
894
|
+
else {
|
|
895
|
+
this.gitSelected = Math.max(0, Math.min(this.host.pulls.pulls.length - 1, this.gitSelected + delta));
|
|
896
|
+
this.gitDetailOffset = 0;
|
|
897
|
+
void this.loadPullDetail();
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
case "knowledge":
|
|
901
|
+
return this.moveKnowledge(delta);
|
|
810
902
|
case "metrics":
|
|
811
903
|
return this.scrollPane("table", delta);
|
|
812
904
|
}
|
|
813
905
|
}
|
|
814
906
|
|
|
907
|
+
/** Arrows and pages on the Knowledge tab: pick a file, or pick an entry of the open one (a page moves several). */
|
|
908
|
+
private moveKnowledge(delta: number): void {
|
|
909
|
+
const knowledge = this.data.knowledge;
|
|
910
|
+
if (this.kFocus === "list") {
|
|
911
|
+
const next = Math.max(0, Math.min(knowledge.files.length - 1, this.kSelected + delta));
|
|
912
|
+
if (next === this.kSelected) return;
|
|
913
|
+
this.kSelected = next;
|
|
914
|
+
this.kCursor = 0;
|
|
915
|
+
this.kOffset = 0;
|
|
916
|
+
this.reloadKnowledge();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const count = knowledge.view?.entries.length ?? 0;
|
|
920
|
+
const step = Math.abs(delta) > 1 ? Math.sign(delta) * Math.max(2, Math.round(Math.abs(delta) / 3)) : delta;
|
|
921
|
+
this.kCursor = Math.max(0, Math.min(count - 1, this.kCursor + step));
|
|
922
|
+
}
|
|
923
|
+
|
|
815
924
|
/**
|
|
816
925
|
* Scroll one pane of the current tab by `delta` lines, positive toward its
|
|
817
926
|
* end. Lists move their selection instead; the draft moves its cursor (the
|
|
@@ -990,6 +1099,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
990
1099
|
if (row?.kind === "task" && !this.data.comments.has(row.id)) this.data.comments.set(row.id, this.host.comments(row.id));
|
|
991
1100
|
}
|
|
992
1101
|
|
|
1102
|
+
private async loadPullDetail(): Promise<void> {
|
|
1103
|
+
const pull = this.host.pulls.pulls[this.gitSelected];
|
|
1104
|
+
if (pull) await this.host.pulls.detail(pull.number);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
993
1107
|
private async loadIssueDetail(): Promise<void> {
|
|
994
1108
|
const issue = this.host.issues.issues[this.issueSelected];
|
|
995
1109
|
if (issue) await this.host.issues.detail(issue.number);
|
|
@@ -1051,6 +1165,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
1051
1165
|
return false;
|
|
1052
1166
|
case "issues":
|
|
1053
1167
|
return this.issuesCommand(data, enter, escape);
|
|
1168
|
+
case "git":
|
|
1169
|
+
return this.gitCommand(data, enter, escape);
|
|
1170
|
+
case "knowledge":
|
|
1171
|
+
return this.knowledgeCommand(data, enter, escape);
|
|
1054
1172
|
case "metrics":
|
|
1055
1173
|
if (data === "g") return (this.metricsBy = this.metricsBy === "model" ? "model-kind" : "model", this.metricsSelected = 0), true;
|
|
1056
1174
|
if (data === "s") return (this.metricsSort = SORT_KEYS[(SORT_KEYS.indexOf(this.metricsSort) + 1) % SORT_KEYS.length]!), true;
|
|
@@ -1462,17 +1580,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
1462
1580
|
this.say(on ? `auto mode on — the oracle drives ${task.id} to completion without asking` : `auto mode off — the oracle asks you again on ${task.id}`);
|
|
1463
1581
|
}
|
|
1464
1582
|
|
|
1465
|
-
/**
|
|
1583
|
+
/**
|
|
1584
|
+
* Save the planning session's draft to the pending tasks, from any tab and
|
|
1585
|
+
* in either mode. A plan with many steps is first offered a split into
|
|
1586
|
+
* several tasks, which takes the oracle a moment and asks the user.
|
|
1587
|
+
*/
|
|
1466
1588
|
savePlan(): void {
|
|
1467
1589
|
const session = this.host.planner();
|
|
1468
1590
|
if (!session?.reply?.plan) return this.say(session?.busy ? "the first draft is still being written" : "no plan to save yet — describe a task in the Plan tab", "warning");
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1591
|
+
void this.host.savePlan().then(
|
|
1592
|
+
(notice) => {
|
|
1593
|
+
this.say(notice);
|
|
1594
|
+
this.refreshData(true);
|
|
1595
|
+
this.host.requestRender();
|
|
1596
|
+
},
|
|
1597
|
+
(error: Error) => {
|
|
1598
|
+
this.say(error.message, "warning");
|
|
1599
|
+
this.host.requestRender();
|
|
1600
|
+
},
|
|
1601
|
+
);
|
|
1476
1602
|
}
|
|
1477
1603
|
|
|
1478
1604
|
/** The oracle puts the round's questions to the user through the questionnaire. */
|
|
@@ -1589,6 +1715,162 @@ export class LobbyView implements Component, Focusable {
|
|
|
1589
1715
|
return false;
|
|
1590
1716
|
}
|
|
1591
1717
|
|
|
1718
|
+
/** The selected pull request on the Git tab. */
|
|
1719
|
+
private selectedPull() {
|
|
1720
|
+
return this.host.pulls.pulls[this.gitSelected];
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** Review the selected pull request with a read-only agent, optionally looking at `focus` first. */
|
|
1724
|
+
private startReview(focus?: string): void {
|
|
1725
|
+
const pull = this.selectedPull();
|
|
1726
|
+
if (!pull) return this.say("no pull request selected — r loads the open ones", "warning");
|
|
1727
|
+
if (this.host.reviews.running(pull.number)) return this.say(`already reviewing #${pull.number} — x stops it`, "warning");
|
|
1728
|
+
this.gitFocus = "detail";
|
|
1729
|
+
this.gitDetailOffset = 0;
|
|
1730
|
+
this.say(`reviewing #${pull.number}${focus ? ` — looking at: ${clip(focus, 40)}` : ""} — it streams into the activity log; x stops it`);
|
|
1731
|
+
void this.host.reviews.start(pull.number, { ...(focus ? { focus } : {}), ...(this.host.pulls.details.has(pull.number) ? { detail: this.host.pulls.details.get(pull.number)! } : {}) }).then(() => this.host.requestRender());
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
private gitCommand(data: string, enter: boolean, escape: boolean): boolean {
|
|
1735
|
+
const pull = this.selectedPull();
|
|
1736
|
+
if (enter) {
|
|
1737
|
+
this.gitFocus = this.gitFocus === "list" ? "detail" : "list";
|
|
1738
|
+
void this.loadPullDetail();
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
if (escape) return (this.gitFocus = "list"), true;
|
|
1742
|
+
if (data === "r") {
|
|
1743
|
+
this.host.pulls.forget();
|
|
1744
|
+
void this.host.pulls.refresh().then(() => this.loadPullDetail());
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
if (!pull) {
|
|
1748
|
+
// Nothing listed: the review keys say so instead of doing nothing.
|
|
1749
|
+
if (!"vftx".includes(data) || data.length !== 1) return false;
|
|
1750
|
+
this.say("no pull request selected — r loads the open ones", "warning");
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
if (data === "v") return this.startReview(), true;
|
|
1754
|
+
if (data === "f") {
|
|
1755
|
+
this.gitFocusDraft = true;
|
|
1756
|
+
this.setMode("type");
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
if (data === "t") {
|
|
1760
|
+
this.gitFocus = "detail";
|
|
1761
|
+
this.gitDetailOffset = 0;
|
|
1762
|
+
this.say(`asking Jev to read #${pull.number}…`);
|
|
1763
|
+
void this.host.reviews.readWithJev(pull.number, this.host.pulls.details.get(pull.number)).then(() => this.host.requestRender());
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
if (data === "x") {
|
|
1767
|
+
this.say(this.host.reviews.cancel(pull.number) ? `stopping the review of #${pull.number}` : `no review of #${pull.number} is running`, "warning");
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/** The picked entry of the open knowledge file, as the edit functions find it again. */
|
|
1774
|
+
private pickedEntry(): { entry: EntryRef; text: string } | undefined {
|
|
1775
|
+
const entry = this.data.knowledge.view?.entries[this.kCursor];
|
|
1776
|
+
return entry ? { entry: { text: entry.text, occurrence: entry.occurrence }, text: entry.text } : undefined;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
private knowledgeCommand(data: string, enter: boolean, escape: boolean): boolean {
|
|
1780
|
+
const info = this.data.knowledge.files[this.kSelected];
|
|
1781
|
+
if (enter) return (this.kFocus = this.kFocus === "list" ? "detail" : "list"), true;
|
|
1782
|
+
if (escape) return (this.kFocus = "list"), true;
|
|
1783
|
+
if (data === "r") {
|
|
1784
|
+
this.reloadKnowledge();
|
|
1785
|
+
this.say("knowledge reread from disk");
|
|
1786
|
+
return true;
|
|
1787
|
+
}
|
|
1788
|
+
if (!info) return false;
|
|
1789
|
+
if (data === "E") {
|
|
1790
|
+
const view = this.data.knowledge.view;
|
|
1791
|
+
if (!view) return true;
|
|
1792
|
+
void this.host.editText(`${info.label} — ${info.agent}/${info.file}`, view.content).then((text) => {
|
|
1793
|
+
if (text !== undefined) this.say(this.host.knowledge.replaceFile(info.agent, info.file, text));
|
|
1794
|
+
this.reloadKnowledge();
|
|
1795
|
+
this.host.requestRender();
|
|
1796
|
+
});
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
if (!"ecndx".includes(data) || data.length !== 1) return false;
|
|
1800
|
+
const picked = this.pickedEntry();
|
|
1801
|
+
// Adding needs no entry to start from: it goes at the end of an empty or entry-less file.
|
|
1802
|
+
if (!picked && data !== "n") {
|
|
1803
|
+
this.say("nothing is picked — n adds an entry", "warning");
|
|
1804
|
+
return true;
|
|
1805
|
+
}
|
|
1806
|
+
this.kFocus = "detail";
|
|
1807
|
+
if (data === "e" && picked) {
|
|
1808
|
+
this.kDraft = { kind: "edit", entry: picked.entry };
|
|
1809
|
+
this.editor.setText(picked.text);
|
|
1810
|
+
this.setMode("type");
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
if (data === "c" && picked) {
|
|
1814
|
+
this.kDraft = { kind: "comment", entry: picked.entry };
|
|
1815
|
+
this.setMode("type");
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
if (data === "n") {
|
|
1819
|
+
this.kDraft = { kind: "add", ...(picked ? { entry: picked.entry } : {}) };
|
|
1820
|
+
this.setMode("type");
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
if (data === "d" && picked) {
|
|
1824
|
+
const key = `kdelete:${info.agent}/${info.file}:${picked.entry.occurrence}:${picked.text}`;
|
|
1825
|
+
if (this.armed !== key) {
|
|
1826
|
+
this.armed = key;
|
|
1827
|
+
this.say("press d again to delete this entry (the file is archived first, and its notes go with it)", "warning");
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
this.armed = undefined;
|
|
1831
|
+
this.say(this.host.knowledge.remove(info.agent, info.file, picked.entry));
|
|
1832
|
+
this.reloadKnowledge();
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
if (data === "x" && picked) {
|
|
1836
|
+
const notes = this.data.knowledge.view?.attached.filter((note) => note.entry === picked.text) ?? [];
|
|
1837
|
+
const newest = notes.at(-1);
|
|
1838
|
+
if (!newest) {
|
|
1839
|
+
this.say("this entry has no notes", "warning");
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
const key = `kunnote:${newest.id}`;
|
|
1843
|
+
if (this.armed !== key) {
|
|
1844
|
+
this.armed = key;
|
|
1845
|
+
this.say(`press x again to remove the note “${clip(newest.text, 40)}”`, "warning");
|
|
1846
|
+
return true;
|
|
1847
|
+
}
|
|
1848
|
+
this.armed = undefined;
|
|
1849
|
+
this.say(this.host.knowledge.unnote(newest.id));
|
|
1850
|
+
this.reloadKnowledge();
|
|
1851
|
+
return true;
|
|
1852
|
+
}
|
|
1853
|
+
return true;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/** The prompt's text on the Knowledge tab: an edit of the picked entry, a note on it, or a new entry after it. */
|
|
1857
|
+
private submitKnowledge(body: string): void {
|
|
1858
|
+
const draft = this.kDraft;
|
|
1859
|
+
const info = this.data.knowledge.files[this.kSelected];
|
|
1860
|
+
if (!draft || !info) {
|
|
1861
|
+
this.editor.setText(body);
|
|
1862
|
+
return this.say("press e to edit the picked entry, c to comment on it, n to add one", "warning");
|
|
1863
|
+
}
|
|
1864
|
+
this.kDraft = undefined;
|
|
1865
|
+
this.setMode("browse");
|
|
1866
|
+
const book = this.host.knowledge;
|
|
1867
|
+
const notice = draft.kind === "edit" ? book.edit(info.agent, info.file, draft.entry!, body) : draft.kind === "comment" ? book.comment(info.agent, info.file, draft.entry!, body) : book.add(info.agent, info.file, draft.entry, body);
|
|
1868
|
+
this.say(notice, /^(saved|note saved)/.test(notice) ? "info" : "warning");
|
|
1869
|
+
this.reloadKnowledge();
|
|
1870
|
+
// A new entry goes right after the picked one, which the cursor then moves onto.
|
|
1871
|
+
if (draft.kind === "add" && notice.startsWith("saved")) this.kCursor = Math.min(this.kCursor + (draft.entry ? 1 : 0), Math.max(0, (this.data.knowledge.view?.entries.length ?? 1) - 1));
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1592
1874
|
/* --------------------------------------------------------------- mouse */
|
|
1593
1875
|
|
|
1594
1876
|
/** Mouse events when pi runs full screen (pi-tui dispatches them to the overlay). */
|
|
@@ -1618,7 +1900,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1618
1900
|
/** The wheel scrolls the pane under the pointer (the focused one when it is over none). */
|
|
1619
1901
|
private wheel(direction: number, x = -1, y = -1): void {
|
|
1620
1902
|
if (this.help) return;
|
|
1621
|
-
if (this.tab === "issues") return this.scroll(direction * WHEEL_LINES);
|
|
1903
|
+
if (this.tab === "issues" || this.tab === "git" || this.tab === "knowledge") return this.scroll(direction * WHEEL_LINES);
|
|
1622
1904
|
this.scrollPane(this.paneAt(x, y) ?? this.focusedPane(), direction * WHEEL_LINES, true);
|
|
1623
1905
|
}
|
|
1624
1906
|
|
|
@@ -1644,9 +1926,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
1644
1926
|
// The page buttons in a scrolling pane's bottom border scroll it a page or two.
|
|
1645
1927
|
const paneBox = pane ? this.panes.get(pane) : undefined;
|
|
1646
1928
|
if (pane && paneBox && paneBox.total > paneBox.rows && row === paneBox.top + paneBox.height - 1) {
|
|
1647
|
-
const button = pagerButton(paneBox.width, x - paneBox.left);
|
|
1929
|
+
const button = pagerButton(paneBox.width, x - paneBox.left, paneBox.total, paneBox.rows);
|
|
1648
1930
|
if (button) {
|
|
1649
|
-
this.scrollPane(pane, button *
|
|
1931
|
+
this.scrollPane(pane, button * pageStep(paneBox.rows), false, true);
|
|
1650
1932
|
return;
|
|
1651
1933
|
}
|
|
1652
1934
|
}
|
|
@@ -1674,6 +1956,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1674
1956
|
private submit(text: string): void {
|
|
1675
1957
|
const body = text.trim();
|
|
1676
1958
|
if (!body) {
|
|
1959
|
+
if (this.tab === "knowledge" && this.kDraft) return this.say(this.kDraft.kind === "edit" ? "nothing to save — d d deletes an entry, esc cancels" : "write something first, or esc to cancel", "warning");
|
|
1677
1960
|
// An empty enter on the Plan tab opens the panel's questions; on the Lobby, a background session's question.
|
|
1678
1961
|
if (this.tab === "plan" && !this.lineTarget && this.host.planner()?.awaitingAnswers) this.answerQuestions();
|
|
1679
1962
|
if (this.tab === "lobby" && !this.newSession) this.answerSessionDialog();
|
|
@@ -1713,6 +1996,16 @@ export class LobbyView implements Component, Focusable {
|
|
|
1713
1996
|
this.say("filing the issue…");
|
|
1714
1997
|
void this.host.issues.create(body);
|
|
1715
1998
|
return;
|
|
1999
|
+
case "knowledge":
|
|
2000
|
+
return this.submitKnowledge(body);
|
|
2001
|
+
case "git":
|
|
2002
|
+
if (!this.gitFocusDraft) {
|
|
2003
|
+
this.editor.setText(body);
|
|
2004
|
+
return this.say("press f to review the selected pull request with a focus", "warning");
|
|
2005
|
+
}
|
|
2006
|
+
this.gitFocusDraft = false;
|
|
2007
|
+
this.setMode("browse");
|
|
2008
|
+
return this.startReview(body);
|
|
1716
2009
|
case "metrics":
|
|
1717
2010
|
return;
|
|
1718
2011
|
}
|
|
@@ -1802,46 +2095,83 @@ export class LobbyView implements Component, Focusable {
|
|
|
1802
2095
|
else if (session?.awaitingAnswers) badges.plan = `${session.questions.length}?`;
|
|
1803
2096
|
if (this.host.quickfix.running) badges.quickfix = spinner(this.tick);
|
|
1804
2097
|
if (this.issuesOn && this.host.issues.issues.length > 0) badges.issues = String(this.host.issues.issues.length);
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
for (const [index, tab] of this.tabs().entries()) {
|
|
2098
|
+
if (this.host.reviews.busy) badges.git = spinner(this.tick);
|
|
2099
|
+
else if (this.host.pulls.pulls.length > 0) badges.git = String(this.host.pulls.pulls.length);
|
|
2100
|
+
const cells = this.tabs().map((tab, index) => {
|
|
1809
2101
|
const number = paint(theme, tab === this.tab ? "accent" : "dim", String(index + 1));
|
|
1810
2102
|
const name = tab === this.tab ? bold(theme, paint(theme, "text", TAB_LABELS[tab])) : paint(theme, "muted", TAB_LABELS[tab]);
|
|
1811
2103
|
const badge = badges[tab] ? ` ${paint(theme, tab === "plan" && session?.awaitingAnswers ? "warning" : "accent", badges[tab]!)}` : "";
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
left += cell;
|
|
1816
|
-
spans.push({ tab, from, to: textWidth(left) });
|
|
1817
|
-
}
|
|
1818
|
-
this.tabSpans = spans;
|
|
1819
|
-
const room = width - textWidth(left) - 1;
|
|
2104
|
+
const cell = ` ${number} ${name}${badge} `;
|
|
2105
|
+
return { tab, text: tab === this.tab ? (theme.bg ? theme.bg("selectedBg", cell) : `[${cell}]`) : cell };
|
|
2106
|
+
});
|
|
1820
2107
|
const entry = this.viewedEntry();
|
|
1821
2108
|
const help = paint(theme, "dim", `${keyLabel(this.keys.help)} keys `);
|
|
1822
2109
|
const auto = entry.auto ? ` ${bold(theme, paint(theme, "success", "⟳ AUTO"))}` : "";
|
|
1823
2110
|
const waiting = this.host.sessions().reduce((count, session) => count + session.dialogs.length, 0);
|
|
1824
2111
|
const asking = waiting > 0 ? ` ${paint(theme, "warning", `● ${waiting} waiting · ${keyLabel(this.keys.sessions)}`)}` : "";
|
|
2112
|
+
// Short of room, the key to open the sessions goes before the waiting count does.
|
|
2113
|
+
const asked = waiting > 0 ? ` ${paint(theme, "warning", `● ${waiting} waiting`)}` : "";
|
|
1825
2114
|
let choices: string[];
|
|
1826
2115
|
if (entry.view.kind !== "here") {
|
|
1827
2116
|
// Another session in view: its name and what it is doing lead, so it is never mistaken for this window.
|
|
1828
2117
|
const working = entry.status === "working";
|
|
1829
2118
|
const dot = paint(theme, working ? "accent" : "dim", working ? spinner(this.tick) : "◆");
|
|
1830
2119
|
const where = paint(theme, "muted", `${entry.name} · ${entry.status}`);
|
|
1831
|
-
choices = [`${dot} ${where}${auto}${asking} ${help}`, `${dot} ${where}${auto}${asking} `, `${dot} ${where}${auto} `, `${dot}${auto} `];
|
|
2120
|
+
choices = [`${dot} ${where}${auto}${asking} ${help}`, `${dot} ${where}${auto}${asking} `, `${dot} ${where}${auto}${asked} `, `${dot}${auto}${asked} `, `${dot} ${where}${auto} `, `${dot}${auto} `];
|
|
1832
2121
|
} else {
|
|
1833
2122
|
const dot = paint(theme, this.host.masterBusy() ? "accent" : "dim", this.host.masterBusy() ? spinner(this.tick) : "●");
|
|
1834
2123
|
const state = zen.task ? paint(theme, "muted", zen.task.paused ? `${zen.task.state} (paused)` : zen.task.state) : "";
|
|
1835
2124
|
// Long task ids give way to the state, then to the dot alone.
|
|
1836
2125
|
choices = zen.task
|
|
1837
|
-
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
1838
|
-
: [`${paint(theme, "dim", "no task in this session")}${asking} ${help}`, `${paint(theme, "dim", "no task in this session")}${asking} `, help, ""];
|
|
2126
|
+
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${zen.task.id} ${state}${auto}${asked} `, `${dot} ${state}${auto}${asked} `, `${dot}${auto}${asked} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
2127
|
+
: [`${paint(theme, "dim", "no task in this session")}${asking} ${help}`, `${paint(theme, "dim", "no task in this session")}${asking} `, `${paint(theme, "dim", "no task in this session")}${asked} `, `${asked.trimStart()} `, help, ""];
|
|
2128
|
+
}
|
|
2129
|
+
// The longest title that leaves room for the tabs and, when a session waits on you, for saying so.
|
|
2130
|
+
const tabsWidth = cells.reduce((sum, cell) => sum + textWidth(cell.text), 0);
|
|
2131
|
+
const titles = this.titles(entry.task, theme);
|
|
2132
|
+
let brand = titles.at(-1)!;
|
|
2133
|
+
let status = "";
|
|
2134
|
+
for (const [index, title] of titles.entries()) {
|
|
2135
|
+
const room = width - textWidth(title) - 1 - tabsWidth - 1;
|
|
2136
|
+
const shown = choices.find((choice) => textWidth(choice) <= room) ?? "";
|
|
2137
|
+
if (index === titles.length - 1 || (room >= 0 && (waiting === 0 || shown.includes("waiting")))) {
|
|
2138
|
+
brand = title;
|
|
2139
|
+
status = shown;
|
|
2140
|
+
break;
|
|
2141
|
+
}
|
|
1839
2142
|
}
|
|
1840
|
-
|
|
2143
|
+
let left = `${brand}${paint(theme, "borderMuted", "│")}`;
|
|
2144
|
+
const spans: Array<{ tab: TabId; from: number; to: number }> = [];
|
|
2145
|
+
for (const cell of cells) {
|
|
2146
|
+
const from = textWidth(left);
|
|
2147
|
+
left += cell.text;
|
|
2148
|
+
spans.push({ tab: cell.tab, from, to: textWidth(left) });
|
|
2149
|
+
}
|
|
2150
|
+
this.tabSpans = spans;
|
|
1841
2151
|
const gap = width - textWidth(left) - textWidth(status);
|
|
1842
2152
|
return gap >= 1 ? `${left}${" ".repeat(gap)}${status}` : fit(left, width);
|
|
1843
2153
|
}
|
|
1844
2154
|
|
|
2155
|
+
/**
|
|
2156
|
+
* The lobby's title, longest first: the repository (or folder) it works
|
|
2157
|
+
* from and the branch, `◆ my-repo (⎇ main)`, then the name alone, then just
|
|
2158
|
+
* the mark. A task with a worktree of its own shows that branch, since that
|
|
2159
|
+
* is where its work happens.
|
|
2160
|
+
*/
|
|
2161
|
+
private titles(task: Task | undefined, theme: LobbyTheme): string[] {
|
|
2162
|
+
const workspace = this.host.workspace?.();
|
|
2163
|
+
const name = clip(workspace?.name || "bot-lobby", TITLE_NAME_MAX);
|
|
2164
|
+
const own = task?.git?.mode === "worktree" ? task.git.branch : undefined;
|
|
2165
|
+
const branch = own ?? workspace?.branch;
|
|
2166
|
+
const glyph = paint(theme, "accent", " ◆ ");
|
|
2167
|
+
const label = (text: string) => bold(theme, paint(theme, "accent", text));
|
|
2168
|
+
return [
|
|
2169
|
+
...(branch ? [`${glyph}${label(name)} ${paint(theme, "muted", `(${BRANCH_GLYPH} ${clip(branch, TITLE_BRANCH_MAX)})`)} `] : []),
|
|
2170
|
+
`${glyph}${label(name)} `,
|
|
2171
|
+
glyph,
|
|
2172
|
+
];
|
|
2173
|
+
}
|
|
2174
|
+
|
|
1845
2175
|
private promptLabel(): string {
|
|
1846
2176
|
const zen = this.host.zen();
|
|
1847
2177
|
const browsing = this.mode === "browse";
|
|
@@ -1876,6 +2206,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
1876
2206
|
return this.host.quickfix.running ? "describe a quick change; it runs after the current one" : "describe a quick change";
|
|
1877
2207
|
case "issues":
|
|
1878
2208
|
return this.issueDraft ? "new issue · first line is the title" : "n files a new issue";
|
|
2209
|
+
case "knowledge": {
|
|
2210
|
+
const picked = this.pickedEntry();
|
|
2211
|
+
if (this.kDraft?.kind === "edit") return `edit “${clip(picked?.text ?? "", 40)}” · enter saves it · esc cancels`;
|
|
2212
|
+
if (this.kDraft?.kind === "comment") return `a note on “${clip(picked?.text ?? "", 40)}” — every agent reads it under the entry · enter saves`;
|
|
2213
|
+
if (this.kDraft?.kind === "add") return picked ? `a new entry after “${clip(picked.text, 40)}” · enter adds it` : "a new entry · enter adds it";
|
|
2214
|
+
return "e edits the picked entry, c comments on it, n adds one";
|
|
2215
|
+
}
|
|
2216
|
+
case "git": {
|
|
2217
|
+
const pull = this.selectedPull();
|
|
2218
|
+
if (this.gitFocusDraft) return `what should the review of #${pull?.number ?? "?"} look at first? · enter reviews`;
|
|
2219
|
+
return pull ? `f reviews #${pull.number} with a focus you type, v without one` : "r loads the open pull requests";
|
|
2220
|
+
}
|
|
1879
2221
|
case "metrics":
|
|
1880
2222
|
return "";
|
|
1881
2223
|
}
|
|
@@ -1963,6 +2305,28 @@ export class LobbyView implements Component, Focusable {
|
|
|
1963
2305
|
{ key: "n", text: "file a new issue" },
|
|
1964
2306
|
{ key: "r", text: "reload issues" },
|
|
1965
2307
|
];
|
|
2308
|
+
case "knowledge":
|
|
2309
|
+
return [
|
|
2310
|
+
{ key: "↑ ↓", text: "pick a file, or (in the entries) pick an entry; PageUp/PageDown several" },
|
|
2311
|
+
{ key: "enter / ← →", text: "move between the files and their entries" },
|
|
2312
|
+
{ key: "e", text: "edit the picked entry: it comes into the prompt (Shift+Enter for a new line, enter saves)" },
|
|
2313
|
+
{ key: "c", text: "comment on it: a note every agent reads under that entry" },
|
|
2314
|
+
{ key: "n", text: "add an entry after the picked one" },
|
|
2315
|
+
{ key: "d d", text: "delete the picked entry, and its notes" },
|
|
2316
|
+
{ key: "x x", text: "take back the newest note on the picked entry" },
|
|
2317
|
+
{ key: "E", text: "edit the whole file in pi's editor" },
|
|
2318
|
+
{ key: "r", text: "reread the files from disk" },
|
|
2319
|
+
];
|
|
2320
|
+
case "git":
|
|
2321
|
+
return [
|
|
2322
|
+
{ key: "↑ ↓", text: "select a pull request, or scroll its detail (PageUp/PageDown)" },
|
|
2323
|
+
{ key: "enter / ← →", text: "move between the list and the detail" },
|
|
2324
|
+
{ key: "v", text: "review it with a read-only agent on QA's model (nothing is posted to GitHub)" },
|
|
2325
|
+
{ key: "f", text: "review it with a focus you type first (Shift+Enter for a new line, enter reviews)" },
|
|
2326
|
+
{ key: "t", text: "Jev's quick read: size, risk, security, breaking changes, missing tests" },
|
|
2327
|
+
{ key: "x", text: "stop the review that is running" },
|
|
2328
|
+
{ key: "r", text: "reload the pull requests" },
|
|
2329
|
+
];
|
|
1966
2330
|
case "metrics":
|
|
1967
2331
|
return [
|
|
1968
2332
|
{ key: "↑ ↓", text: "select a row of the table" },
|
|
@@ -2013,6 +2377,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2013
2377
|
}
|
|
2014
2378
|
if (this.tab === "lobby" && this.host.masterBusy()) return [["enter", "steer"], ["esc", "stop the oracle"], [k("hide"), "hide"]];
|
|
2015
2379
|
if (this.tab === "plan" && this.lineTarget) return [["enter", "add the comment"], ["esc", "cancel"]];
|
|
2380
|
+
if (this.tab === "knowledge" && this.kDraft) return [["enter", this.kDraft.kind === "edit" ? "save" : this.kDraft.kind === "comment" ? "save the note" : "add it"], ["shift+enter", "new line"], ["esc", "cancel"]];
|
|
2381
|
+
if (this.tab === "git" && this.gitFocusDraft) return [["enter", "review with this focus"], ["shift+enter", "new line"], ["esc", "cancel"]];
|
|
2016
2382
|
const enter = this.tab === "plan" && session?.awaitingAnswers && !this.editor.getText().trim() ? "answer questions" : this.tab === "quickfix" ? "run it" : "send";
|
|
2017
2383
|
return [["enter", enter], ["esc", "browse"], [k("hide"), "hide"]];
|
|
2018
2384
|
}
|
|
@@ -2048,6 +2414,19 @@ export class LobbyView implements Component, Focusable {
|
|
|
2048
2414
|
case "issues":
|
|
2049
2415
|
keys.push(["↑↓", "select"], ["p", "plan it"], ["n", "new"]);
|
|
2050
2416
|
break;
|
|
2417
|
+
case "knowledge":
|
|
2418
|
+
keys.push(["↑↓", "pick"], ["enter", this.kFocus === "list" ? "entries" : "files"]);
|
|
2419
|
+
if (this.kFocus === "detail" || this.pickedEntry()) keys.push(["e", "edit"], ["c", "comment"], ["n", "new"], ["d d", "delete"]);
|
|
2420
|
+
keys.push(["E", "edit file"]);
|
|
2421
|
+
break;
|
|
2422
|
+
case "git": {
|
|
2423
|
+
const pull = this.selectedPull();
|
|
2424
|
+
keys.push(["↑↓", "select"]);
|
|
2425
|
+
if (pull) keys.push(["v", "review"], ["f", "review with a focus"], ["t", "Jev's read"]);
|
|
2426
|
+
if (pull && this.host.reviews.running(pull.number)) keys.push(["x", "stop"]);
|
|
2427
|
+
keys.push(["r", "reload"]);
|
|
2428
|
+
break;
|
|
2429
|
+
}
|
|
2051
2430
|
case "metrics":
|
|
2052
2431
|
keys.push(["↑↓", "select"], ["g", "group"], ["s", "sort"]);
|
|
2053
2432
|
break;
|
|
@@ -2068,7 +2447,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2068
2447
|
...row("Ctrl+C", "clear the prompt, or hide the lobby", inner),
|
|
2069
2448
|
...row("click", "a tab to open it, a pane to give it the keys, a draft plan line to comment on it", inner),
|
|
2070
2449
|
...row("wheel", "scroll the pane under the pointer", inner),
|
|
2071
|
-
...row("
|
|
2450
|
+
...row("prev next", "click on a long pane's bottom edge (▲ prev · page 2/5 · next ▼) to move a page; dimmed at either end", inner),
|
|
2072
2451
|
];
|
|
2073
2452
|
const modes = (inner: number) => [
|
|
2074
2453
|
section("Typing"),
|
|
@@ -2142,6 +2521,26 @@ export class LobbyView implements Component, Focusable {
|
|
|
2142
2521
|
loading: issues.loading, loaded: issues.loaded, ...(issues.error ? { error: issues.error } : {}), ...(issues.notice ? { notice: issues.notice } : {}), tick: this.tick, now,
|
|
2143
2522
|
}, width, height, theme);
|
|
2144
2523
|
}
|
|
2524
|
+
case "knowledge": {
|
|
2525
|
+
const knowledge = this.data.knowledge;
|
|
2526
|
+
this.kSelected = Math.min(this.kSelected, Math.max(0, knowledge.files.length - 1));
|
|
2527
|
+
this.kCursor = Math.min(this.kCursor, Math.max(0, (knowledge.view?.entries.length ?? 1) - 1));
|
|
2528
|
+
const lines = renderKnowledge({ files: knowledge.files, selected: this.kSelected, ...(knowledge.view ? { view: knowledge.view } : {}), cursor: this.kCursor, focus: this.kFocus, offset: this.kOffset, layout: this.kLayout }, width, height, theme);
|
|
2529
|
+
this.kOffset = this.kLayout.offset;
|
|
2530
|
+
return lines;
|
|
2531
|
+
}
|
|
2532
|
+
case "git": {
|
|
2533
|
+
const pulls = this.host.pulls;
|
|
2534
|
+
this.gitSelected = Math.min(this.gitSelected, Math.max(0, pulls.pulls.length - 1));
|
|
2535
|
+
const selected = pulls.pulls[this.gitSelected];
|
|
2536
|
+
const detail = selected ? pulls.details.get(selected.number) : undefined;
|
|
2537
|
+
const review = selected ? this.host.reviews.review(selected.number) : undefined;
|
|
2538
|
+
const read = selected ? this.host.reviews.reads.get(selected.number) : undefined;
|
|
2539
|
+
return renderGit({
|
|
2540
|
+
pulls: pulls.pulls, selected: this.gitSelected, ...(detail ? { detail } : {}), ...(review ? { review } : {}), ...(read ? { read } : {}),
|
|
2541
|
+
focus: this.gitFocus, detailOffset: this.gitDetailOffset, loading: pulls.loading, loaded: pulls.loaded, ...(pulls.error ? { error: pulls.error } : {}), tick: this.tick, now,
|
|
2542
|
+
}, width, height, theme);
|
|
2543
|
+
}
|
|
2145
2544
|
case "metrics": {
|
|
2146
2545
|
const { records, groups, taskTimes, stats, jev } = this.metricsFigures(query);
|
|
2147
2546
|
this.metricsSelected = Math.min(this.metricsSelected, Math.max(0, groups.length - 1));
|
|
@@ -2198,6 +2597,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2198
2597
|
nextMode: session.nextMode,
|
|
2199
2598
|
...(session.seed ? { seed: session.seed } : {}),
|
|
2200
2599
|
...(session.saved ? { saved: session.saved } : {}),
|
|
2600
|
+
...(session.savedParts.length > 1 ? { savedParts: session.savedParts.length } : {}),
|
|
2201
2601
|
...(session.title ? { title: session.title } : {}),
|
|
2202
2602
|
awaitingAnswers: session.awaitingAnswers,
|
|
2203
2603
|
answeredChunks: session.answered.length,
|