From e1463944d66d7feef68bcbe4c6396f46424dc7f9 Mon Sep 17 00:00:00 2001
From: Arul Sharma <31745423+arul28@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:05:33 -0400
Subject: [PATCH 01/11] ship: checkpoint before automate/finalize
---
.../main/services/lanes/autoRebaseService.ts | 24 +--
.../services/prs/prRebaseResolver.test.ts | 2 +-
.../src/main/services/prs/prRebaseResolver.ts | 1 -
.../src/main/services/prs/prService.ts | 6 +-
.../lanes/LaneGitActionsPane.test.tsx | 4 +-
.../components/lanes/LaneGitActionsPane.tsx | 8 +-
.../components/lanes/LaneRebaseBanner.tsx | 8 +-
.../renderer/components/lanes/LanesPage.tsx | 4 +-
.../renderer/components/prs/CreatePrModal.tsx | 9 +-
.../src/renderer/components/prs/PRsPage.tsx | 22 +--
.../components/prs/prsRouteState.test.ts | 45 ++++-
.../renderer/components/prs/prsRouteState.ts | 36 ++++
.../components/prs/state/PrsContext.test.tsx | 54 ++++++
.../components/prs/state/PrsContext.tsx | 52 ++++--
.../components/prs/tabs/IntegrationTab.tsx | 7 +-
.../renderer/components/prs/tabs/QueueTab.tsx | 6 +-
.../components/prs/tabs/WorkflowsTab.tsx | 2 +-
.../settings/LaneBehaviorSection.tsx | 6 +-
.../terminals/useWorkSessions.test.ts | 154 +++++++++++++++++-
.../components/terminals/useWorkSessions.ts | 23 ++-
20 files changed, 392 insertions(+), 81 deletions(-)
diff --git a/apps/desktop/src/main/services/lanes/autoRebaseService.ts b/apps/desktop/src/main/services/lanes/autoRebaseService.ts
index 39cccb963..308b1c924 100644
--- a/apps/desktop/src/main/services/lanes/autoRebaseService.ts
+++ b/apps/desktop/src/main/services/lanes/autoRebaseService.ts
@@ -112,17 +112,17 @@ function blockedMessage(
laneId: string | null,
reason: "conflict" | "manual" | "lookup" | "failed" | "unavailable" | null,
): string {
- if (!laneId) return "Pending: auto-rebase stopped at an earlier lane. Open the Rebase tab to continue.";
+ if (!laneId) return "Pending: auto-rebase stopped at an earlier lane. Open the Rebase/Merge tab to continue.";
if (reason === "manual") {
- return `Pending: ancestor lane '${laneId}' has a fixed PR base. Rebase that lane manually from the Rebase tab before descendants can continue.`;
+ return `Pending: ancestor lane '${laneId}' has a fixed PR base. Rebase that lane manually from the Rebase/Merge tab before descendants can continue.`;
}
if (reason === "lookup" || reason === "unavailable") {
- return `Pending: ancestor lane '${laneId}' needs review before descendants can continue. Open the Rebase tab to inspect it.`;
+ return `Pending: ancestor lane '${laneId}' needs review before descendants can continue. Open the Rebase/Merge tab to inspect it.`;
}
if (reason === "failed") {
- return `Pending: ancestor lane '${laneId}' failed automatic rebase. Open the Rebase tab to retry.`;
+ return `Pending: ancestor lane '${laneId}' failed automatic rebase. Open the Rebase/Merge tab to retry.`;
}
- return `Pending: ancestor lane '${laneId}' has unresolved rebase conflicts. Open the Rebase tab to continue.`;
+ return `Pending: ancestor lane '${laneId}' has unresolved rebase conflicts. Open the Rebase/Merge tab to continue.`;
}
function resolveAffectedChainLaneId(
@@ -459,7 +459,7 @@ export function createAutoRebaseService(args: {
parentHeadSha: null,
state: "rebasePending",
conflictCount: 0,
- message: "Pending: parent lane is unavailable. Open the Rebase tab to review the lane."
+ message: "Pending: parent lane is unavailable. Open the Rebase/Merge tab to review the lane."
});
blocked = true;
blockedLaneId = lane.id;
@@ -522,7 +522,7 @@ export function createAutoRebaseService(args: {
parentHeadSha,
state: "rebaseConflict",
conflictCount: Math.max(1, need.conflictingFiles.length),
- message: `Auto-rebase blocked: ${Math.max(1, need.conflictingFiles.length)} conflict(s) expected. Open the Rebase tab to resolve and publish.`
+ message: `Auto-rebase blocked: ${Math.max(1, need.conflictingFiles.length)} conflict(s) expected. Open the Rebase/Merge tab to resolve and publish.`
});
continue;
}
@@ -541,7 +541,7 @@ export function createAutoRebaseService(args: {
parentHeadSha,
state: "rebasePending",
conflictCount: 0,
- message: "PR carries an immutable base — drift detected. Rebase manually from the Rebase tab when ready."
+ message: "PR carries an immutable base — drift detected. Rebase manually from the Rebase/Merge tab when ready."
});
continue;
}
@@ -572,8 +572,8 @@ export function createAutoRebaseService(args: {
state: conflictHint ? "rebaseConflict" : "rebaseFailed",
conflictCount: conflictHint ? 1 : 0,
message: conflictHint
- ? "Auto-rebase stopped due to conflicts. Open the Rebase tab to resolve, then publish."
- : `Auto-rebase failed: ${rebaseRun.run.error}. Open the Rebase tab to retry.`
+ ? "Auto-rebase stopped due to conflicts. Open the Rebase/Merge tab to resolve, then publish."
+ : `Auto-rebase failed: ${rebaseRun.run.error}. Open the Rebase/Merge tab to retry.`
});
continue;
}
@@ -625,8 +625,8 @@ export function createAutoRebaseService(args: {
state: "rebaseFailed",
conflictCount: 0,
message: rollbackError
- ? `Auto-push failed: ${pushError}. Automatic rollback also failed: ${rollbackError}. Open the Rebase tab to retry.`
- : `Auto-push failed: ${pushError}. The lane was restored to its pre-rebase state. Open the Rebase tab to retry.`
+ ? `Auto-push failed: ${pushError}. Automatic rollback also failed: ${rollbackError}. Open the Rebase/Merge tab to retry.`
+ : `Auto-push failed: ${pushError}. The lane was restored to its pre-rebase state. Open the Rebase/Merge tab to retry.`
});
}
}
diff --git a/apps/desktop/src/main/services/prs/prRebaseResolver.test.ts b/apps/desktop/src/main/services/prs/prRebaseResolver.test.ts
index 0349af8c7..bb6e38853 100644
--- a/apps/desktop/src/main/services/prs/prRebaseResolver.test.ts
+++ b/apps/desktop/src/main/services/prs/prRebaseResolver.test.ts
@@ -141,10 +141,10 @@ describe("launchRebaseResolutionChat", () => {
expect(sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "session-rebase-1",
- displayText: "Rebase feature/rebase-target onto main",
reasoningEffort: "high",
}),
);
+ expect(sendMessage.mock.calls[0]?.[0]).not.toHaveProperty("displayText");
expect(result).toEqual({
sessionId: "session-rebase-1",
laneId: lane.id,
diff --git a/apps/desktop/src/main/services/prs/prRebaseResolver.ts b/apps/desktop/src/main/services/prs/prRebaseResolver.ts
index c9cbd3cdf..f38a6bb33 100644
--- a/apps/desktop/src/main/services/prs/prRebaseResolver.ts
+++ b/apps/desktop/src/main/services/prs/prRebaseResolver.ts
@@ -158,7 +158,6 @@ export async function launchRebaseResolutionChat(
await deps.agentChatService.sendMessage({
sessionId: session.id,
text: prompt,
- displayText: title,
...(reasoningEffort ? { reasoningEffort } : {}),
});
diff --git a/apps/desktop/src/main/services/prs/prService.ts b/apps/desktop/src/main/services/prs/prService.ts
index 2d29ecb20..22841aa4e 100644
--- a/apps/desktop/src/main/services/prs/prService.ts
+++ b/apps/desktop/src/main/services/prs/prService.ts
@@ -871,7 +871,7 @@ export function createPrService({
parentHeadSha: null,
state: "rebaseFailed",
conflictCount: 0,
- message: `Auto-rebase failed after '${args.landedLaneName}' merged because ADE could not find a new parent lane. Open the Rebase tab to recover this lane.`,
+ message: `Auto-rebase failed after '${args.landedLaneName}' merged because ADE could not find a new parent lane. Open the Rebase/Merge tab to recover this lane.`,
}, child.id);
}
return {
@@ -979,8 +979,8 @@ export function createPrService({
state: "rebaseFailed",
conflictCount: 0,
message: rollbackError
- ? `Auto-rebase failed after '${args.landedLaneName}' merged: ${childError}. Automatic rollback also failed: ${rollbackError}. Open the Rebase tab to recover this lane.`
- : `Auto-rebase failed after '${args.landedLaneName}' merged: ${childError}. The lane was restored to its pre-rebase state. Open the Rebase tab to recover this lane.`,
+ ? `Auto-rebase failed after '${args.landedLaneName}' merged: ${childError}. Automatic rollback also failed: ${rollbackError}. Open the Rebase/Merge tab to recover this lane.`
+ : `Auto-rebase failed after '${args.landedLaneName}' merged: ${childError}. The lane was restored to its pre-rebase state. Open the Rebase/Merge tab to recover this lane.`,
}, child.id);
failedLaneIds.push(child.id);
}
diff --git a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx
index 8fabc39ab..d9fa62791 100644
--- a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx
+++ b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.test.tsx
@@ -328,7 +328,7 @@ describe("LaneGitActionsPane rescue action", () => {
expect(syncButton.getAttribute("title")).toMatch(/before rebasing and pushing/i);
});
- it("treats auto-rebase conflicts as failures and links to the Rebase tab", async () => {
+ it("treats auto-rebase conflicts as failures and links to the Rebase/Merge tab", async () => {
const user = userEvent.setup();
const resolveRebaseConflict = vi.fn();
mockAutoRebaseStatuses = [
@@ -345,7 +345,7 @@ describe("LaneGitActionsPane rescue action", () => {
renderPane({ onResolveRebaseConflict: resolveRebaseConflict });
- const rebaseTabButton = await screen.findByRole("button", { name: /open rebase tab/i });
+ const rebaseTabButton = await screen.findByRole("button", { name: /open rebase\/merge tab/i });
screen.getByText("AUTO-REBASE FAILED");
screen.getByText(/auto-rebase failed\. files need follow-up before this lane can be pushed\./i);
diff --git a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx
index 60cafb46a..e05b49ff1 100644
--- a/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx
+++ b/apps/desktop/src/renderer/components/lanes/LaneGitActionsPane.tsx
@@ -1546,7 +1546,7 @@ export function LaneGitActionsPane({
onResolveRebaseConflict(laneId, rebaseConflictParentLaneId);
return;
}
- const search = new URLSearchParams({ tab: "rebase", laneId });
+ const search = new URLSearchParams({ tab: "workflows", workflow: "rebase", laneId });
if (rebaseConflictParentLaneId) search.set("parentLaneId", rebaseConflictParentLaneId);
navigate(`/prs?${search.toString()}`);
};
@@ -1571,8 +1571,8 @@ export function LaneGitActionsPane({
{autoRebaseStatus.state !== "autoRebased" ? (
isAutoRebaseFailure ? (
) : (
diff --git a/apps/desktop/src/renderer/components/lanes/LaneRebaseBanner.tsx b/apps/desktop/src/renderer/components/lanes/LaneRebaseBanner.tsx
index c5462aa72..11ff13f9d 100644
--- a/apps/desktop/src/renderer/components/lanes/LaneRebaseBanner.tsx
+++ b/apps/desktop/src/renderer/components/lanes/LaneRebaseBanner.tsx
@@ -50,13 +50,13 @@ export function LaneRebaseBanner({
-
+
@@ -120,13 +120,13 @@ export function LaneRebaseBanner({
-
+
diff --git a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx
index 15fd9cba8..2fbc521eb 100644
--- a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx
+++ b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx
@@ -1101,7 +1101,7 @@ export function LanesPage() {
const failedLane = start.run.failedLaneId ? lanesById.get(start.run.failedLaneId)?.name ?? start.run.failedLaneId : null;
const detail = start.run.error ?? "Rebase failed.";
setRebaseSuggestionError(`Rebase needs attention${failedLane ? ` for ${failedLane}` : ""}. ${detail}`);
- navigate("/prs?tab=rebase");
+ navigate("/prs?tab=workflows&workflow=rebase");
return;
}
@@ -1121,7 +1121,7 @@ export function LanesPage() {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setRebaseSuggestionError(message);
- navigate("/prs?tab=rebase");
+ navigate("/prs?tab=workflows&workflow=rebase");
}
}, [lanesById, navigate, refreshLanes, requestPushSelection, requestRebaseScope]);
diff --git a/apps/desktop/src/renderer/components/prs/CreatePrModal.tsx b/apps/desktop/src/renderer/components/prs/CreatePrModal.tsx
index a078a9f7f..71bde9132 100644
--- a/apps/desktop/src/renderer/components/prs/CreatePrModal.tsx
+++ b/apps/desktop/src/renderer/components/prs/CreatePrModal.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { useNavigate } from "react-router-dom";
import * as Dialog from "@radix-ui/react-dialog";
import { GitPullRequest, GitMerge, Stack as Layers, CheckCircle, Warning, CircleNotch, X, GitBranch, Sparkle, ArrowRight, ArrowLeft, Check, DotsSixVertical, Trash, ArrowUp, ArrowDown } from "@phosphor-icons/react";
import { useAppStore } from "../../state/appStore";
@@ -446,7 +447,7 @@ function LaneWarningPanel({
cursor: "pointer",
}}
>
- Open Rebase Tab
+ Open Rebase/Merge Tab
Review rebase status before PR creation{rebaseLaneIds.length > 1 ? ` (${rebaseLaneIds.length} lanes)` : ""}.
@@ -470,6 +471,7 @@ export function CreatePrModal({
onOpenChange: (open: boolean) => void;
onCreated?: (created: PrSummary[]) => void | Promise;
}) {
+ const navigate = useNavigate();
const lanes = useAppStore((s) => s.lanes);
const primaryLane = React.useMemo(() => lanes.find((l) => l.laneType === "primary") ?? null, [lanes]);
@@ -596,8 +598,9 @@ export function CreatePrModal({
const openRebaseTab = React.useCallback((laneId: string) => {
onOpenChange(false);
- window.location.hash = `#/prs?tab=rebase&laneId=${encodeURIComponent(laneId)}`;
- }, [onOpenChange]);
+ const search = new URLSearchParams({ tab: "workflows", workflow: "rebase", laneId });
+ navigate({ pathname: "/prs", search: `?${search.toString()}` });
+ }, [navigate, onOpenChange]);
// Reset on close
React.useEffect(() => {
diff --git a/apps/desktop/src/renderer/components/prs/PRsPage.tsx b/apps/desktop/src/renderer/components/prs/PRsPage.tsx
index e10366d45..ec8f5ba97 100644
--- a/apps/desktop/src/renderer/components/prs/PRsPage.tsx
+++ b/apps/desktop/src/renderer/components/prs/PRsPage.tsx
@@ -11,7 +11,7 @@ import { GitHubTab } from "./tabs/GitHubTab";
import { WorkflowsTab, type WorkflowCategory } from "./tabs/WorkflowsTab";
import { SANS_FONT } from "../lanes/laneDesignTokens";
import { isMissionLaneHiddenByDefault } from "../lanes/laneUtils";
-import { buildPrsRouteSearch, parsePrsRouteState } from "./prsRouteState";
+import { buildPrsRouteSearch, parsePrsRouteState, resolvePrsActiveTab } from "./prsRouteState";
import { resolveRouteRebaseSelection } from "./shared/rebaseNeedUtils";
import type { PrSummary } from "../../../shared/types";
@@ -84,31 +84,21 @@ function PRsPageInner() {
search: location.search,
hash: window.location.hash,
});
- const tab = routeState.tab;
- const workflowTab = routeState.workflowTab;
+ const resolved = resolvePrsActiveTab(routeState);
const routeRebaseItemId = resolveRouteRebaseSelection({
rebaseNeeds,
routeItemId: routeState.laneId,
});
- if (tab === "github" || tab === "normal") {
- setActiveTab("normal");
- } else if (tab === "workflows") {
- const nextWorkflowTab = workflowTab === "queue" || workflowTab === "integration" || workflowTab === "rebase"
- ? workflowTab
- : "integration";
- setActiveTab(nextWorkflowTab);
- } else if (tab === "queue" || tab === "integration" || tab === "rebase") {
- setActiveTab(tab);
- }
+ setActiveTab(resolved.activeTab);
- if (tab === "normal" || tab === "github") {
+ if (!resolved.isWorkflowRoute && (routeState.tab === "normal" || routeState.tab === "github")) {
setSelectedPrId(routeState.prId ?? null);
}
- if (tab === "queue" || workflowTab === "queue") {
+ if (resolved.effectiveWorkflow === "queue") {
setSelectedQueueGroupId(routeState.queueGroupId ?? null);
}
- if (tab === "rebase" || workflowTab === "rebase") {
+ if (resolved.effectiveWorkflow === "rebase") {
setSelectedRebaseItemId(routeRebaseItemId);
}
} catch {
diff --git a/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts b/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts
index 421bd9c11..1e91c6339 100644
--- a/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts
+++ b/apps/desktop/src/renderer/components/prs/prsRouteState.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { buildPrsRouteSearch, parsePrsRouteState } from "./prsRouteState";
+import { buildPrsRouteSearch, parsePrsRouteState, resolvePrsActiveTab } from "./prsRouteState";
describe("prsRouteState", () => {
it("parses route state from search params and falls back to hash params when needed", () => {
@@ -98,3 +98,46 @@ describe("prsRouteState", () => {
).toBe("?tab=workflows&workflow=rebase&laneId=lane-456");
});
});
+
+describe("resolvePrsActiveTab", () => {
+ it("routes a stale tab=normal + hash workflow=rebase to the rebase workflow", () => {
+ const parsed = parsePrsRouteState({
+ search: "?tab=normal",
+ hash: "#/prs?tab=workflows&workflow=rebase&laneId=lane-1",
+ });
+ const resolved = resolvePrsActiveTab(parsed);
+ expect(resolved.isWorkflowRoute).toBe(true);
+ expect(resolved.effectiveWorkflow).toBe("rebase");
+ expect(resolved.activeTab).toBe("rebase");
+ });
+
+ it("falls back to integration when tab=workflows has no workflow param", () => {
+ const parsed = parsePrsRouteState({ search: "?tab=workflows" });
+ const resolved = resolvePrsActiveTab(parsed);
+ expect(resolved.isWorkflowRoute).toBe(true);
+ expect(resolved.effectiveWorkflow).toBeNull();
+ expect(resolved.activeTab).toBe("integration");
+ });
+
+ it("treats a workflow-alias tab (tab=queue) as a workflow route", () => {
+ const parsed = parsePrsRouteState({ search: "?tab=queue&queueGroupId=g-1" });
+ const resolved = resolvePrsActiveTab(parsed);
+ expect(resolved.isWorkflowRoute).toBe(true);
+ expect(resolved.effectiveWorkflow).toBe("queue");
+ expect(resolved.activeTab).toBe("queue");
+ });
+
+ it("keeps tab=normal on the normal tab when no workflow signal is present", () => {
+ const parsed = parsePrsRouteState({ search: "?tab=normal&prId=pr-1" });
+ const resolved = resolvePrsActiveTab(parsed);
+ expect(resolved.isWorkflowRoute).toBe(false);
+ expect(resolved.activeTab).toBe("normal");
+ });
+
+ it("returns normal when the route has no tab or workflow signal", () => {
+ const parsed = parsePrsRouteState({ search: "" });
+ const resolved = resolvePrsActiveTab(parsed);
+ expect(resolved.isWorkflowRoute).toBe(false);
+ expect(resolved.activeTab).toBe("normal");
+ });
+});
diff --git a/apps/desktop/src/renderer/components/prs/prsRouteState.ts b/apps/desktop/src/renderer/components/prs/prsRouteState.ts
index ecdea45a5..3a1db1960 100644
--- a/apps/desktop/src/renderer/components/prs/prsRouteState.ts
+++ b/apps/desktop/src/renderer/components/prs/prsRouteState.ts
@@ -60,6 +60,42 @@ export function parsePrsRouteState(args: { search?: string | null; hash?: string
};
}
+export type ResolvedPrsRoute = {
+ isWorkflowRoute: boolean;
+ effectiveWorkflow: PrWorkflowTab | null;
+ activeTab: "normal" | PrWorkflowTab;
+};
+
+/**
+ * Collapse a parsed route into a single activeTab decision.
+ *
+ * Routing bounce-back guard: the presence of a `workflow=` param, or a
+ * workflow-alias `tab=` value (queue/integration/rebase), is treated as
+ * authoritative evidence of a workflow route. This prevents a stale
+ * `?tab=normal` in the outer search (BrowserRouter mock mode) from shadowing
+ * a hash-based workflow URL.
+ */
+export function resolvePrsActiveTab(route: ParsedPrsRouteState): ResolvedPrsRoute {
+ const workflowAlias: PrWorkflowTab | null =
+ route.tab === "queue" || route.tab === "integration" || route.tab === "rebase"
+ ? route.tab
+ : null;
+ const effectiveWorkflow = route.workflowTab ?? workflowAlias;
+ const isWorkflowRoute = Boolean(effectiveWorkflow) || route.tab === "workflows";
+ if (isWorkflowRoute) {
+ return {
+ isWorkflowRoute: true,
+ effectiveWorkflow,
+ activeTab: effectiveWorkflow ?? "integration",
+ };
+ }
+ return {
+ isWorkflowRoute: false,
+ effectiveWorkflow: null,
+ activeTab: "normal",
+ };
+}
+
export function buildPrsRouteSearch(args: {
activeTab: PrActiveTab;
selectedPrId: string | null;
diff --git a/apps/desktop/src/renderer/components/prs/state/PrsContext.test.tsx b/apps/desktop/src/renderer/components/prs/state/PrsContext.test.tsx
index df0557a5a..7d68f33c1 100644
--- a/apps/desktop/src/renderer/components/prs/state/PrsContext.test.tsx
+++ b/apps/desktop/src/renderer/components/prs/state/PrsContext.test.tsx
@@ -23,6 +23,18 @@ function Harness() {
);
}
+function RouteHarness() {
+ const { activeTab, selectedPrId, selectedQueueGroupId, selectedRebaseItemId } = usePrs();
+ return (
+
{
- window.location.hash = `#/prs?tab=rebase&laneId=${encodeURIComponent(laneId)}`;
+ const search = new URLSearchParams({ tab: "workflows", workflow: "rebase", laneId });
+ navigate({ pathname: "/prs", search: `?${search.toString()}` });
}}
>
- OPEN REBASE TAB
+ OPEN REBASE/MERGE TAB
{onResimulate ? (
@@ -1447,7 +1447,7 @@ export function QueueTab({
) : null}
- Batch rebases stop at the first failure so the queue does not quietly drift. Use the Rebase tab for detailed lane history, aborts, rollbacks, and any follow-up after a partial run.
+ Batch rebases stop at the first failure so the queue does not quietly drift. Use the Rebase/Merge tab for detailed lane history, aborts, rollbacks, and any follow-up after a partial run.
>
@@ -1502,7 +1502,7 @@ export function QueueTab({
cursor: "pointer",
}}
>
- Open failed lane in rebase tab
+ Open failed lane in Rebase/Merge tab
) : null}
diff --git a/apps/desktop/src/renderer/components/prs/tabs/WorkflowsTab.tsx b/apps/desktop/src/renderer/components/prs/tabs/WorkflowsTab.tsx
index c0e4c0074..6947f0690 100644
--- a/apps/desktop/src/renderer/components/prs/tabs/WorkflowsTab.tsx
+++ b/apps/desktop/src/renderer/components/prs/tabs/WorkflowsTab.tsx
@@ -762,7 +762,7 @@ export function WorkflowsTab({
{([
{ id: "integration" as WorkflowCategory, label: "Integration", icon: GitBranch },
{ id: "queue" as WorkflowCategory, label: "Queue", icon: CaretRight },
- { id: "rebase" as WorkflowCategory, label: "Rebase", icon: Sparkle },
+ { id: "rebase" as WorkflowCategory, label: "Rebase/Merge", icon: Sparkle },
]).map((category) => {
const selected = activeCategory === category.id;
const catTheme = CATEGORY_THEMES[category.id];
diff --git a/apps/desktop/src/renderer/components/settings/LaneBehaviorSection.tsx b/apps/desktop/src/renderer/components/settings/LaneBehaviorSection.tsx
index fd4f42df2..485d4b563 100644
--- a/apps/desktop/src/renderer/components/settings/LaneBehaviorSection.tsx
+++ b/apps/desktop/src/renderer/components/settings/LaneBehaviorSection.tsx
@@ -224,15 +224,15 @@ export function LaneBehaviorSection() {
)}
- {/* Save + Open Rebase tab */}
+ {/* Save + Open Rebase/Merge tab */}
Review rebase status before PR creation{rebaseLaneIds.length > 1 ? ` (${rebaseLaneIds.length} lanes)` : ""}.
diff --git a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx
index 36c8e74cb..a0e46a057 100644
--- a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx
+++ b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx
@@ -35,6 +35,7 @@ import { DEFAULT_PR_TIMELINE_FILTERS } from "../shared/PrTimeline";
import { buildPrAiResolutionContextKey } from "../../../../shared/types";
import { getModelById } from "../../../../shared/modelRegistry";
import { parsePrsRouteState, resolvePrsActiveTab } from "../prsRouteState";
+import { resolveRouteRebaseSelection } from "../shared/rebaseNeedUtils";
type PrTab = "normal" | "queue" | "integration" | "rebase";
@@ -232,7 +233,14 @@ function readInitialRouteState(): {
? route.prId
: null,
selectedQueueGroupId: resolved.effectiveWorkflow === "queue" ? route.queueGroupId : null,
- selectedRebaseItemId: resolved.effectiveWorkflow === "rebase" ? route.laneId : null,
+ // Mirror PRsPage's resolver so the shape of this id matches what the
+ // rebase UI later expects. rebaseNeeds are empty at provider mount, so
+ // this returns the bare lane id; PRsPage's syncFromLocation effect runs
+ // the same resolver again once needs load and upgrades it to the
+ // canonical need-item key.
+ selectedRebaseItemId: resolved.effectiveWorkflow === "rebase"
+ ? resolveRouteRebaseSelection({ rebaseNeeds: [], routeItemId: route.laneId })
+ : null,
};
} catch { /* ignore */ }
return {
@@ -284,7 +292,11 @@ function diffPrIds(prev: PrWithConflicts[], next: PrWithConflicts[]): string[] {
}
export function PrsProvider({ children }: { children: React.ReactNode }) {
- const initialRouteState = readInitialRouteState();
+ // Compute initial route state exactly once per provider mount. Reading
+ // window.location + running parsePrsRouteState/resolvePrsActiveTab on every
+ // render would be wasteful; useMemo with empty deps captures it once so all
+ // four useState calls below share a single computation.
+ const initialRouteState = useMemo(() => readInitialRouteState(), []);
const [activeTab, setActiveTab] = useState(initialRouteState.activeTab);
const [prs, setPrs] = useState([]);
const [lanes, setLanes] = useState([]);
diff --git a/apps/desktop/src/renderer/components/prs/tabs/IntegrationTab.tsx b/apps/desktop/src/renderer/components/prs/tabs/IntegrationTab.tsx
index afcc3a4c9..1d7aedc59 100644
--- a/apps/desktop/src/renderer/components/prs/tabs/IntegrationTab.tsx
+++ b/apps/desktop/src/renderer/components/prs/tabs/IntegrationTab.tsx
@@ -161,7 +161,7 @@ function RebaseGuidancePanel({
navigate({ pathname: "/prs", search: `?${search.toString()}` });
}}
>
- OPEN REBASE/MERGE TAB
+ Open Rebase/Merge tab
{onResimulate ? (