pi-workflow-engine 0.13.0 → 1.0.0
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/.pi/extensions/pi-workflow-engine/index.ts +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +8 -8
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +56 -19
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +31 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,312 +1,229 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
1
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { decodeKittyPrintable, matchesKey, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { findingPathColumn, renderFindingDetail, renderFindingRow, sortIssuesForDisplay } from "./review-format.ts";
|
|
4
|
+
import { dot, fit, frame, GLYPH, keyHints, overlayHeight, scrollHint } from "../ui/kit.ts";
|
|
5
|
+
import { isCommentableIssue, type ReviewIssue, type ReviewIssueSelection } from "./review-issues.ts";
|
|
6
|
+
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "../ui/workflow-viewer-layout.ts";
|
|
7
|
+
|
|
8
|
+
/** Inner width from which the list and the detail pane sit side by side. */
|
|
8
9
|
const SPLIT_WIDTH = 96;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const SPLIT_LIST_SHARE = 0.5;
|
|
11
|
+
const SPLIT_LIST_MIN = 40;
|
|
12
|
+
const SPLIT_LIST_MAX = 64;
|
|
13
|
+
/** Stacked layout keeps at least this many detail rows below the list. */
|
|
14
|
+
const STACKED_DETAIL_MIN = 6;
|
|
15
|
+
const PREFIX_WIDTH = 4;
|
|
12
16
|
|
|
13
17
|
type ViewerTui = Pick<TUI, "requestRender" | "terminal">;
|
|
18
|
+
type Hint = readonly [key: string, description: string];
|
|
19
|
+
|
|
20
|
+
export interface ReviewResultsViewerContext {
|
|
21
|
+
readonly ui: Pick<ExtensionContext["ui"], "custom">;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function showReviewResultsViewer(
|
|
25
|
+
ctx: ReviewResultsViewerContext,
|
|
26
|
+
issues: readonly ReviewIssue[],
|
|
27
|
+
): Promise<ReviewIssueSelection> {
|
|
28
|
+
return await ctx.ui.custom<ReviewIssueSelection>(
|
|
29
|
+
(tui, theme, _keybindings, done) => new ReviewResultsViewer(issues, tui, theme, done),
|
|
30
|
+
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
14
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Framed findings triage overlay: most severe first, a selectable list with the cursor finding's full
|
|
36
|
+
* detail beside it (or below it when narrow), and selection-gated patch-preview and PR-comment actions.
|
|
37
|
+
*/
|
|
15
38
|
export class ReviewResultsViewer implements Component {
|
|
39
|
+
private readonly issues: readonly ReviewIssue[];
|
|
40
|
+
private readonly reportOrder: readonly ReviewIssue[];
|
|
16
41
|
private readonly selected = new Set<string>();
|
|
17
42
|
private cursor = 0;
|
|
18
43
|
private listScroll = 0;
|
|
19
44
|
private detailScroll = 0;
|
|
20
|
-
private
|
|
45
|
+
private detailClipped = false;
|
|
21
46
|
private warning: string | undefined;
|
|
47
|
+
private detailHeightCache: { readonly width: number; readonly rows: number } | undefined;
|
|
22
48
|
|
|
23
49
|
constructor(
|
|
24
|
-
|
|
25
|
-
private readonly workflowName: string,
|
|
50
|
+
issues: readonly ReviewIssue[],
|
|
26
51
|
private readonly tui: ViewerTui,
|
|
27
52
|
private readonly theme: Theme,
|
|
28
53
|
private readonly done: (result: ReviewIssueSelection) => void,
|
|
29
|
-
) {
|
|
54
|
+
) {
|
|
55
|
+
this.reportOrder = issues;
|
|
56
|
+
this.issues = sortIssuesForDisplay(issues);
|
|
57
|
+
}
|
|
30
58
|
|
|
31
|
-
invalidate(): void {
|
|
59
|
+
invalidate(): void {
|
|
60
|
+
this.detailHeightCache = undefined;
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
render(width: number): string[] {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
64
|
+
const inner = Math.max(1, width - 4);
|
|
65
|
+
this.detailClipped = false;
|
|
66
|
+
const body = this.issues.length === 0
|
|
67
|
+
? [this.theme.fg("muted", "No findings.")]
|
|
68
|
+
: inner >= SPLIT_WIDTH ? this.renderSplit(inner) : this.renderStacked(inner);
|
|
69
|
+
return frame(body, width, this.theme, {
|
|
70
|
+
title: `${this.theme.fg("accent", this.theme.bold("Review findings"))}${dot(this.theme)}${this.theme.fg("muted", "code-review")}`,
|
|
71
|
+
right: this.counts(),
|
|
72
|
+
footer: this.footer(inner),
|
|
73
|
+
});
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
handleInput(data: string): void {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
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")) {
|
|
77
|
+
const key = decodeKittyPrintable(data) ?? data;
|
|
78
|
+
if (key === "q" || matchesKey(data, "escape")) {
|
|
92
79
|
this.done({ action: "close", issueIds: this.selectedIssueIds() });
|
|
80
|
+
return;
|
|
93
81
|
}
|
|
82
|
+
this.warning = undefined;
|
|
83
|
+
if (matchesKey(data, "up") || key === "k") this.moveCursor(-1);
|
|
84
|
+
else if (matchesKey(data, "down") || key === "j") this.moveCursor(1);
|
|
85
|
+
else if (matchesKey(data, "home") || key === "g") this.moveCursor(-this.issues.length);
|
|
86
|
+
else if (matchesKey(data, "end") || key === "G") this.moveCursor(this.issues.length);
|
|
87
|
+
else if (matchesKey(data, "pageUp")) this.detailScroll = Math.max(0, this.detailScroll - this.pageSize());
|
|
88
|
+
else if (matchesKey(data, "pageDown")) this.detailScroll += this.pageSize();
|
|
89
|
+
else if (matchesKey(data, "space") || matchesKey(data, "enter")) this.toggleCurrent();
|
|
90
|
+
else if (key === "a") this.toggleAll();
|
|
91
|
+
else if (key === "f") this.finish("fix");
|
|
92
|
+
else if (key === "c") this.finish("comment");
|
|
93
|
+
this.tui.requestRender();
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
private
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
96
|
+
private renderSplit(inner: number): string[] {
|
|
97
|
+
const listWidth = Math.min(SPLIT_LIST_MAX, Math.max(SPLIT_LIST_MIN, Math.floor(inner * SPLIT_LIST_SHARE)));
|
|
98
|
+
const detailWidth = Math.max(1, inner - listWidth - 3);
|
|
99
|
+
const height = Math.min(this.bodyCeiling(), Math.max(this.issues.length, this.detailHeight(detailWidth)));
|
|
100
|
+
const list = this.renderList(listWidth, height);
|
|
101
|
+
const detail = this.renderDetail(detailWidth, height);
|
|
124
102
|
const divider = this.theme.fg("borderMuted", "│");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
const rows: string[] = [];
|
|
104
|
+
for (let index = 0; index < height; index++) rows.push(`${fit(list[index] ?? "", listWidth)} ${divider} ${detail[index] ?? ""}`);
|
|
105
|
+
return rows;
|
|
128
106
|
}
|
|
129
107
|
|
|
130
|
-
private renderStacked(
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
108
|
+
private renderStacked(inner: number): string[] {
|
|
109
|
+
const ceiling = this.bodyCeiling();
|
|
110
|
+
const detailNeed = this.detailHeight(inner);
|
|
111
|
+
const listRows = Math.max(1, Math.min(this.issues.length, ceiling - 1 - Math.min(detailNeed, STACKED_DETAIL_MIN)));
|
|
112
|
+
const detailRows = Math.max(1, Math.min(detailNeed, ceiling - listRows - 1));
|
|
113
|
+
const detail = this.renderDetail(inner, detailRows);
|
|
134
114
|
return [
|
|
135
|
-
...this.
|
|
136
|
-
this.theme.fg("borderMuted", "─".repeat(
|
|
137
|
-
...
|
|
115
|
+
...this.renderList(inner, listRows),
|
|
116
|
+
this.theme.fg("borderMuted", "─".repeat(inner)),
|
|
117
|
+
...detail,
|
|
118
|
+
...new Array<string>(Math.max(0, detailRows - detail.length)).fill(""),
|
|
138
119
|
];
|
|
139
120
|
}
|
|
140
121
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
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);
|
|
122
|
+
/** Finding rows with cursor and selection marks; a scroll hint takes the last row only when clipped. */
|
|
123
|
+
private renderList(width: number, height: number): string[] {
|
|
124
|
+
const rows = this.issues.length > height ? Math.max(1, height - 1) : height;
|
|
154
125
|
if (this.cursor < this.listScroll) this.listScroll = this.cursor;
|
|
155
|
-
if (
|
|
156
|
-
this.listScroll = Math.
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
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);
|
|
126
|
+
if (this.cursor >= this.listScroll + rows) this.listScroll = this.cursor - rows + 1;
|
|
127
|
+
this.listScroll = Math.max(0, Math.min(this.listScroll, this.issues.length - rows));
|
|
128
|
+
const visible = this.issues.slice(this.listScroll, this.listScroll + rows).map((issue, offset) => this.renderRow(issue, this.listScroll + offset, width));
|
|
129
|
+
const hint = scrollHint(this.listScroll, this.issues.length - this.listScroll - visible.length, this.theme);
|
|
130
|
+
return hint && this.issues.length > height ? [...visible, `${" ".repeat(PREFIX_WIDTH)}${hint}`] : visible;
|
|
167
131
|
}
|
|
168
132
|
|
|
169
|
-
private
|
|
170
|
-
const cursor = index === this.cursor ? this.theme.fg("accent",
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
const row = `${cursor} [${checked}] ${issue.id} ${severity} ${issue.finding.category} ${formatIssueLocation(issue)} — ${issue.finding.summary}`;
|
|
174
|
-
return truncateDisplay(row, width);
|
|
133
|
+
private renderRow(issue: ReviewIssue, index: number, width: number): string {
|
|
134
|
+
const cursor = index === this.cursor ? this.theme.fg("accent", GLYPH.cursor) : " ";
|
|
135
|
+
const mark = this.selected.has(issue.id) ? this.theme.fg("success", GLYPH.running) : this.theme.fg("dim", GLYPH.queued);
|
|
136
|
+
return `${cursor} ${mark} ${renderFindingRow(issue, Math.max(1, width - PREFIX_WIDTH), this.theme, findingPathColumn(this.issues))}`;
|
|
175
137
|
}
|
|
176
138
|
|
|
177
|
-
private
|
|
178
|
-
if (height <= 0) return [];
|
|
139
|
+
private renderDetail(width: number, height: number): string[] {
|
|
179
140
|
const issue = this.issues[this.cursor];
|
|
180
|
-
if (!issue) return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return [heading, ...fitWorkflowViewerRows(visible, bodyHeight), indicator].slice(0, height);
|
|
141
|
+
if (!issue) return [];
|
|
142
|
+
const lines = renderFindingDetail(issue, width, this.theme);
|
|
143
|
+
if (lines.length <= height) {
|
|
144
|
+
this.detailScroll = 0;
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
this.detailClipped = true;
|
|
148
|
+
const rows = Math.max(1, height - 1);
|
|
149
|
+
this.detailScroll = Math.max(0, Math.min(this.detailScroll, lines.length - rows));
|
|
150
|
+
const below = lines.length - this.detailScroll - rows;
|
|
151
|
+
const hint = scrollHint(this.detailScroll, below, this.theme) ?? "";
|
|
152
|
+
return [...lines.slice(this.detailScroll, this.detailScroll + rows), hint];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The tallest finding detail at this width, so the overlay keeps one height while the cursor moves. */
|
|
156
|
+
private detailHeight(width: number): number {
|
|
157
|
+
if (this.detailHeightCache?.width !== width) {
|
|
158
|
+
const rows = Math.max(0, ...this.issues.map((issue) => renderFindingDetail(issue, width, this.theme).length));
|
|
159
|
+
this.detailHeightCache = { width, rows };
|
|
160
|
+
}
|
|
161
|
+
return this.detailHeightCache.rows;
|
|
202
162
|
}
|
|
203
163
|
|
|
204
|
-
private
|
|
205
|
-
|
|
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
|
-
);
|
|
164
|
+
private bodyCeiling(): number {
|
|
165
|
+
return Math.max(3, overlayHeight(this.tui.terminal.rows, Number.MAX_SAFE_INTEGER) - 2);
|
|
211
166
|
}
|
|
212
167
|
|
|
213
|
-
private
|
|
214
|
-
|
|
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
|
-
);
|
|
168
|
+
private pageSize(): number {
|
|
169
|
+
return Math.max(1, Math.floor(this.bodyCeiling() / 2));
|
|
221
170
|
}
|
|
222
171
|
|
|
223
|
-
private
|
|
224
|
-
|
|
172
|
+
private counts(): string {
|
|
173
|
+
const findings = `${this.issues.length} finding${this.issues.length === 1 ? "" : "s"}`;
|
|
174
|
+
const selected = this.selected.size > 0 ? this.theme.fg("success", `${this.selected.size} selected`) : undefined;
|
|
175
|
+
return [this.theme.fg("muted", findings), selected].filter(Boolean).join(dot(this.theme));
|
|
225
176
|
}
|
|
226
177
|
|
|
227
|
-
|
|
228
|
-
|
|
178
|
+
/** Key hints in the bottom border; a transient warning replaces them until the next key. */
|
|
179
|
+
private footer(inner: number): string {
|
|
180
|
+
if (this.warning) return this.theme.fg("warning", `${GLYPH.warning} ${this.warning}`);
|
|
181
|
+
const scroll: Hint[] = this.detailClipped ? [["pgup/pgdn", "scroll"]] : [];
|
|
182
|
+
const variants: Hint[][] = [
|
|
183
|
+
[["↑↓", "move"], ["space", "select"], ["a", "all"], ["f", "patch preview"], ["c", "PR comment"], ...scroll, ["q", "close"]],
|
|
184
|
+
[["↑↓", "move"], ["space", "select"], ["a", "all"], ["f", "patch"], ["c", "comment"], ...scroll, ["q", "close"]],
|
|
185
|
+
[["space", "select"], ["f", "patch"], ["c", "comment"], ["q", "close"]],
|
|
186
|
+
[["space", "select"], ["q", "close"]],
|
|
187
|
+
];
|
|
188
|
+
const rendered = variants.map((hints) => keyHints(hints, this.theme));
|
|
189
|
+
return rendered.find((hints) => visibleWidth(hints) <= inner - 2) ?? rendered[rendered.length - 1] ?? "";
|
|
229
190
|
}
|
|
230
191
|
|
|
231
192
|
private moveCursor(delta: number): void {
|
|
232
193
|
if (this.issues.length === 0) return;
|
|
233
|
-
|
|
234
|
-
this.detailScroll = 0;
|
|
235
|
-
this.
|
|
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();
|
|
194
|
+
const next = Math.min(this.issues.length - 1, Math.max(0, this.cursor + delta));
|
|
195
|
+
if (next !== this.cursor) this.detailScroll = 0;
|
|
196
|
+
this.cursor = next;
|
|
249
197
|
}
|
|
250
198
|
|
|
251
199
|
private toggleCurrent(): void {
|
|
252
200
|
const issue = this.issues[this.cursor];
|
|
253
201
|
if (!issue) return;
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
256
|
-
this.requestRender();
|
|
202
|
+
if (this.selected.has(issue.id)) this.selected.delete(issue.id);
|
|
203
|
+
else this.selected.add(issue.id);
|
|
257
204
|
}
|
|
258
205
|
|
|
259
|
-
private
|
|
206
|
+
private toggleAll(): void {
|
|
260
207
|
const allSelected = this.issues.length > 0 && this.issues.every((issue) => this.selected.has(issue.id));
|
|
261
208
|
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();
|
|
209
|
+
if (!allSelected) for (const issue of this.issues) this.selected.add(issue.id);
|
|
267
210
|
}
|
|
268
211
|
|
|
269
|
-
private
|
|
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 {
|
|
212
|
+
private finish(action: "fix" | "comment"): void {
|
|
276
213
|
const issueIds = this.selectedIssueIds();
|
|
277
214
|
if (issueIds.length === 0) {
|
|
278
|
-
this.warning = `Select
|
|
279
|
-
|
|
215
|
+
this.warning = `Select findings first (space or a), then press ${action === "fix" ? "f" : "c"}`;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (action === "comment" && !this.issues.some((issue) => this.selected.has(issue.id) && isCommentableIssue(issue))) {
|
|
219
|
+
this.warning = "None of the selected findings has a file and line to comment on";
|
|
280
220
|
return;
|
|
281
221
|
}
|
|
282
222
|
this.done({ action, issueIds });
|
|
283
223
|
}
|
|
284
224
|
|
|
285
|
-
|
|
286
|
-
if (this.selected.has(id)) this.selected.delete(id);
|
|
287
|
-
else this.selected.add(id);
|
|
288
|
-
}
|
|
289
|
-
|
|
225
|
+
/** Selected ids in report order, which keeps follow-up prompts and comments stable. */
|
|
290
226
|
private selectedIssueIds(): string[] {
|
|
291
|
-
return this.
|
|
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";
|
|
227
|
+
return this.reportOrder.filter((issue) => this.selected.has(issue.id)).map((issue) => issue.id);
|
|
311
228
|
}
|
|
312
229
|
}
|
|
@@ -2,14 +2,14 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { sessionKey } from "../session-identity.ts";
|
|
3
3
|
import { resolveWorkflowRunOptions, type ResolvedWorkflowRunOptions } from "../options.ts";
|
|
4
4
|
import type { LoadedWorkflow } from "../types.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { WorkflowResultEnvelope } from "../workflow-execution.ts";
|
|
6
6
|
import { handleReviewViewerAction } from "./review-actions.ts";
|
|
7
7
|
import { ReviewFixBudgetLedger } from "./review-budget.ts";
|
|
8
8
|
import { toReviewIssues, type ReviewIssue, type ReviewIssueSelection } from "./review-issues.ts";
|
|
9
|
-
import {
|
|
10
|
-
import type
|
|
9
|
+
import { showReviewResultsViewer } from "./review-results-viewer.ts";
|
|
10
|
+
import { isReviewReport, type ReviewReport } from "./review-report.ts";
|
|
11
11
|
|
|
12
|
-
interface RetainedCodeReviewResult {
|
|
12
|
+
export interface RetainedCodeReviewResult {
|
|
13
13
|
readonly report: ReviewReport;
|
|
14
14
|
readonly concurrency: number;
|
|
15
15
|
readonly parallelSubmissionLimit?: number;
|
|
@@ -22,8 +22,8 @@ export interface ReviewSessionCoordinatorDependencies {
|
|
|
22
22
|
ctx: ExtensionContext,
|
|
23
23
|
workflow: LoadedWorkflow,
|
|
24
24
|
options: ResolvedWorkflowRunOptions,
|
|
25
|
-
) => Promise<
|
|
26
|
-
readonly publish: (
|
|
25
|
+
) => Promise<WorkflowResultEnvelope>;
|
|
26
|
+
readonly publish: (envelope: WorkflowResultEnvelope) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Owns retained code-review state and every path that presents or acts on it. */
|
|
@@ -35,46 +35,39 @@ export class ReviewSessionCoordinator {
|
|
|
35
35
|
private readonly dependencies: ReviewSessionCoordinatorDependencies,
|
|
36
36
|
) {}
|
|
37
37
|
|
|
38
|
+
/** Retains a code-review result for this session and returns it; any other result retains nothing. */
|
|
38
39
|
remember(
|
|
39
40
|
ctx: ExtensionContext,
|
|
40
|
-
|
|
41
|
+
envelope: WorkflowResultEnvelope,
|
|
41
42
|
options: ResolvedWorkflowRunOptions,
|
|
42
|
-
):
|
|
43
|
-
|
|
44
|
-
if (name !== "code-review") return;
|
|
43
|
+
): RetainedCodeReviewResult | undefined {
|
|
44
|
+
if (envelope.name !== "code-review") return undefined;
|
|
45
45
|
const key = sessionKey(ctx);
|
|
46
|
-
const report =
|
|
47
|
-
if (!report) {
|
|
46
|
+
const report = envelope.result;
|
|
47
|
+
if (!isReviewReport(report)) {
|
|
48
48
|
this.sessions.delete(key);
|
|
49
|
-
return;
|
|
49
|
+
return undefined;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
const retained: RetainedCodeReviewResult = {
|
|
52
52
|
report,
|
|
53
53
|
concurrency: options.concurrency,
|
|
54
54
|
parallelSubmissionLimit: options.parallelSubmissionLimit ?? undefined,
|
|
55
55
|
perf: options.perf,
|
|
56
|
-
budget: new ReviewFixBudgetLedger(options.budget,
|
|
57
|
-
}
|
|
56
|
+
budget: new ReviewFixBudgetLedger(options.budget, envelope.usage),
|
|
57
|
+
};
|
|
58
|
+
this.sessions.set(key, retained);
|
|
59
|
+
return retained;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/** Opens the findings viewer for a result remember() just retained, when explicitly requested. */
|
|
60
63
|
async present(
|
|
61
64
|
ctx: ExtensionContext,
|
|
62
|
-
|
|
65
|
+
retained: RetainedCodeReviewResult | undefined,
|
|
63
66
|
options: ResolvedWorkflowRunOptions,
|
|
64
67
|
): Promise<void> {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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);
|
|
68
|
+
if (!retained || retained.report.findings.length === 0) return;
|
|
69
|
+
if (options.resultViewer !== "open" || ctx.mode !== "tui" || !ctx.hasUI) return;
|
|
70
|
+
await this.openAndHandle(ctx, retained);
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
async reopen(ctx: ExtensionContext): Promise<void> {
|
|
@@ -92,18 +85,15 @@ export class ReviewSessionCoordinator {
|
|
|
92
85
|
return;
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
await this.openAndHandle(ctx, retained
|
|
88
|
+
await this.openAndHandle(ctx, retained);
|
|
96
89
|
}
|
|
97
90
|
|
|
98
91
|
dispose(ctx: ExtensionContext): void {
|
|
99
92
|
this.sessions.delete(sessionKey(ctx));
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
private async openAndHandle(
|
|
103
|
-
|
|
104
|
-
retained: RetainedCodeReviewResult,
|
|
105
|
-
issues: readonly ReviewIssue[],
|
|
106
|
-
): Promise<void> {
|
|
95
|
+
private async openAndHandle(ctx: ExtensionContext, retained: RetainedCodeReviewResult): Promise<void> {
|
|
96
|
+
const issues = toReviewIssues(retained.report);
|
|
107
97
|
let action: ReviewIssueSelection | undefined;
|
|
108
98
|
try {
|
|
109
99
|
action = await showReviewResultsViewer(ctx, issues);
|
|
@@ -132,8 +122,8 @@ export class ReviewSessionCoordinator {
|
|
|
132
122
|
try {
|
|
133
123
|
const followUp = await handleReviewViewerAction(this.pi, ctx, action, issues, retained.report.reviewContext);
|
|
134
124
|
if (!followUp) return;
|
|
135
|
-
const
|
|
136
|
-
this.dependencies.publish(
|
|
125
|
+
const envelope = await this.dependencies.runFollowUp(ctx, followUp, followUpOptions(retained, ctx.signal));
|
|
126
|
+
this.dependencies.publish(envelope);
|
|
137
127
|
} finally {
|
|
138
128
|
if (lease?.ok) lease.release();
|
|
139
129
|
}
|