Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/browserMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,20 @@ const ALL_PRS = USE_ADE_DB_SNAPSHOT
: []
: [...NORMAL_PRS, ...INTEGRATION_PRS];

function getAdeDbPrSnapshotByGithubCoordinates(args: any): any | null {
const repoOwner = String(args?.repoOwner ?? "").trim().toLowerCase();
const repoName = String(args?.repoName ?? "").trim().toLowerCase();
const githubPrNumber = Number(args?.githubPrNumber);
if (!repoOwner || !repoName || !Number.isInteger(githubPrNumber) || githubPrNumber <= 0) return null;

const pr = ALL_PRS.find((candidate: any) =>
String(candidate.repoOwner ?? "").trim().toLowerCase() === repoOwner
&& String(candidate.repoName ?? "").trim().toLowerCase() === repoName
&& Number(candidate.githubPrNumber) === githubPrNumber,
);
return pr ? ADE_DB_PR_SNAPSHOT_BY_ID.get(String(pr.id)) ?? null : null;
}

// ── Merge Contexts ────────────────────────────────────────────
const BUILTIN_MOCK_MERGE_CONTEXTS: Record<string, any> = {
// Normal PRs — no group
Expand Down Expand Up @@ -6065,6 +6079,16 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) {
MOCK_REVIEWS_BY_PR[prId] ??
[],
getReviewThreads: resolvedArg([]),
getDetailByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.detail ?? null,
getFilesByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.files ?? [],
getCommitsByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.commits ?? [],
getActionRunsByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.actionRuns ?? [],
getActivityByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.activity ?? [],
getStatusByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.status ?? null,
getChecksByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.checks ?? [],
getReviewsByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.reviews ?? [],
getCommentsByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.comments ?? [],
getReviewThreadsByGithub: async (args: any) => getAdeDbPrSnapshotByGithubCoordinates(args)?.reviewThreads ?? [],
updateDescription: resolvedArg(undefined),
delete: resolvedArg({ deleted: true }),
draftDescription: resolvedArg({
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/renderer/components/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@ const workRoute = createPreloadableRoute<{ active?: boolean }>(() =>
);
const TerminalsPage = workRoute.Component;
const preloadTerminalsPage = workRoute.preload;
const PRsPage = React.lazy(() =>
const prsRoute = createPreloadableRoute<{ active?: boolean }>(() =>
import("../prs/PRsPage").then((m) => ({ default: m.PRsPage }))
);
const PRsPage = prsRoute.Component;
const preloadPrsPage = prsRoute.preload;
const ReviewPage = React.lazy(() =>
import("../review/ReviewPage").then((m) => ({ default: m.ReviewPage }))
);
Expand Down Expand Up @@ -753,6 +755,7 @@ function ProjectTabHost() {
void preloadTerminalsPage().catch(() => undefined);
void preloadLanesPage().catch(() => undefined);
void preloadFilesTab().catch(() => undefined);
void preloadPrsPage().catch(() => undefined);
void preloadCtoPage().catch(() => undefined);
};
const idleWindow = window as Window & {
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/components/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const search = buildPrsRouteSearch({
activeTab: "normal",
selectedPrId: toast.event.prId,
selectedPrNumber: toast.event.prNumber,
repoOwner: toast.event.repoOwner,
repoName: toast.event.repoName,
selectedRebaseItemId: null,
detailTab,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ export const ChatGitToolbar = React.memo(function ChatGitToolbar({
localPath: `/prs${buildPrsRouteSearch({
activeTab: "normal",
selectedPrId: pr.id,
selectedPrNumber: pr.githubPrNumber,
repoOwner: pr.repoOwner,
repoName: pr.repoName,
selectedLaneId: laneId,
selectedRebaseItemId: null,
})}`,
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/components/lanes/LaneWorkPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ function LanePullRequestsSection({ lane, prs }: { lane: LaneSummary | null; prs:
onClick={() => navigate(`/prs${buildPrsRouteSearch({
activeTab: "normal",
selectedPrId: pr.id,
selectedPrNumber: pr.githubPrNumber,
repoOwner: pr.repoOwner,
repoName: pr.repoName,
selectedLaneId: pr.laneId,
selectedRebaseItemId: null,
})}`)}
Expand Down
35 changes: 34 additions & 1 deletion apps/desktop/src/renderer/components/lanes/LanesPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getDeferredLanePaneDelayMs,
githubPrMatchesCurrentBranch,
laneHasAncestor,
lanePrTagRoutePath,
lanePrMatchesCurrentBranch,
lanePrRole,
planLaneDeleteBatches,
Expand All @@ -20,7 +21,11 @@ import {
shouldApplyLaneIdsDeepLink,
sortLaneListRows,
} from "./lanePageModel";
import { buildLaneSplitColumnsKey, shouldMountGitActionsPane } from "./LanesPage";
import {
buildLaneSplitColumnsKey,
shouldMountGitActionsPane,
shouldRetryLaneGithubSnapshotForceRefresh,
} from "./LanesPage";
import type {
GitHubPrListItem,
LaneSummary,
Expand Down Expand Up @@ -94,6 +99,34 @@ function makeGitHubPr(overrides: Partial<GitHubPrListItem> = {}): GitHubPrListIt
};
}

describe("lane GitHub snapshot force refresh", () => {
it("retries after a transient forced refresh failure for the same project", () => {
expect(shouldRetryLaneGithubSnapshotForceRefresh({
currentProjectRoot: "/project",
markedProjectRoot: "/project",
refreshSucceeded: false,
startedProjectRoot: "/project",
})).toBe(true);
expect(shouldRetryLaneGithubSnapshotForceRefresh({
currentProjectRoot: "/project",
markedProjectRoot: "/project",
refreshSucceeded: true,
startedProjectRoot: "/project",
})).toBe(false);
});
});

describe("lane PR badge routes", () => {
it("opens an unmapped lane PR badge in the GitHub tab by coordinates", () => {
expect(lanePrTagRoutePath({
linkedPrId: null,
githubPrNumber: 224,
repoOwner: "arul28",
repoName: "ADE",
})).toBe("/prs?tab=github&pr=224&repoOwner=arul28&repoName=ADE");
});
});

describe("resolveCreateLaneRequest", () => {
it("creates an independent lane from the selected primary branch", () => {
expect(
Expand Down
87 changes: 69 additions & 18 deletions apps/desktop/src/renderer/components/lanes/LanesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
resolveVisibleLaneIds,
runLaneDeleteBatchWithConcurrency,
selectLanePrs,
lanePrTagRoutePath,
selectVisibleLanePrRefreshIds,
selectLaneTabPrTags,
shouldApplyLaneIdsDeepLink,
Expand Down Expand Up @@ -83,7 +84,7 @@ import {
DEFAULT_REBASE_SUGGESTIONS,
type RebaseSuggestionDisplay,
} from "../../../shared/types/config";
import { getGitHubSnapshotCoalesced, listPrsCoalesced, refreshPrsCoalesced, warmPrSurfaceCoalesced } from "../../lib/prReadCache";
import { getGitHubSnapshotCoalesced, listPrsCoalesced, refreshPrsCoalesced } from "../../lib/prReadCache";
import { logRendererDebugEvent } from "../../lib/debugLog";
import { shouldRefreshSessionListForChatEvent } from "../../lib/chatSessionEvents";
import { useLaneListInvalidation } from "../../hooks/useLaneListInvalidation";
Expand Down Expand Up @@ -131,6 +132,22 @@ export function shouldMountGitActionsPane({
return surface !== "inline" || !laneId || expandedGitActionsLaneId !== laneId;
}

export function shouldRetryLaneGithubSnapshotForceRefresh({
currentProjectRoot,
markedProjectRoot,
refreshSucceeded,
startedProjectRoot,
}: {
currentProjectRoot: string | null;
markedProjectRoot: string | null;
refreshSucceeded: boolean;
startedProjectRoot: string;
}): boolean {
return !refreshSucceeded
&& currentProjectRoot === startedProjectRoot
&& markedProjectRoot === startedProjectRoot;
}

type RebasePushReviewState = {
runId: string;
lanes: Array<{ laneId: string; laneName: string; selected: boolean }>;
Expand Down Expand Up @@ -482,6 +499,7 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) {
const laneGithubPrTagsRequestRef = useRef(0);
const laneVisiblePrRefreshRequestedAtRef = useRef<Map<string, number>>(new Map());
const laneVisiblePrRefreshProjectRootRef = useRef<string | null>(null);
const laneGithubSnapshotForceRefreshProjectRootRef = useRef<string | null>(null);
const [laneVisiblePrRefreshVisibilityToken, setLaneVisiblePrRefreshVisibilityToken] = useState(0);
const hasActiveLaneRuntimeRef = useRef(false);
const [autoRebaseEnabled, setAutoRebaseEnabled] = useState(false);
Expand Down Expand Up @@ -911,21 +929,23 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) {
}
}, [getActiveProjectRoot]);

const refreshLaneGithubPrTags = useCallback(async (options?: { force?: boolean }) => {
const refreshLaneGithubPrTags = useCallback(async (options?: { force?: boolean }): Promise<boolean> => {
const requestId = ++laneGithubPrTagsRequestRef.current;
const startedRoot = getActiveProjectRoot();
try {
const snapshot = await getGitHubSnapshotCoalesced(
{ force: options?.force === true },
{ projectRoot: startedRoot },
);
if (requestId !== laneGithubPrTagsRequestRef.current) return;
if (getActiveProjectRoot() !== startedRoot) return;
if (requestId !== laneGithubPrTagsRequestRef.current) return false;
if (getActiveProjectRoot() !== startedRoot) return false;
setLaneGithubPrTags(snapshot.repoPullRequests);
return true;
} catch {
if (requestId !== laneGithubPrTagsRequestRef.current) return;
if (getActiveProjectRoot() !== startedRoot) return;
if (requestId !== laneGithubPrTagsRequestRef.current) return false;
if (getActiveProjectRoot() !== startedRoot) return false;
// Keep the last usable GitHub snapshot visible on transient refresh failures.
return false;
}
}, [getActiveProjectRoot]);

Expand Down Expand Up @@ -1075,23 +1095,57 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) {
useEffect(() => {
lanePrTagsRequestRef.current += 1;
laneGithubPrTagsRequestRef.current += 1;
laneGithubSnapshotForceRefreshProjectRootRef.current = null;
setLanePrTags([]);
setLaneGithubPrTags([]);
if (!active || !activeProjectRoot) {
return;
}
void refreshLanePrTags({ refreshMapped: true });
void refreshLaneGithubPrTags({ force: true });
void warmPrSurfaceCoalesced({
projectRoot: activeProjectRoot,
includeGithubSnapshot: false,
});
// Keep the lane surface local-first. Visible stale rows are refreshed by
// the debounced viewport pass below; a forced snapshot and per-lane PR
// refresh here made opening the next PR surface compete with GitHub work.
void refreshLanePrTags();
void refreshLaneGithubPrTags();
return () => {
lanePrTagsRequestRef.current += 1;
laneGithubPrTagsRequestRef.current += 1;
};
}, [active, refreshLanePrTags, refreshLaneGithubPrTags, activeProjectRoot, lanePrBranchSignature]);

useEffect(() => {
if (!active || !activeProjectRoot || document.visibilityState !== "visible") return;
if (laneGithubSnapshotForceRefreshProjectRootRef.current === activeProjectRoot) return;
const hasVisibleGithubOnlyPr = visibleLaneIds.some((laneId) =>
lanePrTagsByLaneId.get(laneId)?.some((tag) => tag.source === "github" && !tag.linkedPrId) ?? false,
);
if (!hasVisibleGithubOnlyPr) return;

const startedRoot = activeProjectRoot;
const timer = window.setTimeout(() => {
if (getActiveProjectRoot() !== startedRoot) return;
laneGithubSnapshotForceRefreshProjectRootRef.current = startedRoot;
void refreshLaneGithubPrTags({ force: true }).then((refreshSucceeded) => {
if (shouldRetryLaneGithubSnapshotForceRefresh({
currentProjectRoot: getActiveProjectRoot(),
markedProjectRoot: laneGithubSnapshotForceRefreshProjectRootRef.current,
refreshSucceeded,
startedProjectRoot: startedRoot,
})) {
laneGithubSnapshotForceRefreshProjectRootRef.current = null;
}
});
}, 750);
return () => window.clearTimeout(timer);
}, [
active,
activeProjectRoot,
getActiveProjectRoot,
lanePrTagsByLaneId,
laneVisiblePrRefreshVisibilityToken,
refreshLaneGithubPrTags,
visibleLaneIds,
]);

useEffect(() => {
if (!active) return;
return window.ade.prs.onEvent((event) => {
Expand Down Expand Up @@ -3285,12 +3339,9 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) {
})}`)}
onActivate={(_event, selectedPr) => {
const target = selectedPr ?? lanePr;
if (target.linkedPrId) {
navigate(`/prs${buildPrsRouteSearch({
activeTab: "normal",
selectedPrId: target.linkedPrId,
selectedRebaseItemId: null,
})}`);
const prRoute = lanePrTagRoutePath(target);
if (prRoute) {
navigate(prRoute);
return;
}
if (target.githubUrl && isTrustedGitHubUrl(target.githubUrl)) {
Expand Down
33 changes: 31 additions & 2 deletions apps/desktop/src/renderer/components/lanes/lanePageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
import type { CreateLaneMode } from "./CreateLaneDialog";
import { mergeUnique } from "./laneUtils";
import { isTerminalPrState } from "../../lib/prState";
import { buildPrsRouteSearch, prRouteCoordinatesMatch } from "../prs/prsRouteState";

type CreateLaneRequest =
| { kind: "child"; args: { name: string; parentLaneId: string } }
Expand All @@ -30,6 +31,9 @@ export type LaneTabPrTag = {
linkedPrId: string | null;
githubPrNumber: number;
githubUrl: string;
/** Repository coordinates keep badge deep links usable before local hydration. */
repoOwner: string;
repoName: string;
title: string;
state: PrSummary["state"];
/** A live PR follows the lane's current branch; previous PRs remain history on the lane. */
Expand All @@ -53,6 +57,25 @@ export type LaneTabPrTag = {
stack?: GitHubPrStackMembership | null;
};

export function lanePrTagRoutePath(
target: Pick<LaneTabPrTag, "linkedPrId" | "githubPrNumber" | "repoOwner" | "repoName">,
): string | null {
if (
!Number.isInteger(target.githubPrNumber)
|| target.githubPrNumber <= 0
|| !target.repoOwner.trim()
|| !target.repoName.trim()
) return null;
return `/prs${buildPrsRouteSearch({
activeTab: target.linkedPrId ? "normal" : "github",
selectedPrId: target.linkedPrId,
selectedPrNumber: target.githubPrNumber,
repoOwner: target.repoOwner,
repoName: target.repoName,
selectedRebaseItemId: null,
})}`;
}

export const VISIBLE_LANE_PR_REFRESH_LIMIT = 4;
export const VISIBLE_LANE_PR_REFRESH_STALE_MS = 15_000;
export const DEFERRED_LANE_PANE_STEP_MS = 220;
Expand Down Expand Up @@ -280,8 +303,10 @@ export function githubPrMatchesCurrentBranch(
if (!laneBranch || !prHeadBranch || laneBranch !== prHeadBranch) return false;
const headRepoOwner = pr.headRepoOwner?.trim();
const headRepoName = pr.headRepoName?.trim();
if (headRepoOwner && pr.repoOwner && headRepoOwner.toLowerCase() !== pr.repoOwner.toLowerCase()) return false;
if (headRepoName && pr.repoName && headRepoName.toLowerCase() !== pr.repoName.toLowerCase()) return false;
if (!prRouteCoordinatesMatch(
{ prNumber: null, repoOwner: headRepoOwner ?? null, repoName: headRepoName ?? null },
{ prNumber: null, repoOwner: pr.repoOwner, repoName: pr.repoName },
)) return false;
if (lane.laneType === "primary") {
const baseBranch = normalizeLanePrBranch(lane.baseRef);
if (laneBranch && baseBranch && laneBranch === baseBranch) return false;
Expand Down Expand Up @@ -312,6 +337,8 @@ function toLaneTabPrTagFromPrSummary(pr: PrSummary, laneRole?: "active" | "previ
linkedPrId: pr.id,
githubPrNumber: pr.githubPrNumber,
githubUrl: pr.githubUrl,
repoOwner: pr.repoOwner,
repoName: pr.repoName,
title: pr.title,
state: pr.state,
laneRole,
Expand Down Expand Up @@ -341,6 +368,8 @@ function toLaneTabPrTagFromGithubItem(
linkedPrId,
githubPrNumber: pr.githubPrNumber,
githubUrl: pr.githubUrl,
repoOwner: pr.repoOwner,
repoName: pr.repoName,
title: pr.title,
state: pr.isDraft ? "draft" : pr.state,
laneRole,
Expand Down
Loading
Loading