@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
package/src/lobby/view.ts
CHANGED
|
@@ -22,16 +22,26 @@ import type { BackgroundSession } from "./sessions.ts";
|
|
|
22
22
|
import type { QuickFixQueue } from "./quickfix.ts";
|
|
23
23
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
24
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
25
|
+
import { agentsIndicator, workingAgents } from "./mini.ts";
|
|
25
26
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
26
|
-
import { beside, bold, box, fit, highlight, 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";
|
|
27
28
|
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
28
29
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
29
30
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
30
31
|
import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
31
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";
|
|
32
39
|
import { filterRecords, renderMetrics } from "./tabs/metrics.ts";
|
|
33
40
|
|
|
34
|
-
|
|
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;
|
|
35
45
|
export type TabId = (typeof TAB_IDS)[number];
|
|
36
46
|
|
|
37
47
|
export const TAB_LABELS: Record<TabId, string> = {
|
|
@@ -41,6 +51,8 @@ export const TAB_LABELS: Record<TabId, string> = {
|
|
|
41
51
|
quickfix: "Quick fix",
|
|
42
52
|
issues: "Issues",
|
|
43
53
|
metrics: "Metrics",
|
|
54
|
+
git: "Git",
|
|
55
|
+
knowledge: "Knowledge",
|
|
44
56
|
};
|
|
45
57
|
|
|
46
58
|
/** The tabs on show: Issues only while `lobby.issues` switches it on. */
|
|
@@ -148,6 +160,8 @@ export interface LobbyHost {
|
|
|
148
160
|
newPlanner(seed?: PlannerSeed, seats?: readonly PanelMember[]): PlanningSession;
|
|
149
161
|
/** The oracle puts the panel's open questions to the user, one questionnaire at a time; returns a notice. */
|
|
150
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>;
|
|
151
165
|
/** The seats a new session starts with, from settings. */
|
|
152
166
|
defaultPanel(): readonly PanelMember[];
|
|
153
167
|
/** Planning rounds before the oracle finalizes alone (`lobby.maxPlanningRounds`); 0 = unlimited. */
|
|
@@ -155,6 +169,14 @@ export interface LobbyHost {
|
|
|
155
169
|
/** `model · thinking` a panel seat runs on. */
|
|
156
170
|
seatLabel(member: PanelMember): string;
|
|
157
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>;
|
|
158
180
|
/** The Issues tab is switched on (`lobby.issues`). */
|
|
159
181
|
issuesEnabled(): boolean;
|
|
160
182
|
/** Which panes show (`lobby.panels`), and remembering a change. */
|
|
@@ -196,6 +218,10 @@ export interface LobbyHost {
|
|
|
196
218
|
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
197
219
|
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
198
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;
|
|
199
225
|
requestRender(): void;
|
|
200
226
|
now?(): number;
|
|
201
227
|
}
|
|
@@ -208,6 +234,9 @@ export const DATA_REFRESH_MS = 2000;
|
|
|
208
234
|
const NOTICE_MS = 6000;
|
|
209
235
|
/** Lines one wheel notch scrolls. */
|
|
210
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;
|
|
211
240
|
|
|
212
241
|
/** The prompt editor, with its target written into the top border. */
|
|
213
242
|
class LobbyEditor extends Editor {
|
|
@@ -338,19 +367,33 @@ export class LobbyView implements Component, Focusable {
|
|
|
338
367
|
private issueFocus: "list" | "detail" = "list";
|
|
339
368
|
private issueDetailOffset = 0;
|
|
340
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;
|
|
341
383
|
private metricsSelected = 0;
|
|
342
384
|
private metricsBy: GroupBy = "model";
|
|
343
385
|
private metricsSort: SortKey = "runs";
|
|
344
386
|
|
|
345
387
|
/** The Tasks tab lists archived tasks too (v). */
|
|
346
388
|
showArchived = false;
|
|
347
|
-
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 } = {
|
|
348
390
|
tasks: [],
|
|
349
391
|
plans: [],
|
|
350
392
|
archived: [],
|
|
351
393
|
comments: new Map(),
|
|
352
394
|
metrics: [],
|
|
353
395
|
classifierMetrics: [],
|
|
396
|
+
knowledge: { files: [] },
|
|
354
397
|
at: 0,
|
|
355
398
|
};
|
|
356
399
|
|
|
@@ -414,6 +457,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
414
457
|
|| Boolean(this.host.planner()?.busy)
|
|
415
458
|
|| this.host.sessions().some((session) => session.busy)
|
|
416
459
|
|| (this.issuesOn && this.host.issues.loading)
|
|
460
|
+
|| (this.tab === "git" && this.host.pulls.loading)
|
|
461
|
+
|| this.host.reviews.busy
|
|
417
462
|
|| zen.runs.some((run) => run.status === "running")
|
|
418
463
|
|| Boolean(zen.task && !zen.task.paused && !TERMINAL_STATES.includes(zen.task.state));
|
|
419
464
|
}
|
|
@@ -432,6 +477,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
432
477
|
const now = this.now();
|
|
433
478
|
if (!force && now - this.data.at < DATA_REFRESH_MS) return;
|
|
434
479
|
this.dataVersion += 1;
|
|
480
|
+
this.host.refreshWorkspace?.();
|
|
435
481
|
this.data = {
|
|
436
482
|
tasks: this.host.tasks(),
|
|
437
483
|
plans: this.host.plans(),
|
|
@@ -439,6 +485,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
439
485
|
comments: new Map(),
|
|
440
486
|
metrics: this.tab === "metrics" ? this.host.metrics() : this.data.metrics,
|
|
441
487
|
classifierMetrics: this.tab === "metrics" ? this.host.classifierMetrics?.() ?? [] : this.data.classifierMetrics,
|
|
488
|
+
knowledge: this.tab === "knowledge" ? this.readKnowledge() : this.data.knowledge,
|
|
442
489
|
at: now,
|
|
443
490
|
};
|
|
444
491
|
if (this.tab === "tasks") {
|
|
@@ -447,6 +494,19 @@ export class LobbyView implements Component, Focusable {
|
|
|
447
494
|
}
|
|
448
495
|
}
|
|
449
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
|
+
|
|
450
510
|
private say(text: string, kind: Notice["kind"] = "info"): void {
|
|
451
511
|
this.notice = { text, at: this.now(), kind };
|
|
452
512
|
}
|
|
@@ -474,12 +534,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
474
534
|
setTab(tab: TabId): void {
|
|
475
535
|
if (tab === this.tab) return;
|
|
476
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
|
+
}
|
|
477
542
|
this.drafts[this.tab] = this.editor.getText();
|
|
478
543
|
this.tab = tab;
|
|
479
544
|
this.editor.setText(this.drafts[tab] ?? "");
|
|
480
545
|
this.commentTarget = undefined;
|
|
481
546
|
this.lineTarget = undefined;
|
|
482
547
|
this.issueDraft = false;
|
|
548
|
+
this.gitFocusDraft = false;
|
|
483
549
|
this.armed = undefined;
|
|
484
550
|
this.help = false;
|
|
485
551
|
this.picking = false;
|
|
@@ -488,6 +554,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
488
554
|
this.setMode(PROMPT_FIRST.has(tab) ? "type" : "browse");
|
|
489
555
|
this.refreshData(true);
|
|
490
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();
|
|
491
559
|
this.host.requestRender();
|
|
492
560
|
}
|
|
493
561
|
|
|
@@ -687,6 +755,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
687
755
|
}
|
|
688
756
|
this.commentTarget = undefined;
|
|
689
757
|
this.issueDraft = false;
|
|
758
|
+
this.gitFocusDraft = false;
|
|
759
|
+
if (this.kDraft) {
|
|
760
|
+
this.kDraft = undefined;
|
|
761
|
+
this.editor.setText("");
|
|
762
|
+
}
|
|
690
763
|
this.newSession = false;
|
|
691
764
|
if (this.lineTarget) {
|
|
692
765
|
this.lineTarget = undefined;
|
|
@@ -713,7 +786,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
713
786
|
if (matchesKey(data, Key.home) || matchesKey(data, Key.end)) return this.scrollToEdge(matchesKey(data, Key.home));
|
|
714
787
|
const tabs = this.tabs();
|
|
715
788
|
if (!PROMPT_FIRST.has(this.tab) && /^[1-9]$/.test(data) && Number(data) <= tabs.length) return this.setTab(tabs[Number(data) - 1]!);
|
|
716
|
-
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");
|
|
717
790
|
if (this.tabCommand(data)) return;
|
|
718
791
|
const printable = isPrintable(data);
|
|
719
792
|
if (printable && PROMPT_FIRST.has(this.tab)) {
|
|
@@ -735,6 +808,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
735
808
|
return this.fixFocus;
|
|
736
809
|
case "issues":
|
|
737
810
|
return this.issueFocus;
|
|
811
|
+
case "git":
|
|
812
|
+
return this.gitFocus;
|
|
813
|
+
case "knowledge":
|
|
814
|
+
return this.kFocus;
|
|
738
815
|
case "metrics":
|
|
739
816
|
return "table";
|
|
740
817
|
}
|
|
@@ -781,6 +858,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
781
858
|
case "issues":
|
|
782
859
|
this.issueFocus = direction < 0 ? "list" : "detail";
|
|
783
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;
|
|
784
867
|
case "metrics":
|
|
785
868
|
return;
|
|
786
869
|
}
|
|
@@ -806,17 +889,44 @@ export class LobbyView implements Component, Focusable {
|
|
|
806
889
|
void this.loadIssueDetail();
|
|
807
890
|
}
|
|
808
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);
|
|
809
902
|
case "metrics":
|
|
810
903
|
return this.scrollPane("table", delta);
|
|
811
904
|
}
|
|
812
905
|
}
|
|
813
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
|
+
|
|
814
924
|
/**
|
|
815
925
|
* Scroll one pane of the current tab by `delta` lines, positive toward its
|
|
816
926
|
* end. Lists move their selection instead; the draft moves its cursor (the
|
|
817
927
|
* wheel scrolls it and the cursor follows only when it would leave the screen).
|
|
818
928
|
*/
|
|
819
|
-
private scrollPane(pane: string, delta: number, wheel = false): void {
|
|
929
|
+
private scrollPane(pane: string, delta: number, wheel = false, page = false): void {
|
|
820
930
|
if (delta === 0) return;
|
|
821
931
|
switch (pane) {
|
|
822
932
|
case "conversation":
|
|
@@ -828,13 +938,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
828
938
|
this.planOffset = Math.max(0, this.planOffset - delta);
|
|
829
939
|
break;
|
|
830
940
|
case "draft":
|
|
831
|
-
if (wheel) this.wheelDraft(delta);
|
|
941
|
+
if (wheel || page) this.wheelDraft(delta);
|
|
832
942
|
else this.moveCursor(delta);
|
|
833
943
|
break;
|
|
834
944
|
case "list":
|
|
835
|
-
if (this.tab === "tasks") this.selectTask(this.tasksSelected + Math.sign(delta));
|
|
945
|
+
if (this.tab === "tasks") this.selectTask(this.tasksSelected + (page ? delta : Math.sign(delta)));
|
|
836
946
|
else if (this.tab === "quickfix") {
|
|
837
|
-
this.fixSelected = Math.max(0, Math.min(this.fixJobs().length - 1, this.fixSelected + Math.sign(delta)));
|
|
947
|
+
this.fixSelected = Math.max(0, Math.min(this.fixJobs().length - 1, this.fixSelected + (page ? delta : Math.sign(delta))));
|
|
838
948
|
this.fixDetailOffset = 0;
|
|
839
949
|
}
|
|
840
950
|
break;
|
|
@@ -843,7 +953,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
843
953
|
else if (this.tab === "quickfix") this.fixDetailOffset = Math.max(0, this.fixDetailOffset + delta);
|
|
844
954
|
break;
|
|
845
955
|
case "table":
|
|
846
|
-
this.metricsSelected = Math.max(0, this.metricsSelected + (wheel ? Math.sign(delta) : delta));
|
|
956
|
+
this.metricsSelected = Math.max(0, this.metricsSelected + (wheel && !page ? Math.sign(delta) : delta));
|
|
847
957
|
break;
|
|
848
958
|
}
|
|
849
959
|
this.clampScroll();
|
|
@@ -989,6 +1099,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
989
1099
|
if (row?.kind === "task" && !this.data.comments.has(row.id)) this.data.comments.set(row.id, this.host.comments(row.id));
|
|
990
1100
|
}
|
|
991
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
|
+
|
|
992
1107
|
private async loadIssueDetail(): Promise<void> {
|
|
993
1108
|
const issue = this.host.issues.issues[this.issueSelected];
|
|
994
1109
|
if (issue) await this.host.issues.detail(issue.number);
|
|
@@ -1050,6 +1165,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
1050
1165
|
return false;
|
|
1051
1166
|
case "issues":
|
|
1052
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);
|
|
1053
1172
|
case "metrics":
|
|
1054
1173
|
if (data === "g") return (this.metricsBy = this.metricsBy === "model" ? "model-kind" : "model", this.metricsSelected = 0), true;
|
|
1055
1174
|
if (data === "s") return (this.metricsSort = SORT_KEYS[(SORT_KEYS.indexOf(this.metricsSort) + 1) % SORT_KEYS.length]!), true;
|
|
@@ -1461,17 +1580,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
1461
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}`);
|
|
1462
1581
|
}
|
|
1463
1582
|
|
|
1464
|
-
/**
|
|
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
|
+
*/
|
|
1465
1588
|
savePlan(): void {
|
|
1466
1589
|
const session = this.host.planner();
|
|
1467
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");
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
+
);
|
|
1475
1602
|
}
|
|
1476
1603
|
|
|
1477
1604
|
/** The oracle puts the round's questions to the user through the questionnaire. */
|
|
@@ -1588,6 +1715,162 @@ export class LobbyView implements Component, Focusable {
|
|
|
1588
1715
|
return false;
|
|
1589
1716
|
}
|
|
1590
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
|
+
|
|
1591
1874
|
/* --------------------------------------------------------------- mouse */
|
|
1592
1875
|
|
|
1593
1876
|
/** Mouse events when pi runs full screen (pi-tui dispatches them to the overlay). */
|
|
@@ -1617,7 +1900,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1617
1900
|
/** The wheel scrolls the pane under the pointer (the focused one when it is over none). */
|
|
1618
1901
|
private wheel(direction: number, x = -1, y = -1): void {
|
|
1619
1902
|
if (this.help) return;
|
|
1620
|
-
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);
|
|
1621
1904
|
this.scrollPane(this.paneAt(x, y) ?? this.focusedPane(), direction * WHEEL_LINES, true);
|
|
1622
1905
|
}
|
|
1623
1906
|
|
|
@@ -1640,6 +1923,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
1640
1923
|
if (this.tab === "lobby" && pane) this.homeFocus = pane as HomePane;
|
|
1641
1924
|
if (this.tab === "tasks" && (pane === "list" || pane === "detail")) this.tasksFocus = pane;
|
|
1642
1925
|
if (this.tab === "quickfix" && (pane === "list" || pane === "detail")) this.fixFocus = pane;
|
|
1926
|
+
// The page buttons in a scrolling pane's bottom border scroll it a page or two.
|
|
1927
|
+
const paneBox = pane ? this.panes.get(pane) : undefined;
|
|
1928
|
+
if (pane && paneBox && paneBox.total > paneBox.rows && row === paneBox.top + paneBox.height - 1) {
|
|
1929
|
+
const button = pagerButton(paneBox.width, x - paneBox.left, paneBox.total, paneBox.rows);
|
|
1930
|
+
if (button) {
|
|
1931
|
+
this.scrollPane(pane, button * pageStep(paneBox.rows), false, true);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1643
1935
|
if (this.tab !== "plan" || row < 0 || !this.host.planner()) return;
|
|
1644
1936
|
const layout = this.planLayout;
|
|
1645
1937
|
// The draft pane: its border sits one row above its first line and two columns left of its text.
|
|
@@ -1664,6 +1956,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1664
1956
|
private submit(text: string): void {
|
|
1665
1957
|
const body = text.trim();
|
|
1666
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");
|
|
1667
1960
|
// An empty enter on the Plan tab opens the panel's questions; on the Lobby, a background session's question.
|
|
1668
1961
|
if (this.tab === "plan" && !this.lineTarget && this.host.planner()?.awaitingAnswers) this.answerQuestions();
|
|
1669
1962
|
if (this.tab === "lobby" && !this.newSession) this.answerSessionDialog();
|
|
@@ -1703,6 +1996,16 @@ export class LobbyView implements Component, Focusable {
|
|
|
1703
1996
|
this.say("filing the issue…");
|
|
1704
1997
|
void this.host.issues.create(body);
|
|
1705
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);
|
|
1706
2009
|
case "metrics":
|
|
1707
2010
|
return;
|
|
1708
2011
|
}
|
|
@@ -1792,46 +2095,83 @@ export class LobbyView implements Component, Focusable {
|
|
|
1792
2095
|
else if (session?.awaitingAnswers) badges.plan = `${session.questions.length}?`;
|
|
1793
2096
|
if (this.host.quickfix.running) badges.quickfix = spinner(this.tick);
|
|
1794
2097
|
if (this.issuesOn && this.host.issues.issues.length > 0) badges.issues = String(this.host.issues.issues.length);
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const
|
|
1798
|
-
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) => {
|
|
1799
2101
|
const number = paint(theme, tab === this.tab ? "accent" : "dim", String(index + 1));
|
|
1800
2102
|
const name = tab === this.tab ? bold(theme, paint(theme, "text", TAB_LABELS[tab])) : paint(theme, "muted", TAB_LABELS[tab]);
|
|
1801
2103
|
const badge = badges[tab] ? ` ${paint(theme, tab === "plan" && session?.awaitingAnswers ? "warning" : "accent", badges[tab]!)}` : "";
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
left += cell;
|
|
1806
|
-
spans.push({ tab, from, to: textWidth(left) });
|
|
1807
|
-
}
|
|
1808
|
-
this.tabSpans = spans;
|
|
1809
|
-
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
|
+
});
|
|
1810
2107
|
const entry = this.viewedEntry();
|
|
1811
2108
|
const help = paint(theme, "dim", `${keyLabel(this.keys.help)} keys `);
|
|
1812
2109
|
const auto = entry.auto ? ` ${bold(theme, paint(theme, "success", "⟳ AUTO"))}` : "";
|
|
1813
2110
|
const waiting = this.host.sessions().reduce((count, session) => count + session.dialogs.length, 0);
|
|
1814
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`)}` : "";
|
|
1815
2114
|
let choices: string[];
|
|
1816
2115
|
if (entry.view.kind !== "here") {
|
|
1817
2116
|
// Another session in view: its name and what it is doing lead, so it is never mistaken for this window.
|
|
1818
2117
|
const working = entry.status === "working";
|
|
1819
2118
|
const dot = paint(theme, working ? "accent" : "dim", working ? spinner(this.tick) : "◆");
|
|
1820
2119
|
const where = paint(theme, "muted", `${entry.name} · ${entry.status}`);
|
|
1821
|
-
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} `];
|
|
1822
2121
|
} else {
|
|
1823
2122
|
const dot = paint(theme, this.host.masterBusy() ? "accent" : "dim", this.host.masterBusy() ? spinner(this.tick) : "●");
|
|
1824
2123
|
const state = zen.task ? paint(theme, "muted", zen.task.paused ? `${zen.task.state} (paused)` : zen.task.state) : "";
|
|
1825
2124
|
// Long task ids give way to the state, then to the dot alone.
|
|
1826
2125
|
choices = zen.task
|
|
1827
|
-
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
1828
|
-
: [`${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
|
+
}
|
|
2142
|
+
}
|
|
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) });
|
|
1829
2149
|
}
|
|
1830
|
-
|
|
2150
|
+
this.tabSpans = spans;
|
|
1831
2151
|
const gap = width - textWidth(left) - textWidth(status);
|
|
1832
2152
|
return gap >= 1 ? `${left}${" ".repeat(gap)}${status}` : fit(left, width);
|
|
1833
2153
|
}
|
|
1834
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
|
+
|
|
1835
2175
|
private promptLabel(): string {
|
|
1836
2176
|
const zen = this.host.zen();
|
|
1837
2177
|
const browsing = this.mode === "browse";
|
|
@@ -1866,6 +2206,18 @@ export class LobbyView implements Component, Focusable {
|
|
|
1866
2206
|
return this.host.quickfix.running ? "describe a quick change; it runs after the current one" : "describe a quick change";
|
|
1867
2207
|
case "issues":
|
|
1868
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
|
+
}
|
|
1869
2221
|
case "metrics":
|
|
1870
2222
|
return "";
|
|
1871
2223
|
}
|
|
@@ -1953,6 +2305,28 @@ export class LobbyView implements Component, Focusable {
|
|
|
1953
2305
|
{ key: "n", text: "file a new issue" },
|
|
1954
2306
|
{ key: "r", text: "reload issues" },
|
|
1955
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
|
+
];
|
|
1956
2330
|
case "metrics":
|
|
1957
2331
|
return [
|
|
1958
2332
|
{ key: "↑ ↓", text: "select a row of the table" },
|
|
@@ -1978,7 +2352,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
1978
2352
|
if (this.help) return fit(`${badge("KEYS", "accent")} ${this.chips([["any key", "closes"]], theme)}`, width);
|
|
1979
2353
|
if (this.searching) return fit(`${badge("SEARCH", "warning")} ${this.chips([["enter", "keep"], ["esc", "clear"], ["↑↓", "results"], [k("nextTab"), "next tab"]], theme)}`, width);
|
|
1980
2354
|
const mode = this.mode === "type" ? badge("TYPE", "accent") : badge("BROWSE", "muted");
|
|
1981
|
-
|
|
2355
|
+
const keys = `${mode} ${this.chips(this.hintChips(), theme)}`;
|
|
2356
|
+
// The subagents at work sit at the right end of the line, when the keys leave room.
|
|
2357
|
+
const working = this.viewing.kind === "here" ? agentsIndicator(workingAgents(this.host.zen().runs, this.host.quickfix.running), this.now(), spinner(this.tick), width - textWidth(keys) - 3, theme) : "";
|
|
2358
|
+
const gap = width - textWidth(keys) - textWidth(working) - 1;
|
|
2359
|
+
return working && gap >= 2 ? `${keys}${" ".repeat(gap)}${working} ` : fit(keys, width);
|
|
1982
2360
|
}
|
|
1983
2361
|
|
|
1984
2362
|
/**
|
|
@@ -1999,6 +2377,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1999
2377
|
}
|
|
2000
2378
|
if (this.tab === "lobby" && this.host.masterBusy()) return [["enter", "steer"], ["esc", "stop the oracle"], [k("hide"), "hide"]];
|
|
2001
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"]];
|
|
2002
2382
|
const enter = this.tab === "plan" && session?.awaitingAnswers && !this.editor.getText().trim() ? "answer questions" : this.tab === "quickfix" ? "run it" : "send";
|
|
2003
2383
|
return [["enter", enter], ["esc", "browse"], [k("hide"), "hide"]];
|
|
2004
2384
|
}
|
|
@@ -2034,6 +2414,19 @@ export class LobbyView implements Component, Focusable {
|
|
|
2034
2414
|
case "issues":
|
|
2035
2415
|
keys.push(["↑↓", "select"], ["p", "plan it"], ["n", "new"]);
|
|
2036
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
|
+
}
|
|
2037
2430
|
case "metrics":
|
|
2038
2431
|
keys.push(["↑↓", "select"], ["g", "group"], ["s", "sort"]);
|
|
2039
2432
|
break;
|
|
@@ -2054,6 +2447,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2054
2447
|
...row("Ctrl+C", "clear the prompt, or hide the lobby", inner),
|
|
2055
2448
|
...row("click", "a tab to open it, a pane to give it the keys, a draft plan line to comment on it", inner),
|
|
2056
2449
|
...row("wheel", "scroll the pane under the pointer", inner),
|
|
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),
|
|
2057
2451
|
];
|
|
2058
2452
|
const modes = (inner: number) => [
|
|
2059
2453
|
section("Typing"),
|
|
@@ -2127,6 +2521,26 @@ export class LobbyView implements Component, Focusable {
|
|
|
2127
2521
|
loading: issues.loading, loaded: issues.loaded, ...(issues.error ? { error: issues.error } : {}), ...(issues.notice ? { notice: issues.notice } : {}), tick: this.tick, now,
|
|
2128
2522
|
}, width, height, theme);
|
|
2129
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
|
+
}
|
|
2130
2544
|
case "metrics": {
|
|
2131
2545
|
const { records, groups, taskTimes, stats, jev } = this.metricsFigures(query);
|
|
2132
2546
|
this.metricsSelected = Math.min(this.metricsSelected, Math.max(0, groups.length - 1));
|
|
@@ -2183,6 +2597,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2183
2597
|
nextMode: session.nextMode,
|
|
2184
2598
|
...(session.seed ? { seed: session.seed } : {}),
|
|
2185
2599
|
...(session.saved ? { saved: session.saved } : {}),
|
|
2600
|
+
...(session.savedParts.length > 1 ? { savedParts: session.savedParts.length } : {}),
|
|
2186
2601
|
...(session.title ? { title: session.title } : {}),
|
|
2187
2602
|
awaitingAnswers: session.awaitingAnswers,
|
|
2188
2603
|
answeredChunks: session.answered.length,
|