From a401fc617843b529159564e622b49aed9830db69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 15:11:10 +0200 Subject: [PATCH 1/4] Show compact PR numbers in mobile thread rows - Add shared PR presentation for compact #number labels - Render PR state icons with theme-aware colors Co-authored-by: codex --- .../features/threads/thread-list-items.tsx | 62 ++++++++++++++++--- .../src/state/thread-pr-presentation.ts | 28 +++++++++ apps/mobile/src/state/use-thread-pr.test.ts | 22 +++++++ apps/mobile/src/state/use-thread-pr.ts | 40 +++--------- 4 files changed, 111 insertions(+), 41 deletions(-) create mode 100644 apps/mobile/src/state/thread-pr-presentation.ts create mode 100644 apps/mobile/src/state/use-thread-pr.test.ts diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index eb336179851..f6eca32f659 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -6,8 +6,9 @@ import type { import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; import { memo, useCallback, useMemo, type ComponentProps } from "react"; -import { Pressable, useWindowDimensions, View } from "react-native"; +import { Pressable, useColorScheme, useWindowDimensions, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; +import Svg, { Circle, Path } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; @@ -16,7 +17,7 @@ import { cn } from "../../lib/cn"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; -import { useThreadPr } from "../../state/use-thread-pr"; +import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr"; import type { HomeGroupDisplayAction } from "../home/homeListItems"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { resolveThreadStatus } from "./threadPresentation"; @@ -33,6 +34,41 @@ export type ThreadListVariant = "compact" | "sidebar"; export const THREAD_LIST_COMPACT_INSET = 20; const SIDEBAR_ROW_RADIUS = 12; +function pullRequestTintColor( + state: ThreadPr["state"], + colorScheme: ReturnType, +) { + const dark = colorScheme === "dark"; + switch (state) { + case "open": + return dark ? "#34d399" : "#059669"; + case "merged": + return dark ? "#a78bfa" : "#7c3aed"; + case "closed": + return dark ? "#a1a1aa" : "#71717a"; + } +} + +function PullRequestIcon(props: { readonly size: number; readonly color: string }) { + return ( + + + + + + + ); +} + /* ─── Project group header ───────────────────────────────────────────── */ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props: { @@ -394,6 +430,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { >["simultaneousWithExternalGesture"]; }) { const { width: windowWidth } = useWindowDimensions(); + const colorScheme = useColorScheme(); const compact = props.variant === "compact"; const selected = props.selected === true; // Recycling-safe: resets when the list container is reused for another @@ -479,13 +516,22 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ) : null} {pr !== null ? ( - - {pr.label} - + + + {pr.label} + + ) : null} ) : null; diff --git a/apps/mobile/src/state/thread-pr-presentation.ts b/apps/mobile/src/state/thread-pr-presentation.ts new file mode 100644 index 00000000000..e30918a4b91 --- /dev/null +++ b/apps/mobile/src/state/thread-pr-presentation.ts @@ -0,0 +1,28 @@ +import type { VcsStatusResult } from "@t3tools/contracts"; + +export type ThreadPr = NonNullable; + +export interface ThreadPrPresentation { + readonly number: number; + readonly state: ThreadPr["state"]; + readonly url: string; + /** Compact desktop-style label, e.g. "#3774". */ + readonly label: string; + readonly textClassName: string; +} + +const PR_STATE_TEXT_CLASS: Record = { + open: "text-emerald-600 dark:text-emerald-400", + merged: "text-violet-600 dark:text-violet-400", + closed: "text-zinc-500 dark:text-zinc-400", +}; + +export function presentThreadPr(pr: ThreadPr): ThreadPrPresentation { + return { + number: pr.number, + state: pr.state, + url: pr.url, + label: `#${pr.number}`, + textClassName: PR_STATE_TEXT_CLASS[pr.state], + }; +} diff --git a/apps/mobile/src/state/use-thread-pr.test.ts b/apps/mobile/src/state/use-thread-pr.test.ts new file mode 100644 index 00000000000..4619e05649b --- /dev/null +++ b/apps/mobile/src/state/use-thread-pr.test.ts @@ -0,0 +1,22 @@ +import type { VcsStatusResult } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { presentThreadPr } from "./thread-pr-presentation"; + +const pullRequest: NonNullable = { + number: 3774, + title: "Desktop-style pull request indicator", + url: "https://github.com/t3tools/t3code/pull/3774", + baseRef: "main", + headRef: "codex/desktop-style-pr-indicator", + state: "merged", +}; + +describe("presentThreadPr", () => { + it("uses the compact desktop-style pull request number label", () => { + expect(presentThreadPr(pullRequest)).toMatchObject({ + label: "#3774", + textClassName: "text-violet-600 dark:text-violet-400", + }); + }); +}); diff --git a/apps/mobile/src/state/use-thread-pr.ts b/apps/mobile/src/state/use-thread-pr.ts index b7d890bbe7f..7f5a5eeb892 100644 --- a/apps/mobile/src/state/use-thread-pr.ts +++ b/apps/mobile/src/state/use-thread-pr.ts @@ -1,40 +1,14 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import type { VcsStatusResult } from "@t3tools/contracts"; -import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl"; import { useEnvironmentQuery } from "./query"; +import { presentThreadPr, type ThreadPrPresentation } from "./thread-pr-presentation"; import { vcsEnvironment } from "./vcs"; -export type ThreadPr = NonNullable; - -export interface ThreadPrPresentation { - readonly number: number; - readonly state: ThreadPr["state"]; - readonly url: string; - /** Compact chip label, e.g. "PR open" / "MR merged". */ - readonly label: string; - readonly textClassName: string; -} - -const PR_STATE_TEXT_CLASS: Record = { - open: "text-emerald-600 dark:text-emerald-400", - merged: "text-violet-600 dark:text-violet-400", - closed: "text-zinc-500 dark:text-zinc-400", -}; - -export function presentThreadPr( - pr: ThreadPr, - provider: VcsStatusResult["sourceControlProvider"] | null | undefined, -): ThreadPrPresentation { - const shortName = resolveChangeRequestPresentation(provider).shortName; - return { - number: pr.number, - state: pr.state, - url: pr.url, - label: `${shortName} ${pr.state}`, - textClassName: PR_STATE_TEXT_CLASS[pr.state], - }; -} +export { + presentThreadPr, + type ThreadPr, + type ThreadPrPresentation, +} from "./thread-pr-presentation"; /** * Live PR status for a thread's branch. Subscriptions are deduplicated per @@ -63,5 +37,5 @@ export function useThreadPr( if (!status.pr) { return null; } - return presentThreadPr(status.pr, status.sourceControlProvider); + return presentThreadPr(status.pr); } From 7ecb46e726d01bffebf21bf6b349fdfc282a54ca Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 15:25:37 +0200 Subject: [PATCH 2/4] Use provider-aware PR labels on mobile - Add accessible change request labels that say merge request for GitLab - Keep compact thread list labels as PR numbers --- .../src/features/threads/thread-list-items.tsx | 2 +- apps/mobile/src/state/thread-pr-presentation.ts | 10 +++++++++- apps/mobile/src/state/use-thread-pr.test.ts | 16 +++++++++++++++- apps/mobile/src/state/use-thread-pr.ts | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index f6eca32f659..87138855956 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -517,7 +517,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ) : null} {pr !== null ? ( ; @@ -8,6 +9,8 @@ export interface ThreadPrPresentation { readonly url: string; /** Compact desktop-style label, e.g. "#3774". */ readonly label: string; + /** Full, provider-aware label for assistive technologies. */ + readonly accessibilityLabel: string; readonly textClassName: string; } @@ -17,12 +20,17 @@ const PR_STATE_TEXT_CLASS: Record = { closed: "text-zinc-500 dark:text-zinc-400", }; -export function presentThreadPr(pr: ThreadPr): ThreadPrPresentation { +export function presentThreadPr( + pr: ThreadPr, + provider: VcsStatusResult["sourceControlProvider"] | null | undefined, +): ThreadPrPresentation { + const presentation = resolveChangeRequestPresentation(provider); return { number: pr.number, state: pr.state, url: pr.url, label: `#${pr.number}`, + accessibilityLabel: `#${pr.number} ${presentation.longName} ${pr.state}`, textClassName: PR_STATE_TEXT_CLASS[pr.state], }; } diff --git a/apps/mobile/src/state/use-thread-pr.test.ts b/apps/mobile/src/state/use-thread-pr.test.ts index 4619e05649b..21d388bb407 100644 --- a/apps/mobile/src/state/use-thread-pr.test.ts +++ b/apps/mobile/src/state/use-thread-pr.test.ts @@ -14,9 +14,23 @@ const pullRequest: NonNullable = { describe("presentThreadPr", () => { it("uses the compact desktop-style pull request number label", () => { - expect(presentThreadPr(pullRequest)).toMatchObject({ + expect(presentThreadPr(pullRequest, undefined)).toMatchObject({ label: "#3774", + accessibilityLabel: "#3774 pull request merged", textClassName: "text-violet-600 dark:text-violet-400", }); }); + + it("uses merge-request terminology for GitLab", () => { + expect( + presentThreadPr(pullRequest, { + kind: "gitlab", + name: "GitLab", + baseUrl: "https://gitlab.com", + }), + ).toMatchObject({ + label: "#3774", + accessibilityLabel: "#3774 merge request merged", + }); + }); }); diff --git a/apps/mobile/src/state/use-thread-pr.ts b/apps/mobile/src/state/use-thread-pr.ts index 7f5a5eeb892..a3440cd4848 100644 --- a/apps/mobile/src/state/use-thread-pr.ts +++ b/apps/mobile/src/state/use-thread-pr.ts @@ -37,5 +37,5 @@ export function useThreadPr( if (!status.pr) { return null; } - return presentThreadPr(status.pr); + return presentThreadPr(status.pr, status.sourceControlProvider); } From 519bb79aef07dcf9714dd725f3fd684668a26991 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 9 Jul 2026 15:36:48 +0200 Subject: [PATCH 3/4] Expose PR number indicator to accessibility --- apps/mobile/src/features/threads/thread-list-items.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index 87138855956..b100369a8b2 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -517,7 +517,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ) : null} {pr !== null ? ( Date: Thu, 9 Jul 2026 15:44:03 +0200 Subject: [PATCH 4/4] fix(mobile): expose PR details in thread row label Co-authored-by: codex --- .../src/features/threads/thread-list-items.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index 3dd155c8a4a..b08f9e3cc12 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -450,6 +450,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { const timestamp = relativeTime( thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, ); + const threadAccessibilityLabel = pr ? `${thread.title}, ${pr.accessibilityLabel}` : thread.title; const subtitleParts = [props.environmentLabel, thread.branch].filter((part): part is string => Boolean(part), ); @@ -507,12 +508,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ) : null} {pr !== null ? ( - + { @@ -579,7 +575,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { ) : ( setHovered(true)}