@henryqw/pi-pr 4.0.5 → 4.0.6
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 +5 -5
- package/extensions/pr-github.ts +14 -2
- package/extensions/pr.ts +24 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,17 +129,17 @@ The sweep runs existing non-destructive checks on the clean committed `HEAD` bef
|
|
|
129
129
|
|
|
130
130
|
### Refresh
|
|
131
131
|
|
|
132
|
-
The footer and widget load at session start. A directory outside a Git worktree stays silent
|
|
132
|
+
The footer and widget load once at session start. A directory outside a Git worktree stays silent. The UI shows `PR · status unavailable` for other discovery failures and reports only a generic error.
|
|
133
133
|
|
|
134
|
-
They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. During creation, intermediate refreshes wait until the workflow settles. They also refresh after any successful delegated task settles.
|
|
134
|
+
They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. During creation, intermediate refreshes wait until the workflow settles. They also refresh after any successful delegated task settles. There is no periodic presentation refresh, so external changes may leave the footer and widget stale indefinitely. `/pr` reads fresh state before routing or acting and remains authoritative.
|
|
135
135
|
|
|
136
|
-
The create widget stays hidden until the local branch has a commit beyond its creation point. `/pr` replaces any hint with routing feedback while it selects a route. The feedback clears before route interaction. A dispatched workflow keeps the widget hidden until the agent settles. Direct and no-action routes refresh it after completion. A failed command restores the prior hint and schedules a refresh.
|
|
136
|
+
The create widget stays hidden until the local branch has a commit beyond its creation point. `/pr` replaces any hint with routing feedback while it selects a route. The feedback clears before route interaction. A dispatched workflow keeps the widget hidden until the agent settles. Direct and no-action routes refresh it after completion. A failed command restores the prior hint and schedules a refresh, except when fresh lookup hits the GitHub API quota: it shows the sanitized message `GitHub API rate limit exhausted; retry after GitHub resets it` and does not immediately retry.
|
|
137
137
|
|
|
138
138
|
Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
|
|
139
139
|
|
|
140
140
|
### Session identity
|
|
141
141
|
|
|
142
|
-
The extension records one configured PR identity in the Pi session. It stores only the PR URL, number, host, head identity, and configured target identity. It does not store lifecycle, CI, review, readiness, or base state.
|
|
142
|
+
The extension records one configured PR identity in the Pi session. It stores only the PR URL, number, host, head identity, and configured target identity. It does not store lifecycle, CI, review, readiness, or base state. Event-driven refreshes do not add duplicate entries, and no repository cache file is created.
|
|
143
143
|
|
|
144
144
|
Normal discovery always runs first. If the configured remote ref was deleted, the footer and `/pr` may reload the exact observed PR URL. The current host, repository, branch, remote, ref, and local HEAD must still match the observation. Repository names use case-insensitive GitHub matching.
|
|
145
145
|
|
|
@@ -149,7 +149,7 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
|
|
|
149
149
|
|
|
150
150
|
- `/pr` accepts creation syntax only as a leading `--base BRANCH`, followed by optional creation guidance. It does not open a browser.
|
|
151
151
|
- It does not run `/done` or `/sweep`.
|
|
152
|
-
-
|
|
152
|
+
- Presentation refreshes do not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
|
|
153
153
|
- It does not enable auto-merge or add a merge queue.
|
|
154
154
|
- It does not rebase the local branch, overwrite concurrent remote updates, delete branches, or clean up worktrees. Creation uses exact leases plus ancestry checks; an empty lease is only an atomic absence check.
|
|
155
155
|
- Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
|
package/extensions/pr-github.ts
CHANGED
|
@@ -66,6 +66,13 @@ export class PullRequestLoadError extends Error {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export class GitHubRateLimitError extends PullRequestLoadError {
|
|
70
|
+
constructor() {
|
|
71
|
+
super("GitHub API rate limit exhausted; retry after GitHub resets it");
|
|
72
|
+
this.name = "GitHubRateLimitError";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export type PullRequestRef = {
|
|
70
77
|
repository: string;
|
|
71
78
|
ref: string;
|
|
@@ -369,7 +376,11 @@ async function invoke(
|
|
|
369
376
|
return parseCommandOutput(result, action);
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
function commandFailure(action: string, result: CommandOutput): never {
|
|
379
|
+
function commandFailure(action: string, result: CommandOutput, command?: string): never {
|
|
380
|
+
if (
|
|
381
|
+
command === "gh" && !result.killed && result.code !== 0 &&
|
|
382
|
+
result.stderr.includes("GraphQL: API rate limit exceeded")
|
|
383
|
+
) throw new GitHubRateLimitError();
|
|
373
384
|
fail(action, result.killed ? "command was cancelled" : `exit code ${result.code}`);
|
|
374
385
|
}
|
|
375
386
|
|
|
@@ -381,7 +392,7 @@ async function execute(
|
|
|
381
392
|
args: string[],
|
|
382
393
|
): Promise<CommandOutput> {
|
|
383
394
|
const result = await invoke(pi, context, action, command, args);
|
|
384
|
-
if (result.killed || result.code !== 0) commandFailure(action, result);
|
|
395
|
+
if (result.killed || result.code !== 0) commandFailure(action, result, command);
|
|
385
396
|
return result;
|
|
386
397
|
}
|
|
387
398
|
|
|
@@ -1265,6 +1276,7 @@ async function readRemoteAuthority(
|
|
|
1265
1276
|
) fail("Read fetch repository", "fetch and push repositories do not match");
|
|
1266
1277
|
return { fetchSource: pushUrl.fetchSource, repository: pushRepository };
|
|
1267
1278
|
} catch (error) {
|
|
1279
|
+
if (error instanceof GitHubRateLimitError) throw error;
|
|
1268
1280
|
if (!strict && error instanceof PullRequestLoadError) return null;
|
|
1269
1281
|
throw error;
|
|
1270
1282
|
}
|
package/extensions/pr.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./pr-command.ts";
|
|
20
20
|
import { PullRequestCreator, type CreatePullRequestOptions } from "./pr-create.ts";
|
|
21
21
|
import {
|
|
22
|
+
GitHubRateLimitError,
|
|
22
23
|
loadCurrentPullRequest,
|
|
23
24
|
parsePullRequestObservation,
|
|
24
25
|
pullRequestObservation,
|
|
@@ -37,7 +38,6 @@ import {
|
|
|
37
38
|
} from "./pr-ui.ts";
|
|
38
39
|
import { PullRequestBranchUpdater, type UpdateBranchOptions } from "./pr-update-branch.ts";
|
|
39
40
|
|
|
40
|
-
const POLL_INTERVAL_MS = 30_000;
|
|
41
41
|
const ROUTING_SPINNER_INTERVAL_MS = 80;
|
|
42
42
|
const ROUTING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
43
43
|
const ROUTING_WIDGET_TEXT = "Checking pull request…";
|
|
@@ -282,10 +282,9 @@ export default function pullRequestExtension(
|
|
|
282
282
|
return discovery;
|
|
283
283
|
};
|
|
284
284
|
let sessionGeneration = 0;
|
|
285
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
286
285
|
let active: AbortController | undefined;
|
|
287
286
|
let queued = false;
|
|
288
|
-
let
|
|
287
|
+
let reportedRefreshFailure: "generic" | "quota" | undefined;
|
|
289
288
|
let displayEstablished = false;
|
|
290
289
|
let lastDiscovery: "configured" | "inferred" | "absent" | "blocked" | "inactive" | undefined;
|
|
291
290
|
let lastBlockedIssueKey: string | undefined;
|
|
@@ -561,8 +560,6 @@ export default function pullRequestExtension(
|
|
|
561
560
|
discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
|
|
562
561
|
): void => {
|
|
563
562
|
if (discovery.kind === "inactive") {
|
|
564
|
-
if (timer !== undefined) clearInterval(timer);
|
|
565
|
-
timer = undefined;
|
|
566
563
|
displayEstablished = true;
|
|
567
564
|
lastDiscovery = "inactive";
|
|
568
565
|
displayedWidget = undefined;
|
|
@@ -600,7 +597,7 @@ export default function pullRequestExtension(
|
|
|
600
597
|
context = undefined;
|
|
601
598
|
observation = undefined;
|
|
602
599
|
queued = false;
|
|
603
|
-
|
|
600
|
+
reportedRefreshFailure = undefined;
|
|
604
601
|
displayEstablished = false;
|
|
605
602
|
lastDiscovery = undefined;
|
|
606
603
|
lastBlockedIssueKey = undefined;
|
|
@@ -612,18 +609,22 @@ export default function pullRequestExtension(
|
|
|
612
609
|
activeInvocations.clear();
|
|
613
610
|
stopRoutingSpinner();
|
|
614
611
|
widgetKind = "presentation";
|
|
615
|
-
if (timer !== undefined) clearInterval(timer);
|
|
616
|
-
timer = undefined;
|
|
617
612
|
active?.abort();
|
|
618
613
|
active = undefined;
|
|
619
614
|
};
|
|
620
615
|
|
|
621
|
-
const reportRefreshFailure = (): void => {
|
|
616
|
+
const reportRefreshFailure = (error: unknown): void => {
|
|
622
617
|
const ctx = context;
|
|
623
|
-
|
|
624
|
-
|
|
618
|
+
const category = error instanceof GitHubRateLimitError ? "quota" : "generic";
|
|
619
|
+
if (!ctx || reportedRefreshFailure === category || reportedRefreshFailure === "quota") return;
|
|
620
|
+
reportedRefreshFailure = category;
|
|
625
621
|
try {
|
|
626
|
-
ctx.ui.notify(
|
|
622
|
+
ctx.ui.notify(
|
|
623
|
+
error instanceof GitHubRateLimitError
|
|
624
|
+
? error.message
|
|
625
|
+
: "PR status refresh failed: status unavailable",
|
|
626
|
+
"error",
|
|
627
|
+
);
|
|
627
628
|
} catch {
|
|
628
629
|
console.error("PR status refresh failed and could not be reported");
|
|
629
630
|
}
|
|
@@ -655,7 +656,7 @@ export default function pullRequestExtension(
|
|
|
655
656
|
try {
|
|
656
657
|
discovery = await load(pi, loadContext);
|
|
657
658
|
if (controller.signal.aborted || sessionGeneration !== generation) return;
|
|
658
|
-
} catch {
|
|
659
|
+
} catch (error) {
|
|
659
660
|
// Keep an established footer. A refresh failure must not leave a stale action hint.
|
|
660
661
|
if (!controller.signal.aborted && sessionGeneration === generation) {
|
|
661
662
|
displayedWidget = undefined;
|
|
@@ -665,13 +666,13 @@ export default function pullRequestExtension(
|
|
|
665
666
|
ctx.ui.setStatus(UI_KEY, formatPrFooter(unavailable, ctx.ui.theme));
|
|
666
667
|
displayEstablished = true;
|
|
667
668
|
}
|
|
668
|
-
reportRefreshFailure();
|
|
669
|
+
reportRefreshFailure(error);
|
|
669
670
|
}
|
|
670
671
|
return;
|
|
671
672
|
}
|
|
672
673
|
if (controller.signal.aborted || sessionGeneration !== generation) return;
|
|
673
674
|
render(ctx, discovery);
|
|
674
|
-
|
|
675
|
+
reportedRefreshFailure = undefined;
|
|
675
676
|
|
|
676
677
|
const pullRequest = discovery.kind === "current" ? discovery.pullRequest : undefined;
|
|
677
678
|
if (pendingWorkspaceRename && pullRequest?.target.provenance === "configured") {
|
|
@@ -716,14 +717,10 @@ export default function pullRequestExtension(
|
|
|
716
717
|
|
|
717
718
|
pi.on("session_start", async (_event, ctx) => {
|
|
718
719
|
stop();
|
|
719
|
-
const generation = sessionGeneration;
|
|
720
720
|
observation = latestObservation(ctx);
|
|
721
721
|
if (!ctx.hasUI) return;
|
|
722
722
|
context = ctx;
|
|
723
723
|
await refresh();
|
|
724
|
-
if (sessionGeneration === generation && lastDiscovery !== "inactive") {
|
|
725
|
-
timer = setInterval(refreshInBackground, POLL_INTERVAL_MS);
|
|
726
|
-
}
|
|
727
724
|
});
|
|
728
725
|
|
|
729
726
|
pi.on("session_shutdown", stop);
|
|
@@ -805,8 +802,14 @@ export default function pullRequestExtension(
|
|
|
805
802
|
if (sessionGeneration === generation) {
|
|
806
803
|
cancelRefresh();
|
|
807
804
|
activeInvocations.delete(invocation);
|
|
808
|
-
|
|
809
|
-
|
|
805
|
+
if (error instanceof GitHubRateLimitError) {
|
|
806
|
+
displayedWidget = undefined;
|
|
807
|
+
reconcileWidget(ctx);
|
|
808
|
+
reportRefreshFailure(error);
|
|
809
|
+
} else {
|
|
810
|
+
reconcileWidget(ctx);
|
|
811
|
+
refreshInBackground();
|
|
812
|
+
}
|
|
810
813
|
}
|
|
811
814
|
throw error;
|
|
812
815
|
}
|
package/package.json
CHANGED