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
3 changes: 0 additions & 3 deletions apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,7 +2058,6 @@ function registerLaneRemoteCommands({ args, register }: RemoteCommandRegistratio
register("lanes.listRebaseSuggestions", { viewerAllowed: true }, async () => args.rebaseSuggestionService?.listSuggestions() ?? []);
register("lanes.dismissRebaseSuggestion", { viewerAllowed: true, queueable: true }, async (payload) => {
const laneId = requireString(payload.laneId, "lanes.dismissRebaseSuggestion requires laneId.");
args.conflictService?.dismissRebase(laneId);
if (args.rebaseSuggestionService) {
await args.rebaseSuggestionService.dismiss({ laneId });
}
Expand All @@ -2067,8 +2066,6 @@ function registerLaneRemoteCommands({ args, register }: RemoteCommandRegistratio
register("lanes.deferRebaseSuggestion", { viewerAllowed: true, queueable: true }, async (payload) => {
const laneId = requireString(payload.laneId, "lanes.deferRebaseSuggestion requires laneId.");
const minutes = Math.max(5, Math.min(7 * 24 * 60, Math.floor(asOptionalNumber(payload.minutes) ?? 60)));
const until = new Date(Date.now() + minutes * 60_000).toISOString();
args.conflictService?.deferRebase(laneId, until);
if (args.rebaseSuggestionService) {
await args.rebaseSuggestionService.defer({
laneId,
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/services/adeActions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,14 +1450,11 @@ function buildLaneDomainService(runtime: AdeRuntime): OpaqueService {
},
dismissRebaseSuggestion: async (args?: { laneId?: string }) => {
const laneId = requireNonEmptyString(args?.laneId, "laneId");
runtime.conflictService?.dismissRebase(laneId);
await runtime.rebaseSuggestionService?.dismiss({ laneId });
},
deferRebaseSuggestion: async (args?: { laneId?: string; minutes?: number }) => {
const laneId = requireNonEmptyString(args?.laneId, "laneId");
const minutes = Math.max(5, Math.min(7 * 24 * 60, Math.floor(args?.minutes ?? 60)));
const until = new Date(Date.now() + minutes * 60_000).toISOString();
runtime.conflictService?.deferRebase(laneId, until);
await runtime.rebaseSuggestionService?.defer({ laneId, minutes });
},
listAutoRebaseStatuses: () =>
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/services/lanes/laneService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4066,8 +4066,13 @@ export function createLaneService({
let parentHead = "";
let parentTargetLabel = "";
let operationMetadata: Record<string, unknown> = {
runId,
rootLaneId: target.id,
reason,
recursive: scope === "lane_and_descendants",
scope,
pushMode,
actor,
};
try {
const parent = lane.parent_lane_id ? getLaneRow(lane.parent_lane_id) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,14 @@ function createMockConflictService() {
} as any;
}

function createMockRebaseSuggestionService() {
return {
listSuggestions: vi.fn().mockResolvedValue([]),
dismiss: vi.fn().mockResolvedValue(undefined),
defer: vi.fn().mockResolvedValue(undefined),
} as any;
}

function createMockWorkerAgentService() {
return {
listAgents: vi.fn().mockReturnValue([]),
Expand Down Expand Up @@ -610,6 +618,7 @@ describe("createSyncRemoteCommandService", () => {
let linearSyncService: ReturnType<typeof createMockLinearSyncService>;
let linearCredentialService: ReturnType<typeof createMockLinearCredentialService>;
let conflictService: ReturnType<typeof createMockConflictService>;
let rebaseSuggestionService: ReturnType<typeof createMockRebaseSuggestionService>;
let processService: ReturnType<typeof createMockProcessService>;
let issueInventoryService: ReturnType<typeof createMockIssueInventoryService>;
let queueLandingService: ReturnType<typeof createMockQueueLandingService>;
Expand All @@ -631,6 +640,7 @@ describe("createSyncRemoteCommandService", () => {
linearSyncService = createMockLinearSyncService();
linearCredentialService = createMockLinearCredentialService();
conflictService = createMockConflictService();
rebaseSuggestionService = createMockRebaseSuggestionService();
processService = createMockProcessService();
issueInventoryService = createMockIssueInventoryService();
queueLandingService = createMockQueueLandingService();
Expand All @@ -652,6 +662,7 @@ describe("createSyncRemoteCommandService", () => {
getLinearIssueTracker: () => linearIssueTracker,
getLinearSyncService: () => linearSyncService,
conflictService,
rebaseSuggestionService,
processService,
logger: createLogger() as any,
});
Expand Down Expand Up @@ -3172,36 +3183,29 @@ describe("createSyncRemoteCommandService", () => {
}))).rejects.toThrow("lanes.rebasePush requires laneIds.");
});

it("lanes.dismissRebaseSuggestion routes to conflictService even without a rebaseSuggestionService", async () => {
it("lanes.dismissRebaseSuggestion hides the banner without dismissing the rebase need", async () => {
const result = await service.execute(makePayload("lanes.dismissRebaseSuggestion", {
laneId: "lane-1",
}));
expect(conflictService.dismissRebase).toHaveBeenCalledWith("lane-1");
expect(rebaseSuggestionService.dismiss).toHaveBeenCalledWith({ laneId: "lane-1" });
expect(conflictService.dismissRebase).not.toHaveBeenCalled();
expect(result).toEqual({ ok: true });
});

it("lanes.deferRebaseSuggestion clamps minutes and forwards an ISO timestamp to conflictService", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date("2026-04-15T22:00:00.000Z"));
await service.execute(makePayload("lanes.deferRebaseSuggestion", {
laneId: "lane-1",
minutes: 1,
}));
expect(conflictService.deferRebase).toHaveBeenCalledWith(
"lane-1",
"2026-04-15T22:05:00.000Z",
);
conflictService.deferRebase.mockClear();
await service.execute(makePayload("lanes.deferRebaseSuggestion", {
laneId: "lane-1",
minutes: 60 * 24 * 30,
}));
const [, until] = conflictService.deferRebase.mock.calls.at(-1) ?? [];
expect(until).toBe(new Date(Date.parse("2026-04-15T22:00:00.000Z") + 7 * 24 * 60 * 60_000).toISOString());
} finally {
vi.useRealTimers();
}
it("lanes.deferRebaseSuggestion clamps minutes and snoozes the banner without deferring the rebase need", async () => {
await service.execute(makePayload("lanes.deferRebaseSuggestion", {
laneId: "lane-1",
minutes: 1,
}));
expect(rebaseSuggestionService.defer).toHaveBeenCalledWith({ laneId: "lane-1", minutes: 5 });
expect(conflictService.deferRebase).not.toHaveBeenCalled();

await service.execute(makePayload("lanes.deferRebaseSuggestion", {
laneId: "lane-1",
minutes: 60 * 24 * 30,
}));
expect(rebaseSuggestionService.defer).toHaveBeenLastCalledWith({ laneId: "lane-1", minutes: 7 * 24 * 60 });
expect(conflictService.deferRebase).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type * as ReactNamespace from "react";
import type * as RouterNamespace from "react-router-dom";
import { ADE_OPEN_BUILT_IN_BROWSER_EVENT } from "../../lib/openExternal";

const ROUTE_INTEGRATION_TIMEOUT_MS = 45_000;

const workLifecycle = vi.hoisted(() => ({
mounts: 0,
unmounts: 0,
Expand Down Expand Up @@ -186,6 +188,7 @@ vi.mock("../lanes/LanesPage", async () => {

describe("App Work route keep-alive", () => {
beforeEach(() => {
cleanup();
vi.clearAllMocks();
workLifecycle.mounts = 0;
workLifecycle.unmounts = 0;
Expand Down Expand Up @@ -257,7 +260,7 @@ describe("App Work route keep-alive", () => {
});
expect(workLifecycle.mounts).toBe(1);
expect(workLifecycle.unmounts).toBe(0);
});
}, ROUTE_INTEGRATION_TIMEOUT_MS);

it("parks the native Work browser view when the Work route is backgrounded", async () => {
const { App } = await import("./App");
Expand Down Expand Up @@ -285,7 +288,7 @@ describe("App Work route keep-alive", () => {
visible: false,
});
});
});
}, ROUTE_INTEGRATION_TIMEOUT_MS);

it("reveals the Work browser pane when an ADE browser URL opens from another tab", async () => {
window.history.replaceState({}, "", "/files");
Expand All @@ -310,7 +313,7 @@ describe("App Work route keep-alive", () => {
workSidebarTab: "browser",
}),
);
});
}, ROUTE_INTEGRATION_TIMEOUT_MS);

it("hydrates project stores with launch clipboard reminder preferences", async () => {
appStoreState.launchPromptClipboardNoticeEnabled = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5797,7 +5797,7 @@ describe("AgentChatPane submit recovery", () => {
await waitFor(() => {
expect(create).toHaveBeenCalledTimes(2);
expect(send).toHaveBeenCalledTimes(2);
});
}, { timeout: 5000 });
expect(writeClipboardText).toHaveBeenCalledTimes(1);
expect(writeClipboardText).toHaveBeenCalledWith("Fix the login bug");
expect(create).toHaveBeenNthCalledWith(1, expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ describe("PrRebaseBanner", () => {
execute: vi.fn(async () => {
throw new Error("rebase failed");
}),
dismiss: vi.fn(),
},
lanes: {
dismissRebaseSuggestion: vi.fn(),
},
};

Expand All @@ -58,7 +60,9 @@ describe("PrRebaseBanner", () => {
(window as any).ade = {
rebase: {
execute: vi.fn(async () => undefined),
dismiss: vi.fn(),
},
lanes: {
dismissRebaseSuggestion: vi.fn(),
},
};

Expand All @@ -77,4 +81,34 @@ describe("PrRebaseBanner", () => {
await waitFor(() => expect(onRefresh).toHaveBeenCalledTimes(1));
expect(onRebaseDone).toHaveBeenCalledTimes(1);
});

it("hides the banner without dismissing the active rebase need", async () => {
const dismissRebaseSuggestion = vi.fn(async () => undefined);
const onRefresh = vi.fn(async () => undefined);
(window as any).ade = {
rebase: {
execute: vi.fn(async () => undefined),
dismiss: vi.fn(),
},
lanes: {
dismissRebaseSuggestion,
},
};

render(
<PrRebaseBanner
laneId="lane-1"
rebaseNeeds={[makeNeed()]}
onTabChange={() => {}}
onRefresh={onRefresh}
/>,
);

fireEvent.click(screen.getByRole("button", { name: /HIDE BANNER/i }));

await waitFor(() => expect(dismissRebaseSuggestion).toHaveBeenCalledWith({ laneId: "lane-1" }));
expect((window as any).ade.rebase.dismiss).not.toHaveBeenCalled();
expect(onRefresh).toHaveBeenCalledTimes(1);
expect(screen.queryByText(/2 commits behind main/i)).toBeNull();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
7 changes: 3 additions & 4 deletions apps/desktop/src/renderer/components/prs/PrRebaseBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export function PrRebaseBanner({ laneId, rebaseNeeds, autoRebaseStatuses, onTabC

const need = React.useMemo(() => {
const laneNeed = findLaneBaseNeed(rebaseNeeds, laneId);
if (!laneNeed || laneNeed.behindBy <= 0 || laneNeed.dismissedAt) return null;
if (laneNeed.deferredUntil && new Date(laneNeed.deferredUntil) > new Date()) return null;
if (!laneNeed || laneNeed.behindBy <= 0) return null;
return laneNeed;
}, [laneId, rebaseNeeds]);
const autoStatus = autoRebaseStatuses?.find((s) => s.laneId === laneId);
Expand Down Expand Up @@ -93,7 +92,7 @@ export function PrRebaseBanner({ laneId, rebaseNeeds, autoRebaseStatuses, onTabC
const handleDismiss = async () => {
setActionError(null);
try {
await window.ade.rebase.dismiss(laneId);
await window.ade.lanes.dismissRebaseSuggestion({ laneId });
await onRefresh?.();
setDismissed(true);
} catch (error) {
Expand Down Expand Up @@ -168,7 +167,7 @@ export function PrRebaseBanner({ laneId, rebaseNeeds, autoRebaseStatuses, onTabC
}}
onClick={() => void handleDismiss()}
>
DISMISS
HIDE BANNER
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { isDirtyWorktreeErrorMessage, stripDirtyWorktreePrefix } from "../shared
import { deriveIntegrationPrLiveModel } from "../shared/integrationPrModel";
import { PrAiResolverPanel } from "../shared/PrAiResolverPanel";
import { findLaneBaseNeed, findMatchingRebaseNeed, rebaseNeedItemKey } from "../shared/rebaseNeedUtils";
import { getActiveRebaseNeeds } from "./rebaseWorkflowModel";

/* ---- Outcome dot with design-system colors ---- */

Expand Down Expand Up @@ -603,8 +604,8 @@ export function IntegrationTab({ prs, lanes, mergeContextByPrId, mergeMethod, se
);
const rebaseNeedByLaneId = React.useMemo(
() => new Map(
rebaseNeeds
.filter((need) => need.kind === "lane_base" && need.behindBy > 0 && !need.dismissedAt)
getActiveRebaseNeeds(rebaseNeeds)
.filter((need) => need.kind === "lane_base")
.map((need) => [need.laneId, need] as const),
),
[rebaseNeeds],
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/src/renderer/components/prs/tabs/RebaseTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export function RebaseTab({
if (!selectedNeed) return;
setRebaseError(null);
try {
await window.ade.rebase.dismiss(selectedNeed.laneId);
await window.ade.lanes.dismissRebaseSuggestion({ laneId: selectedNeed.laneId });
await onRefresh();
} catch (err: unknown) {
setRebaseError(err instanceof Error ? err.message : String(err));
Expand All @@ -509,10 +509,9 @@ export function RebaseTab({

const handleDefer = async () => {
if (!selectedNeed) return;
const until = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString();
setRebaseError(null);
try {
await window.ade.rebase.defer(selectedNeed.laneId, until);
await window.ade.lanes.deferRebaseSuggestion({ laneId: selectedNeed.laneId, minutes: 4 * 60 });
await onRefresh();
} catch (err: unknown) {
setRebaseError(err instanceof Error ? err.message : String(err));
Expand Down Expand Up @@ -1329,7 +1328,7 @@ export function RebaseTab({
>
<Clock size={12} className="mr-1" />
<span className="font-mono font-bold uppercase" style={{ fontSize: 10, letterSpacing: "1px" }}>
DEFER 4H
SNOOZE BANNER 4H
</span>
</Button>
<Button
Expand All @@ -1341,7 +1340,7 @@ export function RebaseTab({
>
<XCircle size={12} className="mr-1" />
<span className="font-mono font-bold uppercase" style={{ fontSize: 10, letterSpacing: "1px" }}>
DISMISS
HIDE BANNER
</span>
</Button>
</div>
Expand Down
Loading
Loading