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
10 changes: 7 additions & 3 deletions apps/mobile/src/features/threads/thread-list-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,24 @@ export const THREAD_LIST_COMPACT_INSET = HOME_HORIZONTAL_INSET;
const SIDEBAR_ROW_RADIUS = 12;

function pullRequestTintColor(
pr: Pick<ThreadPrPresentation, "state" | "isDraft" | "others">,
pr: Pick<ThreadPrPresentation, "state" | "isDraft" | "others" | "kind">,
colorScheme: "light" | "dark",
) {
const dark = colorScheme === "dark";
if (pr.others > 0 || (pr.state === "open" && pr.isDraft === true)) {
if (pr.state === "open" && pr.isDraft === true) {
return dark ? "#a1a1aa" : "#71717a";
}
switch (pr.state) {
case "open":
return dark ? "#34d399" : "#059669";
case "merged":
return dark ? "#a78bfa" : "#7c3aed";
case null:
case "closed":
if (pr.kind === "stack" || pr.others > 0) {
return dark ? "#fb7185" : "#e11d48";
}
return dark ? "#a1a1aa" : "#71717a";
case null:
return dark ? "#a1a1aa" : "#71717a";
}
}
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
? materialYouStyleLayoutActive
? "accent-thread-selected-foreground"
: "accent-user-bubble-foreground"
: "accent-foreground-muted"
: pr.state === null || pr.isDraft
? "accent-foreground-muted"
: pr.state === "open"
? "accent-adaptive-emerald-600-400"
: pr.state === "closed"
? "accent-adaptive-rose-600-400"
: "accent-adaptive-violet-600-400"
}
/>
) : null}
Expand Down
23 changes: 17 additions & 6 deletions apps/mobile/src/state/thread-pr-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ export function presentThreadLinkedPullRequests(
const badge = resolveThreadPullRequestBadge(links);
if (link === null || badge === null) return null;
const snapshot = link.snapshot;
const state = badge.kind === "stack" ? badge.state : (snapshot?.state ?? null);
const isDraft = snapshot?.isDraft === true && state === "open";
const linkedCount = badge.kind === "pull-request" && badge.others > 0 ? badge.others + 1 : null;
const isMultiple = badge.kind === "stack" || linkedCount !== null;
const state = isMultiple
? badge.state === "draft"
? "open"
: badge.state
: (snapshot?.state ?? null);
const isDraft = isMultiple
? badge.state === "draft"
: snapshot?.isDraft === true && state === "open";
const label =
badge.kind === "stack"
? String(badge.layers)
Expand All @@ -83,12 +90,16 @@ export function presentThreadLinkedPullRequests(
label,
accessibilityLabel:
badge.kind === "stack"
? `${badge.layers} pull requests in stack, ${state ?? "status pending"}`
: `#${link.number} pull request ${state === null ? "status pending" : isDraft ? "draft" : state}${badge.others > 0 ? `, ${badge.others} more linked` : ""}`,
? `${badge.layers} pull requests in stack, ${isDraft ? "draft" : (state ?? "status pending")}`
: linkedCount !== null
? `${linkedCount} linked pull requests, overall ${badge.state}`
: `#${link.number} pull request ${state === null ? "status pending" : isDraft ? "draft" : state}`,
textClassName:
linkedCount !== null || state === null || isDraft
state === null || isDraft
? "text-foreground-muted"
: PR_STATE_TEXT_CLASS[state],
: isMultiple && state === "closed"
? "text-adaptive-rose-600-400"
: PR_STATE_TEXT_CLASS[state],
};
}

Expand Down
34 changes: 33 additions & 1 deletion apps/mobile/src/state/use-thread-pr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,42 @@ describe("presentThreadLinkedPullRequests", () => {
kind: "pull-request",
label: "+2",
others: 1,
textClassName: "text-foreground-muted",
state: "open",
isDraft: false,
textClassName: "text-adaptive-emerald-600-400",
});
});

it.each([
["closed", false, "closed", false, "closed", false, "text-adaptive-rose-600-400"],
["open", true, "open", true, "open", true, "text-foreground-muted"],
["open", true, "open", false, "open", false, "text-adaptive-emerald-600-400"],
["closed", false, "open", false, "open", false, "text-adaptive-emerald-600-400"],
["merged", false, "merged", false, "merged", false, "text-adaptive-violet-600-400"],
["closed", false, "merged", false, "closed", false, "text-adaptive-rose-600-400"],
] as const)(
"colors linked %s (draft %s) and %s (draft %s) by their aggregate state",
(firstState, firstDraft, secondState, secondDraft, state, isDraft, textClassName) => {
const first = linkedPr(1);
const second = linkedPr(2);
expect(
presentThreadLinkedPullRequests([
{ ...first, snapshot: { ...first.snapshot!, state: firstState, isDraft: firstDraft } },
{
...second,
snapshot: { ...second.snapshot!, state: secondState, isDraft: secondDraft },
},
]),
).toMatchObject({
label: "+2",
state,
isDraft,
textClassName,
accessibilityLabel: `2 linked pull requests, overall ${isDraft ? "draft" : state}`,
});
},
);

it("uses the top of a derived stack even when its bottom was linked later", () => {
const bottom = linkedPr(1, { linkedAt: "2026-09-09T00:00:00.000Z" });
const top = linkedPr(2);
Expand Down
13 changes: 6 additions & 7 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function ThreadPullRequestBadgeControl({
? `Stack of ${badge.layers} pull requests, ${badge.state}`
: `${status?.tooltip ?? `PR #${number}, status pending`}${
badge?.kind === "pull-request" && badge.others > 0
? `, and ${badge.others} more linked`
? `, and ${badge.others} more linked; overall ${badge.state}`
: ""
}`;
const className = cn(
Expand All @@ -170,11 +170,9 @@ export function ThreadPullRequestBadgeControl({
"text-xs tabular-nums",
variant === "ghost" &&
"font-normal text-xs! active:scale-100 [--control-icon-color:currentColor]",
linkedCount !== null
? "text-secondary-label"
: isStack
? PR_STATE_COLOR_CLASS[badge.state]
: (status?.colorClass ?? "text-muted-foreground"),
badge !== null && (isStack || linkedCount !== null)
? PR_STATE_COLOR_CLASS[badge.state]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
: (status?.colorClass ?? "text-muted-foreground"),
);
const content = (
<>
Expand Down Expand Up @@ -276,10 +274,11 @@ export function ThreadPullRequestsMiniList({
}

/** The ink each pull-request state wears in the sidebar, shared by the number and stack badges. */
const PR_STATE_COLOR_CLASS: Record<NonNullable<ThreadPr>["state"], string> = {
const PR_STATE_COLOR_CLASS: Record<ThreadPullRequestBadge["state"], string> = {
open: "text-emerald-600 dark:text-emerald-300/90",
merged: "text-violet-600 dark:text-violet-300/90",
closed: "text-red-600 dark:text-red-300/90",
draft: "text-zinc-500 dark:text-zinc-400/80",
};

export function settledPrHoverColorClass(
Expand Down
41 changes: 40 additions & 1 deletion packages/shared/src/threadPullRequests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,40 @@ describe("resolveThreadPullRequestChains", () => {
});

describe("chain selection and badge state", () => {
it.each([
["open", false, "open", false, "open"],
["closed", false, "closed", false, "closed"],
["open", true, "open", true, "draft"],
["open", false, "open", true, "open"],
["closed", false, "open", true, "open"],
["merged", false, "merged", false, "merged"],
["merged", false, "closed", true, "closed"],
] as const)(
"aggregates %s (draft %s) and %s (draft %s) as %s",
(firstState, firstDraft, secondState, secondDraft, state) => {
for (const stacked of [false, true]) {
const links = [
link(1, {
snapshot: snapshot({ state: firstState, isDraft: firstDraft, headBranch: "base" }),
}),
link(2, {
snapshot: snapshot({
state: secondState,
isDraft: secondDraft,
baseBranch: stacked ? "base" : "main",
}),
}),
link(3, { source: "stack-dismissed" }),
];
expect(resolveThreadPullRequestBadge(links)).toEqual(
stacked
? { kind: "stack", layers: 2, state }
: { kind: "pull-request", others: 1, state },
);
}
},
);

it.each(["open", "merged", "closed"] as const)(
"targets the top of a derived %s chain despite a later bottom update and link",
(state) => {
Expand Down Expand Up @@ -309,6 +343,7 @@ describe("chain selection and badge state", () => {
expect(resolveThreadPullRequestBadge([bottom, top, link(3)])).toEqual({
kind: "pull-request",
others: 2,
state: "open",
});
expect(resolveThreadPullRequestBadge([link(3, { source: "stack-dismissed" })])).toBeNull();
});
Expand Down Expand Up @@ -340,7 +375,11 @@ describe("chain selection and badge state", () => {
kind: "stack",
top: { number: 2 },
});
expect(resolveThreadPullRequestBadge(links)).toEqual({ kind: "pull-request", others: 1 });
expect(resolveThreadPullRequestBadge(links)).toEqual({
kind: "pull-request",
others: 1,
state: "open",
});
});

it("does not guess a parent when a head branch was reused", () => {
Expand Down
22 changes: 13 additions & 9 deletions packages/shared/src/threadPullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,31 +243,35 @@ export function resolveThreadPullRequestChains(
return chains;
}

export type ThreadPullRequestBadge =
export type ThreadPullRequestBadge = {
readonly state: "open" | "closed" | "merged" | "draft";
} & (
| {
readonly kind: "stack";
readonly layers: number;
readonly state: "open" | "closed" | "merged";
}
| { readonly kind: "pull-request"; readonly others: number };
| { readonly kind: "pull-request"; readonly others: number }
);

/** Aggregate a single chain's state; unrelated links show a count beside the current PR. */
/** Aggregate visible links' state for both stacks and unrelated linked counts. */
export function resolveThreadPullRequestBadge(
pullRequests: ReadonlyArray<ThreadPullRequestLink> | undefined,
): ThreadPullRequestBadge | null {
const visible = visibleThreadPullRequests(pullRequests ?? []);
if (visible.length === 0) return null;
const chains = resolveThreadPullRequestChains(visible);
if (visible.length > 1 && chains.length === 1) {
const states = visible.map((link) => link.snapshot?.state ?? "open");
const state = states.includes("open")
const states = visible.map((link) => link.snapshot?.state ?? "open");
const state = visible.every((link) => link.snapshot?.state === "open" && link.snapshot.isDraft)
? "draft"
: states.includes("open")
? "open"
: states.every((entry) => entry === "merged")
? "merged"
: "closed";
const chains = resolveThreadPullRequestChains(visible);
if (visible.length > 1 && chains.length === 1) {
return { kind: "stack", layers: visible.length, state };
}
return { kind: "pull-request", others: visible.length - 1 };
return { kind: "pull-request", others: visible.length - 1, state };
}

/** Search terms for visible PR links, including the legacy single-link projection. */
Expand Down
Loading