pi-bro 0.23.1 → 0.24.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/CHANGELOG.md +15 -0
- package/README.md +10 -9
- package/docs/guided-review.md +8 -12
- package/docs/user-guide.md +35 -17
- package/package.json +3 -2
- package/src/advisor.ts +3 -3
- package/src/backend.ts +65 -42
- package/src/btw.ts +100 -116
- package/src/config-ui.ts +71 -44
- package/src/doctor.ts +21 -24
- package/src/explain-modal.ts +188 -169
- package/src/modal-kit.ts +258 -0
- package/src/review-modal.ts +95 -48
- package/src/review-ui.ts +52 -46
- package/src/ui-capabilities.ts +13 -6
package/src/review-ui.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
matchesKey,
|
|
3
|
-
truncateToWidth,
|
|
4
|
-
visibleWidth,
|
|
5
3
|
wrapTextWithAnsi,
|
|
6
4
|
} from "@earendil-works/pi-tui";
|
|
7
5
|
import {
|
|
@@ -10,8 +8,8 @@ import {
|
|
|
10
8
|
type ExtensionCommandContext,
|
|
11
9
|
} from "@earendil-works/pi-coding-agent";
|
|
12
10
|
import { join } from "node:path";
|
|
13
|
-
import { execute,
|
|
14
|
-
import { readSettings, selectionForCapability } from "./settings.ts";
|
|
11
|
+
import { execute, reviewBackendNotice } from "./backend.ts";
|
|
12
|
+
import { capabilityBackend, readSettings, selectionForCapability } from "./settings.ts";
|
|
15
13
|
import {
|
|
16
14
|
replaceGuide,
|
|
17
15
|
guideEvidence,
|
|
@@ -24,19 +22,22 @@ import {
|
|
|
24
22
|
type ReviewRecord,
|
|
25
23
|
} from "./review.ts";
|
|
26
24
|
import { captureReview, readEvidence, resolveReviewTarget } from "./review-source.ts";
|
|
27
|
-
import { ReviewModal, newReviewView, safeReviewText } from "./review-modal.ts";
|
|
25
|
+
import { REVIEW_OVERLAY, ReviewModal, newReviewView, safeReviewText } from "./review-modal.ts";
|
|
26
|
+
import { formatNotice, modalHeight, openBroOverlay, renderFrame } from "./modal-kit.ts";
|
|
28
27
|
import { consumeWheel } from "./ui-capabilities.ts";
|
|
29
28
|
|
|
29
|
+
export const REVIEW_ACQUIRE_OVERLAY = {
|
|
30
|
+
anchor: "center",
|
|
31
|
+
width: "70%",
|
|
32
|
+
minWidth: 44,
|
|
33
|
+
maxHeight: "60%",
|
|
34
|
+
margin: { top: 1, bottom: 1 },
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
30
37
|
const activeReviews = new Map<string, AbortController | undefined>();
|
|
31
38
|
export function stopGuidedReviews(): void {
|
|
32
39
|
for (const controller of activeReviews.values()) controller?.abort();
|
|
33
40
|
}
|
|
34
|
-
const overlayOptions = {
|
|
35
|
-
anchor: "center" as const,
|
|
36
|
-
width: "100%" as const,
|
|
37
|
-
maxHeight: "100%" as const,
|
|
38
|
-
margin: { top: 1, bottom: 1 },
|
|
39
|
-
};
|
|
40
41
|
|
|
41
42
|
export async function openGuidedReview(
|
|
42
43
|
ctx: ExtensionCommandContext,
|
|
@@ -74,10 +75,12 @@ export async function openGuidedReview(
|
|
|
74
75
|
if (!picked) return;
|
|
75
76
|
record = records[labels.indexOf(picked)];
|
|
76
77
|
} else {
|
|
77
|
-
record = await
|
|
78
|
+
record = await openBroOverlay<ReviewRecord | undefined>(
|
|
79
|
+
ctx,
|
|
78
80
|
(tui, theme, _keys, done) => {
|
|
79
81
|
let controller = new AbortController();
|
|
80
82
|
let message = "Resolving PR identity…";
|
|
83
|
+
let advisory: string | undefined;
|
|
81
84
|
let failed = false;
|
|
82
85
|
let retryable = true;
|
|
83
86
|
let startedAt = Date.now();
|
|
@@ -95,11 +98,15 @@ export async function openGuidedReview(
|
|
|
95
98
|
const acquire = async () => {
|
|
96
99
|
failed = false;
|
|
97
100
|
retryable = true;
|
|
101
|
+
advisory = undefined;
|
|
98
102
|
startedAt = Date.now();
|
|
99
103
|
controller = new AbortController();
|
|
100
104
|
message = "Resolving PR identity…";
|
|
101
105
|
tui.requestRender();
|
|
102
106
|
try {
|
|
107
|
+
// Resolved before PR identity: the advisory is about the backend that will run the
|
|
108
|
+
// review, so it belongs on a resolution failure too, not only a capture failure.
|
|
109
|
+
advisory = reviewBackendNotice(capabilityBackend(await readSettings(), "review"));
|
|
103
110
|
const target = await resolveReviewTarget(value, ctx.cwd, controller.signal);
|
|
104
111
|
if (disposed) return;
|
|
105
112
|
const id = reviewKey(target);
|
|
@@ -125,12 +132,6 @@ export async function openGuidedReview(
|
|
|
125
132
|
if (!disposed) done(existing);
|
|
126
133
|
return;
|
|
127
134
|
}
|
|
128
|
-
const selection = selectionForCapability(await readSettings(), "review");
|
|
129
|
-
const backendError = reviewBackendError(selection.backend ?? "agy");
|
|
130
|
-
if (backendError) {
|
|
131
|
-
retryable = false;
|
|
132
|
-
throw Error(backendError);
|
|
133
|
-
}
|
|
134
135
|
message = `${target.repository} #${target.number} · ${target.head.slice(0, 8)}\nFetching captured source…`;
|
|
135
136
|
tui.requestRender();
|
|
136
137
|
const snapshot = await captureReview(target, root, controller.signal);
|
|
@@ -153,29 +154,22 @@ export async function openGuidedReview(
|
|
|
153
154
|
void acquire();
|
|
154
155
|
return {
|
|
155
156
|
render(width: number) {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
...body.map(row),
|
|
173
|
-
row(failed ? (retryable ? "Esc close · Enter retry" : "Esc close") : "Esc cancel"),
|
|
174
|
-
`╰${border}╯`,
|
|
175
|
-
];
|
|
176
|
-
return lines
|
|
177
|
-
.slice(0, height)
|
|
178
|
-
.map((line) => theme.fg("accent", truncateToWidth(line, width, "")));
|
|
157
|
+
const height = modalHeight(tui, REVIEW_ACQUIRE_OVERLAY);
|
|
158
|
+
const hints = failed ? (retryable ? "Enter retry · Esc close" : "Esc close") : "Esc cancel";
|
|
159
|
+
return renderFrame(theme, width, height, {
|
|
160
|
+
header: () =>
|
|
161
|
+
theme.fg("accent", theme.bold("Bro · Review")) +
|
|
162
|
+
(failed ? "" : theme.fg("dim", ` · ${Math.floor((Date.now() - startedAt) / 1000)}s`)),
|
|
163
|
+
body: (_rows, innerWidth) =>
|
|
164
|
+
wrapTextWithAnsi(safeReviewText(message), innerWidth).map((line) =>
|
|
165
|
+
failed ? theme.fg("error", line) : theme.fg("dim", line),
|
|
166
|
+
),
|
|
167
|
+
// An advisory is information, not a failure: keep it visible even when the fetch fails.
|
|
168
|
+
notice: advisory ? formatNotice(theme, advisory, "warning") : undefined,
|
|
169
|
+
hints,
|
|
170
|
+
fill: false,
|
|
171
|
+
minBody: 1,
|
|
172
|
+
});
|
|
179
173
|
},
|
|
180
174
|
invalidate() {},
|
|
181
175
|
handleMouse: consumeWheel,
|
|
@@ -195,7 +189,7 @@ export async function openGuidedReview(
|
|
|
195
189
|
},
|
|
196
190
|
};
|
|
197
191
|
},
|
|
198
|
-
{ overlay:
|
|
192
|
+
{ overlay: REVIEW_ACQUIRE_OVERLAY },
|
|
199
193
|
);
|
|
200
194
|
}
|
|
201
195
|
if (!record) return;
|
|
@@ -207,7 +201,8 @@ export async function openGuidedReview(
|
|
|
207
201
|
activeReviews.set(state.id, undefined);
|
|
208
202
|
let overlayHandle: { setHidden(hidden: boolean): void } | undefined;
|
|
209
203
|
try {
|
|
210
|
-
await
|
|
204
|
+
await openBroOverlay<void>(
|
|
205
|
+
ctx,
|
|
211
206
|
(tui, theme, _keys, done) => {
|
|
212
207
|
let controller: AbortController | undefined;
|
|
213
208
|
let running: Promise<void> | undefined;
|
|
@@ -309,7 +304,9 @@ export async function openGuidedReview(
|
|
|
309
304
|
await persist();
|
|
310
305
|
const settings = await readSettings();
|
|
311
306
|
const selection = selectionForCapability(settings, "review");
|
|
312
|
-
|
|
307
|
+
const backend = capabilityBackend(settings, "review");
|
|
308
|
+
panel.advisory = reviewBackendNotice(backend) ?? "";
|
|
309
|
+
panel.notice = `${question ? "Answering" : regenerate ? "Regenerating guide" : "Preparing guide"} · ${backend}`;
|
|
313
310
|
panel.stage = {
|
|
314
311
|
step: "investigate",
|
|
315
312
|
detail: question ? "Considering your question" : "Inspecting captured changes",
|
|
@@ -474,6 +471,14 @@ export async function openGuidedReview(
|
|
|
474
471
|
changed,
|
|
475
472
|
});
|
|
476
473
|
void hydrateEvidence();
|
|
474
|
+
// Also on reopen: the advisory is about the selected backend, not about a new turn.
|
|
475
|
+
void readSettings().then(
|
|
476
|
+
(settings) => {
|
|
477
|
+
panel.advisory = reviewBackendNotice(capabilityBackend(settings, "review")) ?? "";
|
|
478
|
+
if (!closed) tui.requestRender();
|
|
479
|
+
},
|
|
480
|
+
() => {},
|
|
481
|
+
);
|
|
477
482
|
if (fresh)
|
|
478
483
|
queueMicrotask(() => {
|
|
479
484
|
if (!closed && !closing) running = run("whole review");
|
|
@@ -509,8 +514,9 @@ export async function openGuidedReview(
|
|
|
509
514
|
};
|
|
510
515
|
},
|
|
511
516
|
{
|
|
512
|
-
overlay:
|
|
513
|
-
|
|
517
|
+
overlay: REVIEW_OVERLAY,
|
|
518
|
+
// Regular-mode wheel scrolling; released while hidden behind the regenerate confirm.
|
|
519
|
+
mouse: true,
|
|
514
520
|
onHandle: (handle) => {
|
|
515
521
|
overlayHandle = handle;
|
|
516
522
|
},
|
package/src/ui-capabilities.ts
CHANGED
|
@@ -34,16 +34,23 @@ export async function insertBroDesktopText(ctx: UiContext, text: string): Promis
|
|
|
34
34
|
/** Only terminal mode uses synchronous insertion; Desktop uses insertBroDesktopText instead. */
|
|
35
35
|
export function canBroInsertIntoEditor(ctx: UiContext): boolean { return ctx.mode === "tui"; }
|
|
36
36
|
|
|
37
|
+
/** Any raw SGR mouse report; regular-mode reporting delivers these to handleInput. */
|
|
38
|
+
export const SGR_MOUSE = /^\x1b\[<(\d+);(\d+);(\d+)[Mm]$/;
|
|
39
|
+
|
|
40
|
+
/** A raw SGR wheel report (Pi 0.84 and regular mode): ±3 lines at a 1-based terminal cell. */
|
|
41
|
+
export function rawWheel(data: string): { delta: number; column: number; row: number } | undefined {
|
|
42
|
+
const match = SGR_MOUSE.exec(data);
|
|
43
|
+
const button = match ? Number.parseInt(match[1]!, 10) : 0;
|
|
44
|
+
if ((button & 64) === 0 || (button & 3) > 1) return undefined;
|
|
45
|
+
return { delta: button & 1 ? 3 : -3, column: Number(match![2]), row: Number(match![3]) };
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export function wheelDelta(data: string): number {
|
|
38
|
-
|
|
39
|
-
if (!match) return 0;
|
|
40
|
-
const button = Number.parseInt(match[1], 10);
|
|
41
|
-
if ((button & 64) === 0) return 0;
|
|
42
|
-
return (button & 3) === 0 ? -3 : (button & 3) === 1 ? 3 : 0;
|
|
49
|
+
return rawWheel(data)?.delta ?? 0;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/** Structural subset of pi-tui's mouse event; Pi 0.84 declares no mouse types. */
|
|
46
|
-
export type BroMouseEvent = { type: string; wheelDelta?: number };
|
|
53
|
+
export type BroMouseEvent = { type: string; x?: number; y?: number; wheelDelta?: number };
|
|
47
54
|
export type BroMouseResult = { handled: boolean; render: boolean } | undefined;
|
|
48
55
|
|
|
49
56
|
/** Pi 1.2+ scrolls the chat behind an overlay whose handleMouse leaves a wheel unhandled. */
|