@henryqw/pi-pr 3.1.8 → 3.1.10
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 -3
- package/extensions/pr-command.ts +7 -2
- package/extensions/pr-github.ts +40 -25
- package/extensions/pr.ts +86 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,10 +45,12 @@ Add optional instructions to guide a creation, branch-update, CI-fix, or feedbac
|
|
|
45
45
|
| --- | --- | --- |
|
|
46
46
|
| `/pr [instructions]` | command | Run the current pull request's next safe route. |
|
|
47
47
|
| Footer | ui | Show a linked `PR #number` and one plain-language status. |
|
|
48
|
-
| Widget | ui | Show one
|
|
48
|
+
| Widget | ui | Show one action hint or transient routing status. |
|
|
49
49
|
|
|
50
50
|
The footer already shows the pull request and status. Actionable widgets omit duplicate identity and status. Each uses one semantic status icon, a space, and a plain `Run /pr to …` route. `✗` marks errors, `!` warnings, `✓` success, and `●` accent or neutral routes. In TUI, only the icon uses a theme color. RPC and non-TUI output use the same plain text without ANSI.
|
|
51
51
|
|
|
52
|
+
The widget switches to `⠋ Checking pull request…` as soon as `/pr` starts discovery. The braille spinner animates in TUI mode. RPC receives one plain static line. The footer stays unchanged. The routing widget clears after route selection and before any prompt, notification, mutation, or workflow dispatch.
|
|
53
|
+
|
|
52
54
|
## Flow
|
|
53
55
|
|
|
54
56
|
Each footer entry is one linked `PR #number` plus one plain-language status: `N unresolved`, `draft`, `open`, `approved`, `CI running`, `CI failed`, `changes requested`, `base update required`, `merge conflict`, `merge-ready`, `merged`, or `closed`. Colors support the text; they do not carry meaning alone.
|
|
@@ -90,7 +92,7 @@ It renames only the workspace. Outside Herdr, it does nothing.
|
|
|
90
92
|
|
|
91
93
|
If Herdr lookup, JSON validation, or rename fails, the PR and normal UI refresh remain available. Each Herdr command has a 10-second timeout. The extension warns with `Herdr workspace rename failed: <error>`.
|
|
92
94
|
|
|
93
|
-
Current-branch discovery
|
|
95
|
+
Current-branch discovery reads pull requests associated with the exact push repository ref. It does not run a global branch search. It finds a fork-head PR whose base is an upstream repository. A unique historical match uses the exact remote push-ref OID, not local HEAD.
|
|
94
96
|
|
|
95
97
|
A no-action state includes a draft, merged or closed pull request, running CI, pending review, or blocked merge policy. It also includes a mutating workflow whose tree is dirty or whose local HEAD differs from the PR head.
|
|
96
98
|
|
|
@@ -115,7 +117,7 @@ The footer and widget load at session start. A directory outside a Git worktree
|
|
|
115
117
|
|
|
116
118
|
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. Active Git worktrees poll every 30 seconds. Polling updates presentation only and may be stale.
|
|
117
119
|
|
|
118
|
-
The create widget stays hidden until the local branch has a commit beyond its creation point.
|
|
120
|
+
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.
|
|
119
121
|
|
|
120
122
|
Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
|
|
121
123
|
|
package/extensions/pr-command.ts
CHANGED
|
@@ -27,7 +27,11 @@ const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
30
|
-
export type PrCommandHandler = (
|
|
30
|
+
export type PrCommandHandler = (
|
|
31
|
+
args: string,
|
|
32
|
+
ctx: ExtensionCommandContext,
|
|
33
|
+
onRouteResolved?: (nextStep: NextStep) => void,
|
|
34
|
+
) => Promise<NextStep>;
|
|
31
35
|
|
|
32
36
|
type PrCommandDependencies = {
|
|
33
37
|
loadCurrentPullRequest?: typeof loadCurrentPullRequest;
|
|
@@ -175,10 +179,11 @@ export function createPrCommandHandler(
|
|
|
175
179
|
): PrCommandHandler {
|
|
176
180
|
const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
|
|
177
181
|
const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
|
|
178
|
-
return async (args, ctx) => {
|
|
182
|
+
return async (args, ctx, onRouteResolved) => {
|
|
179
183
|
const instructions = args.trim();
|
|
180
184
|
const discovery = await load(pi, ctx);
|
|
181
185
|
const nextStep = deriveNextStep(discovery);
|
|
186
|
+
onRouteResolved?.(nextStep);
|
|
182
187
|
if (instructions && !(nextStep in WORKFLOWS)) {
|
|
183
188
|
throw new Error("The current /pr route does not accept instructions");
|
|
184
189
|
}
|
package/extensions/pr-github.ts
CHANGED
|
@@ -18,11 +18,11 @@ import type {
|
|
|
18
18
|
} from "./pr-routing.ts";
|
|
19
19
|
|
|
20
20
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
21
|
+
const PR_DISCOVERY_PAGE_SIZE = 100;
|
|
22
|
+
const PR_DISCOVERY_CAP = 1_000;
|
|
23
|
+
const PR_DISCOVERY_MAX_PAGES = PR_DISCOVERY_CAP / PR_DISCOVERY_PAGE_SIZE;
|
|
24
24
|
const PR_FIELDS = "id,number,url,state,isDraft,baseRefName,baseRefOid,headRefName,headRefOid,headRepository,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup";
|
|
25
|
-
const
|
|
25
|
+
const PR_DISCOVERY_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!,$endCursor:String){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name associatedPullRequests(first:100,after:$endCursor){totalCount edges{cursor node{__typename number url state baseRepository{nameWithOwner}headRepository{nameWithOwner}headRefName headRefOid}}pageInfo{hasNextPage startCursor endCursor}}}}}";
|
|
26
26
|
const REVIEW_THREADS_QUERY = "query($id:ID!,$endCursor:String){node(id:$id){...on PullRequest{reviewThreads(first:100,after:$endCursor){nodes{isResolved}pageInfo{hasNextPage endCursor}}}}}";
|
|
27
27
|
const BASE_REF_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name target{oid}}}}";
|
|
28
28
|
const BASE_BRANCH_POLICY_QUERY = "query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){nameWithOwner ref(qualifiedName:$qualifiedName){name branchProtectionRule{requiresStrictStatusChecks}}}}";
|
|
@@ -167,7 +167,7 @@ type SearchSelection =
|
|
|
167
167
|
| { kind: "target-invalid" };
|
|
168
168
|
|
|
169
169
|
type SearchPage = {
|
|
170
|
-
|
|
170
|
+
totalCount: number;
|
|
171
171
|
candidates: SearchPullRequest[];
|
|
172
172
|
cursors: string[];
|
|
173
173
|
hasNextPage: boolean;
|
|
@@ -634,18 +634,29 @@ function searchPullRequest(value: unknown, host: string): SearchPullRequest {
|
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
-
function parseSearchPage(output: string,
|
|
637
|
+
function parseSearchPage(output: string, pushTarget: PushTarget): SearchPage | null {
|
|
638
638
|
const page = parseJson(output, "Find pull requests");
|
|
639
639
|
if (!isRecord(page)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
640
640
|
if (page.errors !== undefined) {
|
|
641
641
|
if (!Array.isArray(page.errors)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
642
642
|
if (page.errors.length) fail("Find pull requests", "GitHub GraphQL returned errors");
|
|
643
643
|
}
|
|
644
|
-
const
|
|
644
|
+
const repository = isRecord(page.data) ? page.data.repository : undefined;
|
|
645
|
+
if (!isRecord(repository)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
645
646
|
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
647
|
+
normalizeRepository(repositoryName(repository.nameWithOwner, "Find pull requests", "repository.nameWithOwner")) !==
|
|
648
|
+
pushTarget.repository.normalizedName
|
|
649
|
+
) fail("Find pull requests", "repository does not match push target");
|
|
650
|
+
if (repository.ref === null) return null;
|
|
651
|
+
if (!isRecord(repository.ref)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
652
|
+
if (text(repository.ref.name, "Find pull requests", "ref.name") !== pushTarget.ref) {
|
|
653
|
+
fail("Find pull requests", "ref does not match push target");
|
|
654
|
+
}
|
|
655
|
+
const search = repository.ref.associatedPullRequests;
|
|
656
|
+
if (
|
|
657
|
+
!isRecord(search) || typeof search.totalCount !== "number" ||
|
|
658
|
+
!Number.isSafeInteger(search.totalCount) || search.totalCount < 0 ||
|
|
659
|
+
!Array.isArray(search.edges) || search.edges.length > PR_DISCOVERY_PAGE_SIZE ||
|
|
649
660
|
!isRecord(search.pageInfo)
|
|
650
661
|
) fail("Find pull requests", "invalid GitHub CLI output");
|
|
651
662
|
const candidates: SearchPullRequest[] = [];
|
|
@@ -653,7 +664,7 @@ function parseSearchPage(output: string, host: string): SearchPage {
|
|
|
653
664
|
for (const edge of search.edges) {
|
|
654
665
|
if (!isRecord(edge)) fail("Find pull requests", "invalid GitHub CLI output");
|
|
655
666
|
cursors.push(text(edge.cursor, "Find pull requests", "cursor"));
|
|
656
|
-
candidates.push(searchPullRequest(edge.node, host));
|
|
667
|
+
candidates.push(searchPullRequest(edge.node, pushTarget.repository.host));
|
|
657
668
|
}
|
|
658
669
|
if (new Set(cursors).size !== cursors.length) fail("Find pull requests", "duplicate candidate cursor");
|
|
659
670
|
const { hasNextPage, startCursor, endCursor } = search.pageInfo;
|
|
@@ -664,7 +675,7 @@ function parseSearchPage(output: string, host: string): SearchPage {
|
|
|
664
675
|
fail("Find pull requests", "invalid search pageInfo");
|
|
665
676
|
}
|
|
666
677
|
return {
|
|
667
|
-
|
|
678
|
+
totalCount: search.totalCount,
|
|
668
679
|
candidates,
|
|
669
680
|
cursors,
|
|
670
681
|
hasNextPage,
|
|
@@ -1443,32 +1454,36 @@ async function searchPullRequests(
|
|
|
1443
1454
|
context: PullRequestLoadContext,
|
|
1444
1455
|
pushTarget: PushTarget,
|
|
1445
1456
|
): Promise<SearchSelection> {
|
|
1446
|
-
const
|
|
1447
|
-
const searchQuery = `is:pr${pushTarget.provenance === "inferred" ? " is:open" : ""} head:${headOwner}:${pushTarget.ref}`;
|
|
1457
|
+
const [owner, name] = pushTarget.repository.nameWithOwner.split("/");
|
|
1448
1458
|
const candidates: SearchPullRequest[] = [];
|
|
1449
1459
|
const cursors = new Set<string>();
|
|
1450
|
-
let
|
|
1460
|
+
let totalCount: number | null = null;
|
|
1451
1461
|
let endCursor: string | null = null;
|
|
1452
|
-
for (let pageIndex = 0; pageIndex <
|
|
1462
|
+
for (let pageIndex = 0; pageIndex < PR_DISCOVERY_MAX_PAGES; pageIndex += 1) {
|
|
1453
1463
|
const args = [
|
|
1454
1464
|
"api",
|
|
1455
1465
|
"graphql",
|
|
1456
1466
|
"--hostname",
|
|
1457
1467
|
pushTarget.repository.host,
|
|
1458
1468
|
"-f",
|
|
1459
|
-
`query=${
|
|
1469
|
+
`query=${PR_DISCOVERY_QUERY}`,
|
|
1470
|
+
"-F",
|
|
1471
|
+
`owner=${owner}`,
|
|
1472
|
+
"-F",
|
|
1473
|
+
`name=${name}`,
|
|
1460
1474
|
"-F",
|
|
1461
|
-
`
|
|
1475
|
+
`qualifiedName=refs/heads/${pushTarget.ref}`,
|
|
1462
1476
|
];
|
|
1463
1477
|
if (endCursor !== null) args.push("-F", `endCursor=${endCursor}`);
|
|
1464
1478
|
const result = await execute(pi, context, "Find pull requests", "gh", args);
|
|
1465
|
-
const page = parseSearchPage(result.stdout, pushTarget
|
|
1466
|
-
if (
|
|
1479
|
+
const page = parseSearchPage(result.stdout, pushTarget);
|
|
1480
|
+
if (page === null) return pushTarget.remoteHeadOid === null ? { kind: "none" } : { kind: "target-invalid" };
|
|
1481
|
+
if (totalCount !== null && page.totalCount !== totalCount) {
|
|
1467
1482
|
fail("Find pull requests", "inconsistent search result pages");
|
|
1468
1483
|
}
|
|
1469
|
-
|
|
1470
|
-
if (
|
|
1471
|
-
const expectedPageSize = Math.min(
|
|
1484
|
+
totalCount = page.totalCount;
|
|
1485
|
+
if (totalCount > PR_DISCOVERY_CAP) fail("Find pull requests", "GitHub pull request result cap reached");
|
|
1486
|
+
const expectedPageSize = Math.min(PR_DISCOVERY_PAGE_SIZE, Math.max(0, totalCount - candidates.length));
|
|
1472
1487
|
if (page.candidates.length !== expectedPageSize) fail("Find pull requests", "incomplete search results");
|
|
1473
1488
|
for (const cursor of page.cursors) {
|
|
1474
1489
|
if (cursors.has(cursor)) fail("Find pull requests", "duplicate candidate cursor");
|
|
@@ -1478,7 +1493,7 @@ async function searchPullRequests(
|
|
|
1478
1493
|
if (new Set(candidates.map(({ url }) => url.href.toLowerCase())).size !== candidates.length) {
|
|
1479
1494
|
fail("Find pull requests", "duplicate candidate url");
|
|
1480
1495
|
}
|
|
1481
|
-
const hasMore = candidates.length <
|
|
1496
|
+
const hasMore = candidates.length < totalCount;
|
|
1482
1497
|
if (page.hasNextPage !== hasMore) fail("Find pull requests", "incomplete search results");
|
|
1483
1498
|
if (!hasMore) {
|
|
1484
1499
|
const selected = selectSearchPullRequest(candidates, pushTarget);
|
|
@@ -1498,7 +1513,7 @@ async function searchPullRequests(
|
|
|
1498
1513
|
if (page.endCursor === null) fail("Find pull requests", "invalid search pageInfo");
|
|
1499
1514
|
endCursor = page.endCursor;
|
|
1500
1515
|
}
|
|
1501
|
-
return fail("Find pull requests", "GitHub
|
|
1516
|
+
return fail("Find pull requests", "GitHub pull request result cap reached");
|
|
1502
1517
|
}
|
|
1503
1518
|
|
|
1504
1519
|
export async function loadCurrentPullRequest(
|
package/extensions/pr.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
6
7
|
import { createHerdrClient } from "@henryqw/pi-herdr";
|
|
7
8
|
import { createPrCommandHandler } from "./pr-command.ts";
|
|
8
9
|
import {
|
|
@@ -24,6 +25,9 @@ import {
|
|
|
24
25
|
} from "./pr-ui.ts";
|
|
25
26
|
|
|
26
27
|
const POLL_INTERVAL_MS = 30_000;
|
|
28
|
+
const ROUTING_SPINNER_INTERVAL_MS = 80;
|
|
29
|
+
const ROUTING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
30
|
+
const ROUTING_WIDGET_TEXT = "Checking pull request…";
|
|
27
31
|
const HERDR_TIMEOUT_MS = 10_000;
|
|
28
32
|
const UI_KEY = "pi-pr";
|
|
29
33
|
const OBSERVATION_ENTRY = "pi-pr-observation";
|
|
@@ -130,9 +134,23 @@ export default function pullRequestExtension(
|
|
|
130
134
|
let mergeCompleted = false;
|
|
131
135
|
let displayedWidget: PrDisplay | undefined;
|
|
132
136
|
let commandGeneration = 0;
|
|
133
|
-
const activeInvocations = new Map<number, "routing" | "create-workflow" | "workflow">();
|
|
137
|
+
const activeInvocations = new Map<number, "routing" | "resolved" | "create-workflow" | "workflow">();
|
|
138
|
+
let widgetKind: "presentation" | "routing" = "presentation";
|
|
139
|
+
let routingSpinnerFrame = 0;
|
|
140
|
+
let routingSpinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
141
|
+
let routingSpinnerTui: TUI | undefined;
|
|
142
|
+
let routingSpinnerComponent: (Component & { dispose(): void }) | undefined;
|
|
143
|
+
|
|
144
|
+
const stopRoutingSpinner = (): void => {
|
|
145
|
+
if (routingSpinnerTimer !== undefined) clearInterval(routingSpinnerTimer);
|
|
146
|
+
routingSpinnerTimer = undefined;
|
|
147
|
+
routingSpinnerTui = undefined;
|
|
148
|
+
routingSpinnerComponent = undefined;
|
|
149
|
+
};
|
|
134
150
|
|
|
135
151
|
const setWidget = (ctx: ExtensionContext, display: PrDisplay | undefined): void => {
|
|
152
|
+
stopRoutingSpinner();
|
|
153
|
+
widgetKind = "presentation";
|
|
136
154
|
if (display?.widget === undefined) {
|
|
137
155
|
ctx.ui.setWidget(UI_KEY, undefined);
|
|
138
156
|
return;
|
|
@@ -147,6 +165,57 @@ export default function pullRequestExtension(
|
|
|
147
165
|
ctx.ui.setWidget(UI_KEY, formatPrWidget(display));
|
|
148
166
|
};
|
|
149
167
|
|
|
168
|
+
const setRoutingWidget = (ctx: ExtensionContext): void => {
|
|
169
|
+
if (widgetKind === "routing") return;
|
|
170
|
+
stopRoutingSpinner();
|
|
171
|
+
widgetKind = "routing";
|
|
172
|
+
routingSpinnerFrame = 0;
|
|
173
|
+
if (ctx.mode !== "tui") {
|
|
174
|
+
ctx.ui.setWidget(UI_KEY, [`${ROUTING_SPINNER_FRAMES[0]} ${ROUTING_WIDGET_TEXT}`]);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ctx.ui.setWidget(UI_KEY, (tui, theme) => {
|
|
179
|
+
let cachedWidth: number | undefined;
|
|
180
|
+
let cachedLines: string[] | undefined;
|
|
181
|
+
const component: Component & { dispose(): void } = {
|
|
182
|
+
invalidate() {
|
|
183
|
+
cachedWidth = undefined;
|
|
184
|
+
cachedLines = undefined;
|
|
185
|
+
},
|
|
186
|
+
render(width) {
|
|
187
|
+
if (width <= 0) return [];
|
|
188
|
+
if (cachedLines !== undefined && cachedWidth === width) return cachedLines;
|
|
189
|
+
const line = `${theme.fg("accent", ROUTING_SPINNER_FRAMES[routingSpinnerFrame]!)} ${ROUTING_WIDGET_TEXT}`;
|
|
190
|
+
cachedWidth = width;
|
|
191
|
+
cachedLines = [truncateToWidth(line, Math.max(1, width))];
|
|
192
|
+
return cachedLines;
|
|
193
|
+
},
|
|
194
|
+
dispose() {
|
|
195
|
+
if (routingSpinnerComponent !== component) return;
|
|
196
|
+
stopRoutingSpinner();
|
|
197
|
+
widgetKind = "presentation";
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
routingSpinnerTui = tui;
|
|
201
|
+
routingSpinnerComponent = component;
|
|
202
|
+
return component;
|
|
203
|
+
});
|
|
204
|
+
routingSpinnerTimer = setInterval(() => {
|
|
205
|
+
routingSpinnerFrame = (routingSpinnerFrame + 1) % ROUTING_SPINNER_FRAMES.length;
|
|
206
|
+
routingSpinnerComponent?.invalidate();
|
|
207
|
+
routingSpinnerTui?.requestRender();
|
|
208
|
+
}, ROUTING_SPINNER_INTERVAL_MS);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const reconcileWidget = (ctx: ExtensionContext): void => {
|
|
212
|
+
if ([...activeInvocations.values()].includes("routing")) {
|
|
213
|
+
setRoutingWidget(ctx);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
setWidget(ctx, activeInvocations.size > 0 ? undefined : displayedWidget);
|
|
217
|
+
};
|
|
218
|
+
|
|
150
219
|
const render = (
|
|
151
220
|
ctx: ExtensionContext,
|
|
152
221
|
discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
|
|
@@ -159,7 +228,7 @@ export default function pullRequestExtension(
|
|
|
159
228
|
lastDiscovery = "inactive";
|
|
160
229
|
displayedWidget = undefined;
|
|
161
230
|
ctx.ui.setStatus(UI_KEY, undefined);
|
|
162
|
-
|
|
231
|
+
reconcileWidget(ctx);
|
|
163
232
|
return;
|
|
164
233
|
}
|
|
165
234
|
const display = projectPrDisplay(discovery, localCommit);
|
|
@@ -168,9 +237,8 @@ export default function pullRequestExtension(
|
|
|
168
237
|
throw new Error("Pull request display is missing a footer");
|
|
169
238
|
}
|
|
170
239
|
displayedWidget = display.widget === undefined ? undefined : display;
|
|
171
|
-
const widget = activeInvocations.size > 0 ? undefined : displayedWidget;
|
|
172
240
|
ctx.ui.setStatus(UI_KEY, footer);
|
|
173
|
-
|
|
241
|
+
reconcileWidget(ctx);
|
|
174
242
|
if (discovery.kind === "blocked") {
|
|
175
243
|
const key = discoveryIssueKey(discovery.issue);
|
|
176
244
|
if (lastBlockedIssueKey !== key) {
|
|
@@ -203,6 +271,8 @@ export default function pullRequestExtension(
|
|
|
203
271
|
displayedWidget = undefined;
|
|
204
272
|
commandGeneration = 0;
|
|
205
273
|
activeInvocations.clear();
|
|
274
|
+
stopRoutingSpinner();
|
|
275
|
+
widgetKind = "presentation";
|
|
206
276
|
if (timer !== undefined) clearInterval(timer);
|
|
207
277
|
timer = undefined;
|
|
208
278
|
active?.abort();
|
|
@@ -255,8 +325,9 @@ export default function pullRequestExtension(
|
|
|
255
325
|
if (!controller.signal.aborted && sessionGeneration === generation) {
|
|
256
326
|
if (!displayEstablished) {
|
|
257
327
|
const unavailable = unavailablePrDisplay();
|
|
328
|
+
displayedWidget = undefined;
|
|
258
329
|
ctx.ui.setStatus(UI_KEY, formatPrFooter(unavailable, ctx.ui.theme));
|
|
259
|
-
|
|
330
|
+
reconcileWidget(ctx);
|
|
260
331
|
displayEstablished = true;
|
|
261
332
|
}
|
|
262
333
|
reportRefreshFailure();
|
|
@@ -352,17 +423,21 @@ export default function pullRequestExtension(
|
|
|
352
423
|
const generation = sessionGeneration;
|
|
353
424
|
const invocation = ++commandGeneration;
|
|
354
425
|
activeInvocations.set(invocation, "routing");
|
|
355
|
-
|
|
426
|
+
reconcileWidget(ctx);
|
|
427
|
+
const routeResolved = (): void => {
|
|
428
|
+
if (sessionGeneration !== generation || activeInvocations.get(invocation) !== "routing") return;
|
|
429
|
+
activeInvocations.set(invocation, "resolved");
|
|
430
|
+
reconcileWidget(ctx);
|
|
431
|
+
};
|
|
356
432
|
let nextStep: Awaited<ReturnType<typeof commandHandler>>;
|
|
357
433
|
try {
|
|
358
|
-
nextStep = await commandHandler(args, ctx);
|
|
434
|
+
nextStep = await commandHandler(args, ctx, routeResolved);
|
|
435
|
+
routeResolved();
|
|
359
436
|
} catch (error) {
|
|
360
437
|
if (sessionGeneration === generation) {
|
|
361
438
|
cancelRefresh();
|
|
362
439
|
activeInvocations.delete(invocation);
|
|
363
|
-
|
|
364
|
-
setWidget(ctx, displayedWidget);
|
|
365
|
-
}
|
|
440
|
+
reconcileWidget(ctx);
|
|
366
441
|
refreshInBackground();
|
|
367
442
|
}
|
|
368
443
|
throw error;
|
|
@@ -372,7 +447,7 @@ export default function pullRequestExtension(
|
|
|
372
447
|
if (WORKFLOW_ROUTES.has(nextStep)) {
|
|
373
448
|
activeInvocations.set(invocation, nextStep === "create" ? "create-workflow" : "workflow");
|
|
374
449
|
if (nextStep === "create") ctx.ui.setStatus(UI_KEY, undefined);
|
|
375
|
-
|
|
450
|
+
reconcileWidget(ctx);
|
|
376
451
|
} else {
|
|
377
452
|
if (nextStep === "merge") mergeCompleted = true;
|
|
378
453
|
activeInvocations.delete(invocation);
|
package/package.json
CHANGED