+
{expanded ? (
- Open diff
+ Open diff
Open the full diff
From f131228a5908974c90d9b80a4d8ef866bb29e973 Mon Sep 17 00:00:00 2001
From: Wout Stiens <71498452+StiensWout@users.noreply.github.com>
Date: Wed, 12 Aug 2026 16:06:04 +0200
Subject: [PATCH 14/17] fix(web): theme Clerk surfaces (#6300)
---
.../components/clerk/clerkAppearance.test.ts | 91 +++++++++++++++++++
.../src/components/clerk/clerkAppearance.ts | 34 +++++++
apps/web/src/main.tsx | 9 +-
3 files changed, 132 insertions(+), 2 deletions(-)
create mode 100644 apps/web/src/components/clerk/clerkAppearance.test.ts
create mode 100644 apps/web/src/components/clerk/clerkAppearance.ts
diff --git a/apps/web/src/components/clerk/clerkAppearance.test.ts b/apps/web/src/components/clerk/clerkAppearance.test.ts
new file mode 100644
index 000000000000..2ffbd9d6089d
--- /dev/null
+++ b/apps/web/src/components/clerk/clerkAppearance.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it } from "vite-plus/test";
+
+import {
+ EMBER_THEME,
+ GROVE_THEME,
+ IRIS_THEME,
+ OCEAN_THEME,
+ T3_CHAT_THEME,
+ themeColorToHex,
+ type ThemeColors,
+} from "../../themePalette";
+import { clerkAppearance } from "./clerkAppearance";
+
+function contrastRatio(first: string, second: string): number {
+ const toRgb = (value: string) => {
+ const hex = themeColorToHex(value)?.slice(1, 7);
+ if (!hex) throw new Error(`Expected a theme color, received ${value}`);
+ return [0, 1, 2].map(
+ (channel) => Number.parseInt(hex.slice(channel * 2, channel * 2 + 2), 16) / 255,
+ );
+ };
+ const luminance = (value: string) =>
+ toRgb(value)
+ .map((channel) => (channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4))
+ .reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index]!, 0);
+ const lighter = Math.max(luminance(first), luminance(second));
+ const darker = Math.min(luminance(first), luminance(second));
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+function mixThemeColors(first: string, second: string, firstWeight: number): string {
+ const channels = [first, second].map((value) => {
+ const hex = themeColorToHex(value)?.slice(1, 7);
+ if (!hex) throw new Error(`Expected a theme color, received ${value}`);
+ return [0, 1, 2].map((channel) => Number.parseInt(hex.slice(channel * 2, channel * 2 + 2), 16));
+ });
+ const mixed = channels[0]!.map((channel, index) =>
+ Math.round(channel * firstWeight + channels[1]![index]! * (1 - firstWeight)),
+ );
+ return `#${mixed.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
+}
+
+const builtInThemeModes = [
+ T3_CHAT_THEME,
+ GROVE_THEME,
+ OCEAN_THEME,
+ EMBER_THEME,
+ IRIS_THEME,
+].flatMap((theme) =>
+ [theme.colors, theme.variants?.dark].filter((colors): colors is ThemeColors => !!colors),
+);
+
+describe("clerkAppearance", () => {
+ it("maps theme colors without overriding Clerk's component structure", () => {
+ expect(clerkAppearance).toEqual({
+ variables: {
+ colorPrimary: "var(--update-foreground)",
+ colorPrimaryForeground: "var(--card)",
+ colorDanger: "var(--error)",
+ colorSuccess: "var(--success)",
+ colorWarning: "var(--warning)",
+ colorNeutral: "var(--foreground)",
+ colorForeground: "var(--foreground)",
+ colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))",
+ colorMutedForeground: "var(--muted-foreground)",
+ colorBackground: "var(--card)",
+ colorInputForeground: "var(--foreground)",
+ colorInput: "var(--secondary)",
+ colorRing: "var(--ring)",
+ },
+ elements: {
+ formFieldErrorText: { color: "var(--error-foreground)" },
+ formFieldWarningText: { color: "var(--warning-foreground)" },
+ formFieldSuccessText: { color: "var(--success-foreground)" },
+ otpCodeFieldErrorText: { color: "var(--error-foreground)" },
+ otpCodeFieldSuccessText: { color: "var(--success-foreground)" },
+ },
+ });
+ });
+
+ it.each(builtInThemeModes)("keeps Clerk text readable across a built-in palette", (colors) => {
+ const mutedSurface = mixThemeColors(colors.surface, colors.text, 0.98);
+
+ expect(contrastRatio(colors.text, colors.surface)).toBeGreaterThanOrEqual(4.5);
+ expect(contrastRatio(colors.mutedForeground, mutedSurface)).toBeGreaterThanOrEqual(4.5);
+ expect(contrastRatio(colors.text, colors.secondary)).toBeGreaterThanOrEqual(4.5);
+ expect(contrastRatio(colors.updateForeground, colors.surface)).toBeGreaterThanOrEqual(4.5);
+ expect(contrastRatio(colors.errorForeground, colors.surface)).toBeGreaterThanOrEqual(4.5);
+ expect(contrastRatio(colors.warningForeground, colors.surface)).toBeGreaterThanOrEqual(4.5);
+ });
+});
diff --git a/apps/web/src/components/clerk/clerkAppearance.ts b/apps/web/src/components/clerk/clerkAppearance.ts
new file mode 100644
index 000000000000..307e3f517bcd
--- /dev/null
+++ b/apps/web/src/components/clerk/clerkAppearance.ts
@@ -0,0 +1,34 @@
+import type { ClerkProviderProps } from "@clerk/react";
+
+/** Keeps Clerk's stock component structure while binding its color system to
+ * the live T3 Code palette. CSS variables make theme changes propagate to
+ * portaled sign-in and profile surfaces without remounting Clerk. */
+export const clerkAppearance = {
+ variables: {
+ // Clerk reuses its primary color for filled buttons and bare links. The
+ // app's update foreground is the palette's action hue cast for readable
+ // text, while the card surface provides the inverse filled-control pair.
+ colorPrimary: "var(--update-foreground)",
+ colorPrimaryForeground: "var(--card)",
+ colorDanger: "var(--error)",
+ colorSuccess: "var(--success)",
+ colorWarning: "var(--warning)",
+ colorNeutral: "var(--foreground)",
+ colorForeground: "var(--foreground)",
+ // The stock dark theme's muted token is translucent. Clerk uses this as
+ // the footer's background, so derive an opaque muted surface from the card.
+ colorMuted: "color-mix(in srgb, var(--card) 98%, var(--foreground))",
+ colorMutedForeground: "var(--muted-foreground)",
+ colorBackground: "var(--card)",
+ colorInputForeground: "var(--foreground)",
+ colorInput: "var(--secondary)",
+ colorRing: "var(--ring)",
+ },
+ elements: {
+ formFieldErrorText: { color: "var(--error-foreground)" },
+ formFieldWarningText: { color: "var(--warning-foreground)" },
+ formFieldSuccessText: { color: "var(--success-foreground)" },
+ otpCodeFieldErrorText: { color: "var(--error-foreground)" },
+ otpCodeFieldSuccessText: { color: "var(--success-foreground)" },
+ },
+} satisfies NonNullable;
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index c8f6f4de97b9..3cdc8188b757 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -16,6 +16,7 @@ import {
syncDocumentWindowControlsOverlayClass,
} from "./lib/windowControlsOverlay";
import { AppRoot } from "./AppRoot";
+import { clerkAppearance } from "./components/clerk/clerkAppearance";
// Electron loads the app from a file-backed shell, so hash history avoids path resolution issues.
const history = isElectron ? createHashHistory() : createBrowserHistory();
@@ -35,11 +36,15 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
{clerkPublishableKey && hasCloudPublicConfig() ? (
isElectron ? (
-
+
{app}
) : (
-
+
{app}
)
From b73232bdd31e83914a8a943960c7dc4b6390b39b Mon Sep 17 00:00:00 2001
From: Exotic <118054752+extoci@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:07:39 +0300
Subject: [PATCH 15/17] feat(web): reset sidebar width on double click (#6320)
---
apps/web/src/components/AppSidebarLayout.tsx | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx
index cd6f2c67e1a2..5138e84d2f15 100644
--- a/apps/web/src/components/AppSidebarLayout.tsx
+++ b/apps/web/src/components/AppSidebarLayout.tsx
@@ -10,7 +10,7 @@ import {
import { useLocation, useNavigate } from "@tanstack/react-router";
import { isElectron } from "../env";
-import { getLocalStorageItem } from "../hooks/useLocalStorage";
+import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalStorage";
import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
import { cn, isMacPlatform } from "../lib/utils";
import { primaryServerKeybindingsAtom } from "../state/server";
@@ -147,6 +147,14 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
// that would otherwise refresh a render-time snapshot.
const viewportWidth = useSyncExternalStore(subscribeToViewportWidth, readViewportWidth);
const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(viewportWidth);
+ const resetSidebarWidth = () => {
+ try {
+ removeLocalStorageItem(THREAD_SIDEBAR_WIDTH_STORAGE_KEY);
+ } catch (error) {
+ console.error("Could not clear persisted thread sidebar width.", error);
+ }
+ setSidebarWidth(resolveInitialThreadSidebarWidth(null, viewportWidth));
+ };
const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => {
const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState;
return isMacosDesktop && typeof getWindowFullscreenState === "function"
@@ -225,7 +233,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
) : (
)}
-
+
{children}
From f8948145b10306e35eea4ec3e9abd3319b36646c Mon Sep 17 00:00:00 2001
From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 16:55:50 +0000
Subject: [PATCH 16/17] fix(mobile): restore the dependencies the union dropped
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Unioning conflict hunks kept both sides' code but not both sides' dependency
arrays. Two came out short:
renderV2Item passes snoozePresetMinute={nowMinute} and upstream listed
nowMinute as a dependency; without it a screen left mounted across a minute
boundary keeps handing rows a stale minute, so snooze presets drift from the
times they claim.
The v2 layout memo reads props.threadGrouping for its recency headers but did
not depend on it, so switching grouping could keep recency headers until some
other input changed identity.
Also fixes CONTRIBUTING's link to .github/VOUCHED.td. The fork deleted that
file, and I claimed in the merge that nothing referenced it — the link is right
there in the contributing guide, so a contributor following the trust-status
docs lands on a 404.
Found by the gpt-5.6-sol adversarial review.
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
---
CONTRIBUTING.md | 2 +-
apps/mobile/src/features/home/HomeScreen.tsx | 10 +++++++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8b734a99bbb0..bfce586a5a11 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ If that sounds annoying, that is because it is. This project is still early and
PRs are automatically labeled with a `vouch:*` trust status and a `size:*` diff size based on changed lines.
-If you are an external contributor, expect `vouch:unvouched` until we explicitly add you to [.github/VOUCHED.td](.github/VOUCHED.td).
+If you are an external contributor, expect `vouch:unvouched` until a maintainer vouches for you. This fork does not carry upstream's `.github/VOUCHED.td` roster.
## What We Are Most Likely To Accept
diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx
index c4a5c89c23d5..7aa2f7cbbd05 100644
--- a/apps/mobile/src/features/home/HomeScreen.tsx
+++ b/apps/mobile/src/features/home/HomeScreen.tsx
@@ -958,7 +958,14 @@ export function HomeScreen(props: HomeScreenProps) {
snoozeLabelNow: `${nowMinute}:00.000Z`,
groupByRecency: props.threadGrouping === "recency",
}),
- [nowMinute, settledShelfExpanded, snoozedShelfExpanded, threadListV2Layout, v2PendingTasks],
+ [
+ nowMinute,
+ props.threadGrouping,
+ settledShelfExpanded,
+ snoozedShelfExpanded,
+ threadListV2Layout,
+ v2PendingTasks,
+ ],
);
const renderV2Item = useCallback(
@@ -1083,6 +1090,7 @@ export function HomeScreen(props: HomeScreenProps) {
handleMovePinnedThread,
handlePinThread,
handleRegenerateThreadTitle,
+ nowMinute,
handleSettleThread,
handleSnoozeThread,
handleUnpinThread,
From 3d263e1dc0d3a29a193e8e14c9e9ba8805c63124 Mon Sep 17 00:00:00 2001
From: "omegent-app[bot]" <306514130+omegent-app[bot]@users.noreply.github.com>
Date: Wed, 12 Aug 2026 16:57:32 +0000
Subject: [PATCH 17/17] fix(mobile): depend on the snooze capability the row
reads
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
renderV2Item passes snoozeSupported={snoozeEnvironmentIds.has(...)} but did not
depend on that set, so a row could keep offering — or withholding — snooze after
the environment's capability resolved.
Found by the grok-4.5 adversarial review, alongside the nowMinute drop.
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
---
apps/mobile/src/features/home/HomeScreen.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx
index 7aa2f7cbbd05..0ac81cfd9819 100644
--- a/apps/mobile/src/features/home/HomeScreen.tsx
+++ b/apps/mobile/src/features/home/HomeScreen.tsx
@@ -1100,6 +1100,7 @@ export function HomeScreen(props: HomeScreenProps) {
handleUnsettleThread,
pinningEnvironmentIds,
pinReorderEnvironmentIds,
+ snoozeEnvironmentIds,
projectByKey,
projectCwdByKey,
props.onArchiveThread,