Skip to content

Commit 3af8ad4

Browse files
authored
Improve lane PR badges and lane tab controls (#284)
* ship: prepare lane for review * ship: iteration 1 - address review comments * ship: iteration 2 - stabilize lane PR badges * ship: iteration 3 - address lane PR review
1 parent 2bae204 commit 3af8ad4

6 files changed

Lines changed: 532 additions & 198 deletions

File tree

apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ describe("local runtime connection pool", () => {
305305
expect(fs.existsSync(tsxLoaderPath)).toBe(true);
306306

307307
const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-"));
308-
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-project-"));
308+
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-project-")));
309309
const socketPath = path.join(adeHome, "sock", "ade.sock");
310310
const originalEnv = {
311311
ADE_CLI_JS: process.env.ADE_CLI_JS,
@@ -364,7 +364,7 @@ describe("local runtime connection pool", () => {
364364
expect(fs.existsSync(tsxLoaderPath)).toBe(true);
365365

366366
const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-"));
367-
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-project-"));
367+
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-version-project-")));
368368
const socketPath = path.join(adeHome, "sock", "ade.sock");
369369
const originalEnv = {
370370
ADE_CLI_JS: process.env.ADE_CLI_JS,
@@ -450,7 +450,7 @@ describe("local runtime connection pool", () => {
450450
expect(fs.existsSync(tsxLoaderPath)).toBe(true);
451451

452452
const adeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-"));
453-
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-project-"));
453+
const projectRoot = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), "ade-local-runtime-build-project-")));
454454
const socketPath = path.join(adeHome, "sock", "ade.sock");
455455
const originalEnv = {
456456
ADE_CLI_JS: process.env.ADE_CLI_JS,

apps/desktop/src/renderer/components/lanes/LanesPage.test.ts

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { describe, expect, it } from "vitest";
22
import {
3+
githubPrMatchesCurrentBranch,
34
lanePrMatchesCurrentBranch,
45
resolveLaneDeleteStartSelection,
56
resolveCreateLaneRequest,
67
resolveLaneIdsDeepLinkSelection,
8+
selectGithubLanePrTag,
9+
selectLaneTabPrTag,
710
selectLanePrTag,
8-
} from "./LanesPage";
9-
import type { LaneSummary, PrSummary } from "../../../shared/types";
11+
sortLaneListRows,
12+
} from "./lanePageModel";
13+
import type { GitHubPrListItem, LaneSummary, PrSummary } from "../../../shared/types";
1014

1115
type LanePrTarget = Pick<LaneSummary, "id" | "laneType" | "branchRef" | "baseRef">;
1216

@@ -45,6 +49,36 @@ function makePr(overrides: Partial<PrSummary> = {}): PrSummary {
4549
};
4650
}
4751

52+
function makeGitHubPr(overrides: Partial<GitHubPrListItem> = {}): GitHubPrListItem {
53+
return {
54+
id: "repo-pr-224",
55+
scope: "repo",
56+
repoOwner: "arul28",
57+
repoName: "ADE",
58+
githubPrNumber: 224,
59+
githubUrl: "https://github.com/arul28/ADE/pull/224",
60+
title: "Show merged PR state",
61+
state: "open",
62+
isDraft: false,
63+
baseBranch: "main",
64+
headBranch: "origin/ade/pr-state",
65+
author: "arul",
66+
createdAt: "2026-05-01T00:00:00.000Z",
67+
updatedAt: "2026-05-01T00:00:00.000Z",
68+
linkedPrId: null,
69+
linkedGroupId: null,
70+
linkedLaneId: null,
71+
linkedLaneName: null,
72+
adeKind: null,
73+
workflowDisplayState: null,
74+
cleanupState: null,
75+
labels: [],
76+
isBot: false,
77+
commentCount: 0,
78+
...overrides,
79+
};
80+
}
81+
4882
describe("resolveCreateLaneRequest", () => {
4983
it("creates an independent lane from the selected primary branch", () => {
5084
expect(
@@ -235,3 +269,119 @@ describe("selectLanePrTag", () => {
235269
).toBe(false);
236270
});
237271
});
272+
273+
describe("selectLaneTabPrTag", () => {
274+
it("falls back to repo GitHub PRs by lane branch when no ADE PR row is mapped", () => {
275+
const githubPr = makeGitHubPr({
276+
state: "merged",
277+
linkedPrId: null,
278+
headBranch: "ade/pr-state",
279+
});
280+
281+
expect(githubPrMatchesCurrentBranch(makeLane(), githubPr)).toBe(true);
282+
expect(selectGithubLanePrTag(makeLane(), [githubPr])).toBe(githubPr);
283+
expect(selectLaneTabPrTag(makeLane(), [], [githubPr])).toMatchObject({
284+
source: "github",
285+
linkedPrId: null,
286+
githubPrNumber: 224,
287+
state: "merged",
288+
});
289+
});
290+
291+
it("prefers an ADE-mapped PR over an unlinked GitHub branch match", () => {
292+
const mappedPr = makePr({ id: "mapped-pr", state: "closed" });
293+
const githubPr = makeGitHubPr({ id: "github-pr", state: "open" });
294+
295+
expect(selectLaneTabPrTag(makeLane(), [mappedPr], [githubPr])).toMatchObject({
296+
source: "ade",
297+
id: "mapped-pr",
298+
linkedPrId: "mapped-pr",
299+
state: "closed",
300+
});
301+
});
302+
303+
it("labels GitHub-only draft PRs as draft lane tags", () => {
304+
expect(selectLaneTabPrTag(makeLane(), [], [
305+
makeGitHubPr({
306+
state: "open",
307+
isDraft: true,
308+
}),
309+
])).toMatchObject({
310+
source: "github",
311+
state: "draft",
312+
});
313+
});
314+
315+
it("does not route a branch-matched GitHub PR through a stale linked lane", () => {
316+
const githubPr = makeGitHubPr({
317+
linkedPrId: "other-pr",
318+
linkedLaneId: "other-lane",
319+
});
320+
321+
expect(selectLaneTabPrTag(makeLane(), [], [githubPr])).toMatchObject({
322+
source: "github",
323+
linkedPrId: null,
324+
githubUrl: "https://github.com/arul28/ADE/pull/224",
325+
});
326+
});
327+
328+
it("does not match repo GitHub PRs for primary while primary is on its base branch", () => {
329+
expect(
330+
githubPrMatchesCurrentBranch(
331+
makeLane({
332+
laneType: "primary",
333+
branchRef: "main",
334+
baseRef: "main",
335+
}),
336+
makeGitHubPr({ headBranch: "main" }),
337+
),
338+
).toBe(false);
339+
});
340+
});
341+
342+
describe("sortLaneListRows", () => {
343+
it("keeps primary first and promotes pinned lanes before runtime buckets", () => {
344+
const lanes = [
345+
{ id: "primary", laneType: "primary" },
346+
{ id: "running", laneType: "worktree" },
347+
{ id: "pinned-ended", laneType: "worktree" },
348+
{ id: "idle", laneType: "worktree" },
349+
] as const;
350+
351+
const result = sortLaneListRows({
352+
lanes: [...lanes],
353+
laneRuntimeById: new Map([
354+
["running", { bucket: "running" }],
355+
["pinned-ended", { bucket: "ended" }],
356+
["idle", { bucket: "none" }],
357+
]),
358+
laneStatusFilter: "all",
359+
laneOrderById: new Map(lanes.map((lane, index) => [lane.id, index])),
360+
pinnedLaneIds: new Set(["pinned-ended"]),
361+
});
362+
363+
expect(result.map((lane) => lane.id)).toEqual(["primary", "pinned-ended", "running", "idle"]);
364+
});
365+
366+
it("keeps pinned ordering after applying a status filter", () => {
367+
const lanes = [
368+
{ id: "running-a", laneType: "worktree" },
369+
{ id: "running-pinned", laneType: "worktree" },
370+
{ id: "ended", laneType: "worktree" },
371+
] as const;
372+
373+
const result = sortLaneListRows({
374+
lanes: [...lanes],
375+
laneRuntimeById: new Map([
376+
["running-a", { bucket: "running" }],
377+
["running-pinned", { bucket: "running" }],
378+
["ended", { bucket: "ended" }],
379+
]),
380+
laneStatusFilter: "running",
381+
laneOrderById: new Map(lanes.map((lane, index) => [lane.id, index])),
382+
pinnedLaneIds: new Set(["running-pinned"]),
383+
});
384+
385+
expect(result.map((lane) => lane.id)).toEqual(["running-pinned", "running-a"]);
386+
});
387+
});

0 commit comments

Comments
 (0)