@jameslovespancakes/pi-plus 1.0.11 → 1.0.13
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 +67 -22
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +9 -4
- package/src/core/accounts/oauth-pool.ts +137 -0
- package/src/core/accounts/registry.ts +12 -25
- package/src/core/accounts/routing.ts +144 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/identity.ts +54 -0
- package/src/core/anthropic/oauth.ts +3 -0
- package/src/core/anthropic/quota.ts +232 -78
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +11 -10
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +46 -8
- package/src/core/codex/store.ts +29 -16
- package/src/core/config.ts +16 -17
- package/src/core/store.ts +13 -12
- package/src/domains/agents/format.ts +97 -0
- package/src/domains/agents/index.ts +58 -47
- package/src/domains/compact/archive.ts +140 -0
- package/src/domains/compact/chunking.ts +166 -0
- package/src/domains/compact/index.ts +452 -0
- package/src/domains/compact/jev.ts +242 -0
- package/src/domains/compact/policy.ts +255 -0
- package/src/domains/compact/types.ts +79 -0
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +61 -40
- package/src/domains/subscriptions/accounts.ts +53 -36
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +79 -103
- package/src/domains/subscriptions/providers/anthropic.ts +42 -22
- package/src/domains/subscriptions/providers/codex.ts +146 -113
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +325 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +177 -0
- package/src/domains/workflows/runtime/agent-failure.ts +14 -0
- package/src/domains/workflows/runtime/agent-limits.ts +66 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +225 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +79 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +579 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +28 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +435 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +164 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +636 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +615 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
- package/src/ui/usage-bars.ts +1 -2
- package/src/core/codex/oauth.ts +0 -129
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { renderIssueDetailLines } from "./review-format.ts";
|
|
4
|
+
import { formatIssueLocation, type ReviewIssue, type ReviewIssueSelection } from "./review-issues.ts";
|
|
5
|
+
import { truncateDisplay } from "../ui/workflow-format.ts";
|
|
6
|
+
import { fitWorkflowViewerRow, fitWorkflowViewerRows, workflowViewerHeight } from "../ui/workflow-viewer-layout.ts";
|
|
7
|
+
|
|
8
|
+
const SPLIT_WIDTH = 96;
|
|
9
|
+
const LIST_MIN_WIDTH = 34;
|
|
10
|
+
const LIST_MAX_WIDTH = 48;
|
|
11
|
+
const DETAIL_SCROLL_STEP = 5;
|
|
12
|
+
|
|
13
|
+
type ViewerTui = Pick<TUI, "requestRender" | "terminal">;
|
|
14
|
+
|
|
15
|
+
export class ReviewResultsViewer implements Component {
|
|
16
|
+
private readonly selected = new Set<string>();
|
|
17
|
+
private cursor = 0;
|
|
18
|
+
private listScroll = 0;
|
|
19
|
+
private detailScroll = 0;
|
|
20
|
+
private detailsExpanded = true;
|
|
21
|
+
private warning: string | undefined;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly issues: readonly ReviewIssue[],
|
|
25
|
+
private readonly workflowName: string,
|
|
26
|
+
private readonly tui: ViewerTui,
|
|
27
|
+
private readonly theme: Theme,
|
|
28
|
+
private readonly done: (result: ReviewIssueSelection) => void,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
invalidate(): void {}
|
|
32
|
+
|
|
33
|
+
render(width: number): string[] {
|
|
34
|
+
const outerWidth = Math.max(4, width);
|
|
35
|
+
const contentWidth = Math.max(1, outerWidth - 4);
|
|
36
|
+
const innerHeight = Math.max(1, workflowViewerHeight(this.tui.terminal.rows) - 2);
|
|
37
|
+
const body = fitWorkflowViewerRows(this.renderBody(contentWidth, innerHeight), innerHeight);
|
|
38
|
+
return [
|
|
39
|
+
this.theme.fg("borderAccent", `╭${"─".repeat(Math.max(0, outerWidth - 2))}╮`),
|
|
40
|
+
...body.map((line) => this.borderedRow(line, contentWidth)),
|
|
41
|
+
this.theme.fg("borderAccent", `╰${"─".repeat(Math.max(0, outerWidth - 2))}╯`),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleInput(data: string): void {
|
|
46
|
+
const digitJump = digitJumpIndex(data);
|
|
47
|
+
if (digitJump !== undefined) {
|
|
48
|
+
this.jumpTo(digitJump);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (matchesKey(data, "up")) {
|
|
53
|
+
this.moveCursor(-1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (matchesKey(data, "down")) {
|
|
57
|
+
this.moveCursor(1);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (matchesKey(data, "space") || data === " ") {
|
|
61
|
+
this.toggleCurrent();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (data === "a") {
|
|
65
|
+
this.toggleAllVisible();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\r") {
|
|
69
|
+
this.detailsExpanded = !this.detailsExpanded;
|
|
70
|
+
this.detailScroll = 0;
|
|
71
|
+
this.warning = undefined;
|
|
72
|
+
this.requestRender();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (matchesKey(data, "pageUp") || matchesKey(data, "left")) {
|
|
76
|
+
this.scrollDetail(-DETAIL_SCROLL_STEP);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (matchesKey(data, "pageDown") || matchesKey(data, "right")) {
|
|
80
|
+
this.scrollDetail(DETAIL_SCROLL_STEP);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (data === "f") {
|
|
84
|
+
this.finishSelected("fix");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (data === "c") {
|
|
88
|
+
this.finishSelected("comment");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (data === "q" || matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
92
|
+
this.done({ action: "close", issueIds: this.selectedIssueIds() });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private renderBody(width: number, height: number): string[] {
|
|
97
|
+
const warning = this.warning ? this.theme.fg("warning", this.warning) : "";
|
|
98
|
+
const chromeRows = 5;
|
|
99
|
+
const contentHeight = Math.max(1, height - chromeRows);
|
|
100
|
+
let content: string[];
|
|
101
|
+
if (this.issues.length === 0) {
|
|
102
|
+
content = fitWorkflowViewerRows([this.theme.fg("success", "No findings.")], contentHeight);
|
|
103
|
+
} else if (width >= SPLIT_WIDTH) {
|
|
104
|
+
content = this.renderSplit(width, contentHeight);
|
|
105
|
+
} else {
|
|
106
|
+
content = this.renderStacked(width, contentHeight);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
this.headerLine(width),
|
|
111
|
+
warning,
|
|
112
|
+
this.theme.fg("borderMuted", "─".repeat(width)),
|
|
113
|
+
...fitWorkflowViewerRows(content, contentHeight),
|
|
114
|
+
this.theme.fg("borderMuted", "─".repeat(width)),
|
|
115
|
+
this.helpLine(width),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private renderSplit(width: number, height: number): string[] {
|
|
120
|
+
const listWidth = Math.min(LIST_MAX_WIDTH, Math.max(LIST_MIN_WIDTH, Math.floor(width * 0.38)));
|
|
121
|
+
const detailWidth = Math.max(1, width - listWidth - 3);
|
|
122
|
+
const list = this.renderIssuePane(listWidth, height);
|
|
123
|
+
const details = this.renderDetailPane(detailWidth, height);
|
|
124
|
+
const divider = this.theme.fg("borderMuted", "│");
|
|
125
|
+
return Array.from({ length: height }, (_value, index) =>
|
|
126
|
+
`${fitWorkflowViewerRow(list[index] ?? "", listWidth)} ${divider} ${fitWorkflowViewerRow(details[index] ?? "", detailWidth)}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private renderStacked(width: number, height: number): string[] {
|
|
131
|
+
if (height < 7) return this.renderCompact(width, height);
|
|
132
|
+
const listHeight = Math.max(3, Math.min(height - 4, Math.floor(height * 0.38)));
|
|
133
|
+
const detailHeight = height - listHeight - 1;
|
|
134
|
+
return [
|
|
135
|
+
...this.renderIssuePane(width, listHeight),
|
|
136
|
+
this.theme.fg("borderMuted", "─".repeat(width)),
|
|
137
|
+
...this.renderDetailPane(width, detailHeight),
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private renderCompact(width: number, height: number): string[] {
|
|
142
|
+
if (height <= 0) return [];
|
|
143
|
+
const issue = this.issues[this.cursor];
|
|
144
|
+
const selected = issue ? this.renderIssueRow(issue, this.cursor, width) : this.theme.fg("dim", "No finding selected.");
|
|
145
|
+
if (height === 1) return [selected];
|
|
146
|
+
return [selected, ...this.renderDetailPane(width, height - 1)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private renderIssuePane(width: number, height: number): string[] {
|
|
150
|
+
if (height <= 0) return [];
|
|
151
|
+
const itemRows = Math.max(0, height - 2);
|
|
152
|
+
if (itemRows === 0) this.listScroll = 0;
|
|
153
|
+
const maxStart = Math.max(0, this.issues.length - itemRows);
|
|
154
|
+
if (this.cursor < this.listScroll) this.listScroll = this.cursor;
|
|
155
|
+
if (itemRows > 0 && this.cursor >= this.listScroll + itemRows) this.listScroll = this.cursor - itemRows + 1;
|
|
156
|
+
this.listScroll = Math.min(maxStart, Math.max(0, this.listScroll));
|
|
157
|
+
const end = Math.min(this.issues.length, this.listScroll + itemRows);
|
|
158
|
+
const visible = this.issues.slice(this.listScroll, end).map((issue, offset) => this.renderIssueRow(issue, this.listScroll + offset, width));
|
|
159
|
+
const range = this.issues.length === 0 || itemRows === 0 ? `0/${this.issues.length}` : `${this.listScroll + 1}–${end}/${this.issues.length}`;
|
|
160
|
+
const arrows = scrollArrows(this.listScroll > 0, end < this.issues.length);
|
|
161
|
+
const heading = truncateDisplay(
|
|
162
|
+
`${this.theme.fg("accent", this.theme.bold("Findings"))} ${this.theme.fg("muted", `${this.cursor + 1}/${Math.max(1, this.issues.length)}`)}`,
|
|
163
|
+
width,
|
|
164
|
+
);
|
|
165
|
+
const indicator = truncateDisplay(this.theme.fg("dim", `${range}${arrows ? ` ${arrows}` : ""}`), width);
|
|
166
|
+
return [heading, ...fitWorkflowViewerRows(visible, itemRows), indicator].slice(0, height);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private renderIssueRow(issue: ReviewIssue, index: number, width: number): string {
|
|
170
|
+
const cursor = index === this.cursor ? this.theme.fg("accent", ">") : " ";
|
|
171
|
+
const checked = this.selected.has(issue.id) ? this.theme.fg("success", "x") : " ";
|
|
172
|
+
const severity = this.theme.fg(severityColor(issue.finding.severity), issue.finding.severity);
|
|
173
|
+
const row = `${cursor} [${checked}] ${issue.id} ${severity} ${issue.finding.category} ${formatIssueLocation(issue)} — ${issue.finding.summary}`;
|
|
174
|
+
return truncateDisplay(row, width);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private renderDetailPane(width: number, height: number): string[] {
|
|
178
|
+
if (height <= 0) return [];
|
|
179
|
+
const issue = this.issues[this.cursor];
|
|
180
|
+
if (!issue) return fitWorkflowViewerRows([this.theme.fg("dim", "No finding selected.")], height);
|
|
181
|
+
|
|
182
|
+
const bodyHeight = Math.max(0, height - 2);
|
|
183
|
+
const heading = truncateDisplay(
|
|
184
|
+
`${this.theme.fg("accent", this.theme.bold("Details"))} ${this.theme.fg("muted", issue.id)}`,
|
|
185
|
+
width,
|
|
186
|
+
);
|
|
187
|
+
const lines = this.detailsExpanded
|
|
188
|
+
? renderIssueDetailLines(issue, this.theme, width)
|
|
189
|
+
: [issue.finding.summary, this.theme.fg("dim", "Press enter to expand details.")];
|
|
190
|
+
if (bodyHeight === 0) this.detailScroll = 0;
|
|
191
|
+
const maxScroll = Math.max(0, lines.length - bodyHeight);
|
|
192
|
+
this.detailScroll = Math.min(maxScroll, Math.max(0, this.detailScroll));
|
|
193
|
+
const end = Math.min(lines.length, this.detailScroll + bodyHeight);
|
|
194
|
+
const visible = lines.slice(this.detailScroll, end).map((line) => truncateDisplay(line, width));
|
|
195
|
+
const range = lines.length === 0 || bodyHeight === 0 ? `0/${lines.length}` : `${this.detailScroll + 1}–${end}/${lines.length}`;
|
|
196
|
+
const arrows = scrollArrows(this.detailScroll > 0, bodyHeight === 0 ? lines.length > 0 : end < lines.length);
|
|
197
|
+
const indicator = truncateDisplay(
|
|
198
|
+
this.theme.fg("dim", `${this.detailsExpanded ? "Lines" : "Collapsed"} ${range}${arrows ? ` ${arrows}` : ""}`),
|
|
199
|
+
width,
|
|
200
|
+
);
|
|
201
|
+
return [heading, ...fitWorkflowViewerRows(visible, bodyHeight), indicator].slice(0, height);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private headerLine(width: number): string {
|
|
205
|
+
const count = `${this.issues.length} finding${this.issues.length === 1 ? "" : "s"}`;
|
|
206
|
+
const selected = `${this.selected.size} selected`;
|
|
207
|
+
return truncateDisplay(
|
|
208
|
+
`${this.theme.fg("accent", this.theme.bold("Review results"))} ${this.theme.fg("muted", this.workflowName)} ${this.theme.fg("dim", `· ${count} · ${selected}`)}`,
|
|
209
|
+
width,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private helpLine(width: number): string {
|
|
214
|
+
const help = width >= 110
|
|
215
|
+
? "↑↓ findings · 1-9 jump · space select · a all · enter details · ←→ scroll · f fix · c comment · q close"
|
|
216
|
+
: "q close · ↑↓ move · space select · enter details · ←→ scroll · f fix · c comment";
|
|
217
|
+
return truncateDisplay(
|
|
218
|
+
this.theme.fg("dim", help),
|
|
219
|
+
width,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private borderedRow(content: string, width: number): string {
|
|
224
|
+
return `${this.theme.fg("border", "│")} ${fitWorkflowViewerRow(content, width)} ${this.theme.fg("border", "│")}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private requestRender(): void {
|
|
228
|
+
this.tui.requestRender();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private moveCursor(delta: number): void {
|
|
232
|
+
if (this.issues.length === 0) return;
|
|
233
|
+
this.cursor = Math.min(this.issues.length - 1, Math.max(0, this.cursor + delta));
|
|
234
|
+
this.detailScroll = 0;
|
|
235
|
+
this.warning = undefined;
|
|
236
|
+
this.requestRender();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private jumpTo(index: number): void {
|
|
240
|
+
if (index < 0 || index >= this.issues.length) {
|
|
241
|
+
this.warning = `No finding ${index + 1}.`;
|
|
242
|
+
this.requestRender();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.cursor = index;
|
|
246
|
+
this.detailScroll = 0;
|
|
247
|
+
this.warning = undefined;
|
|
248
|
+
this.requestRender();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private toggleCurrent(): void {
|
|
252
|
+
const issue = this.issues[this.cursor];
|
|
253
|
+
if (!issue) return;
|
|
254
|
+
this.toggleIssue(issue.id);
|
|
255
|
+
this.warning = undefined;
|
|
256
|
+
this.requestRender();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private toggleAllVisible(): void {
|
|
260
|
+
const allSelected = this.issues.length > 0 && this.issues.every((issue) => this.selected.has(issue.id));
|
|
261
|
+
this.selected.clear();
|
|
262
|
+
if (!allSelected) {
|
|
263
|
+
for (const issue of this.issues) this.selected.add(issue.id);
|
|
264
|
+
}
|
|
265
|
+
this.warning = undefined;
|
|
266
|
+
this.requestRender();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private scrollDetail(delta: number): void {
|
|
270
|
+
this.detailScroll = Math.max(0, this.detailScroll + delta);
|
|
271
|
+
this.warning = undefined;
|
|
272
|
+
this.requestRender();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private finishSelected(action: "fix" | "comment"): void {
|
|
276
|
+
const issueIds = this.selectedIssueIds();
|
|
277
|
+
if (issueIds.length === 0) {
|
|
278
|
+
this.warning = `Select at least one finding before pressing ${action === "fix" ? "f" : "c"}.`;
|
|
279
|
+
this.requestRender();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.done({ action, issueIds });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private toggleIssue(id: string): void {
|
|
286
|
+
if (this.selected.has(id)) this.selected.delete(id);
|
|
287
|
+
else this.selected.add(id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private selectedIssueIds(): string[] {
|
|
291
|
+
return this.issues.filter((issue) => this.selected.has(issue.id)).map((issue) => issue.id);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function scrollArrows(hasAbove: boolean, hasBelow: boolean): string {
|
|
296
|
+
return `${hasAbove ? "↑" : ""}${hasBelow ? "↓" : ""}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function digitJumpIndex(data: string): number | undefined {
|
|
300
|
+
return data.length === 1 && data >= "1" && data <= "9" ? Number(data) - 1 : undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function severityColor(severity: ReviewIssue["finding"]["severity"]): Parameters<Theme["fg"]>[0] {
|
|
304
|
+
switch (severity) {
|
|
305
|
+
case "high":
|
|
306
|
+
return "error";
|
|
307
|
+
case "medium":
|
|
308
|
+
return "warning";
|
|
309
|
+
case "low":
|
|
310
|
+
return "muted";
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { sessionKey } from "../session-identity.ts";
|
|
3
|
+
import { resolveWorkflowRunOptions, type ResolvedWorkflowRunOptions } from "../options.ts";
|
|
4
|
+
import type { LoadedWorkflow } from "../types.ts";
|
|
5
|
+
import type { WorkflowExecution } from "../workflow-execution.ts";
|
|
6
|
+
import { handleReviewViewerAction } from "./review-actions.ts";
|
|
7
|
+
import { ReviewFixBudgetLedger } from "./review-budget.ts";
|
|
8
|
+
import { toReviewIssues, type ReviewIssue, type ReviewIssueSelection } from "./review-issues.ts";
|
|
9
|
+
import { codeReviewReport, decideReviewResultsPresentation, showReviewResultsViewer } from "./review-results-flow.ts";
|
|
10
|
+
import type { ReviewReport } from "./review-report.ts";
|
|
11
|
+
|
|
12
|
+
interface RetainedCodeReviewResult {
|
|
13
|
+
readonly report: ReviewReport;
|
|
14
|
+
readonly concurrency?: number;
|
|
15
|
+
readonly parallelSubmissionLimit?: number;
|
|
16
|
+
readonly perf: boolean;
|
|
17
|
+
readonly budget: ReviewFixBudgetLedger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReviewSessionCoordinatorDependencies {
|
|
21
|
+
readonly runFollowUp: (
|
|
22
|
+
ctx: ExtensionContext,
|
|
23
|
+
workflow: LoadedWorkflow,
|
|
24
|
+
options: ResolvedWorkflowRunOptions,
|
|
25
|
+
) => Promise<WorkflowExecution>;
|
|
26
|
+
readonly publish: (execution: WorkflowExecution) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Owns retained code-review state and every path that presents or acts on it. */
|
|
30
|
+
export class ReviewSessionCoordinator {
|
|
31
|
+
private readonly sessions = new Map<string, RetainedCodeReviewResult>();
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly pi: Pick<ExtensionAPI, "sendUserMessage" | "exec">,
|
|
35
|
+
private readonly dependencies: ReviewSessionCoordinatorDependencies,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
remember(
|
|
39
|
+
ctx: ExtensionContext,
|
|
40
|
+
execution: WorkflowExecution,
|
|
41
|
+
options: ResolvedWorkflowRunOptions,
|
|
42
|
+
): void {
|
|
43
|
+
const name = execution.envelope.name;
|
|
44
|
+
if (name !== "code-review") return;
|
|
45
|
+
const key = sessionKey(ctx);
|
|
46
|
+
const report = codeReviewReport(name, execution.envelope.result);
|
|
47
|
+
if (!report) {
|
|
48
|
+
this.sessions.delete(key);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.sessions.set(key, {
|
|
52
|
+
report,
|
|
53
|
+
concurrency: options.concurrency ?? undefined,
|
|
54
|
+
parallelSubmissionLimit: options.parallelSubmissionLimit ?? undefined,
|
|
55
|
+
perf: options.perf,
|
|
56
|
+
budget: new ReviewFixBudgetLedger(options.budget, execution.envelope.usage),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async present(
|
|
61
|
+
ctx: ExtensionContext,
|
|
62
|
+
execution: WorkflowExecution,
|
|
63
|
+
options: ResolvedWorkflowRunOptions,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const name = execution.envelope.name;
|
|
66
|
+
const decision = decideReviewResultsPresentation({
|
|
67
|
+
workflowName: name,
|
|
68
|
+
result: execution.envelope.result,
|
|
69
|
+
mode: ctx.mode,
|
|
70
|
+
hasUI: ctx.hasUI,
|
|
71
|
+
resultViewer: options.resultViewer,
|
|
72
|
+
});
|
|
73
|
+
if (decision.kind !== "open") return;
|
|
74
|
+
|
|
75
|
+
const retained = this.sessions.get(sessionKey(ctx));
|
|
76
|
+
if (!retained) return;
|
|
77
|
+
await this.openAndHandle(ctx, retained, decision.issues);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async reopen(ctx: ExtensionContext): Promise<void> {
|
|
81
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
82
|
+
ctx.ui.notify("Code-review results viewer requires the TUI", "warning");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const retained = this.sessions.get(sessionKey(ctx));
|
|
86
|
+
if (!retained) {
|
|
87
|
+
ctx.ui.notify("No code-review result is available yet. Run /workflow code-review first.", "warning");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (retained.report.findings.length === 0) {
|
|
91
|
+
ctx.ui.notify("The last code review had no findings", "info");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await this.openAndHandle(ctx, retained, toReviewIssues("code-review", retained.report));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
dispose(ctx: ExtensionContext): void {
|
|
99
|
+
this.sessions.delete(sessionKey(ctx));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async openAndHandle(
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
retained: RetainedCodeReviewResult,
|
|
105
|
+
issues: readonly ReviewIssue[],
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
let action: ReviewIssueSelection | undefined;
|
|
108
|
+
try {
|
|
109
|
+
action = await showReviewResultsViewer(ctx, issues);
|
|
110
|
+
} catch {
|
|
111
|
+
ctx.ui.notify("Review completed, but the findings viewer could not be opened.", "warning");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await this.runAction(ctx, retained, action, issues);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async runAction(
|
|
118
|
+
ctx: ExtensionContext,
|
|
119
|
+
retained: RetainedCodeReviewResult,
|
|
120
|
+
action: ReviewIssueSelection | undefined,
|
|
121
|
+
issues: readonly ReviewIssue[],
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const lease = action?.action === "fix" ? retained.budget.acquire() : undefined;
|
|
124
|
+
if (lease && !lease.ok) {
|
|
125
|
+
const message = lease.reason === "exhausted"
|
|
126
|
+
? "Review-fix previews are unavailable because this review's output-token budget is exhausted."
|
|
127
|
+
: "A review-fix preview is already running for this review.";
|
|
128
|
+
ctx.ui.notify(message, "warning");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const followUp = await handleReviewViewerAction(this.pi, ctx, action, issues, retained.report.reviewContext);
|
|
134
|
+
if (!followUp) return;
|
|
135
|
+
const execution = await this.dependencies.runFollowUp(ctx, followUp, followUpOptions(retained, ctx.signal));
|
|
136
|
+
this.dependencies.publish(execution);
|
|
137
|
+
} finally {
|
|
138
|
+
if (lease?.ok) lease.release();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function followUpOptions(retained: RetainedCodeReviewResult, signal: AbortSignal | undefined): ResolvedWorkflowRunOptions {
|
|
144
|
+
return resolveWorkflowRunOptions({
|
|
145
|
+
concurrency: retained.concurrency,
|
|
146
|
+
parallelSubmissionLimit: retained.parallelSubmissionLimit,
|
|
147
|
+
budget: retained.budget.remaining,
|
|
148
|
+
perf: retained.perf,
|
|
149
|
+
resultViewer: "skip",
|
|
150
|
+
signal,
|
|
151
|
+
onUsageSnapshot: (usage) => retained.budget.record(usage),
|
|
152
|
+
}, {});
|
|
153
|
+
}
|