From 932ce67ab1e797228c774f407c4bdfea8c994332 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Jul 2026 15:00:26 +0400 Subject: [PATCH 1/4] feat(web): add Maria's header artwork toggle Add a client-persisted Beta setting for showing or hiding the Dev and Nightly sidebar header artwork. Keep the option hidden on environments without stage artwork while leaving it available after opt-out so users can turn the artwork back on. Preserve the existing enabled-by-default behavior for current users and keep the sidebar toggle styling in sync when the artwork is disabled. --- .../settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/AppSidebarLayout.tsx | 4 ++++ .../components/SidebarStageBackdrop.test.tsx | 13 +++++++++++- .../src/components/SidebarStageBackdrop.tsx | 2 ++ .../components/settings/BetaSettingsPanel.tsx | 20 +++++++++++++++++++ .../src/components/sidebar/SidebarChrome.tsx | 9 ++++++++- packages/contracts/src/settings.test.ts | 9 +++++++++ packages/contracts/src/settings.ts | 2 ++ 8 files changed, 58 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 8f50aa8f882..fe941183399 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -27,6 +27,7 @@ const clientSettings: ClientSettings = { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", + sidebarStageArtworkEnabled: true, sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, sidebarV2Enabled: false, diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 3a70390d0c4..277f7dcfb01 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -48,6 +48,9 @@ function SidebarControl() { const { toggleSidebar } = useSidebar(); const isSidebarVisible = useSidebarVisibility(); const stageBackdropVariant = useSidebarStageBackdropVariant(); + const sidebarStageArtworkEnabled = useClientSettings( + (settings) => settings.sidebarStageArtworkEnabled, + ); const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); useEffect(() => { @@ -83,6 +86,7 @@ function SidebarControl() { className={cn( "pointer-events-auto", isSidebarVisible && + sidebarStageArtworkEnabled && stageBackdropVariant && "[:hover,[data-pressed]]:bg-white/15 focus-visible:ring-white/90 focus-visible:ring-offset-blue-700 [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white!", )} diff --git a/apps/web/src/components/SidebarStageBackdrop.test.tsx b/apps/web/src/components/SidebarStageBackdrop.test.tsx index 41aee21e327..22666dc21c6 100644 --- a/apps/web/src/components/SidebarStageBackdrop.test.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.test.tsx @@ -1,9 +1,20 @@ import { describe, expect, it } from "vite-plus/test"; import { renderToStaticMarkup } from "react-dom/server"; -import { StageBackdropArt, StageBackdropButtonArt } from "./SidebarStageBackdrop"; +import { + resolveSidebarStageBackdropVariant, + StageBackdropArt, + StageBackdropButtonArt, +} from "./SidebarStageBackdrop"; describe("SidebarStageBackdrop", () => { + it("resolves stage artwork only when enabled", () => { + expect(resolveSidebarStageBackdropVariant("Dev")).toBe("dev"); + expect(resolveSidebarStageBackdropVariant("Nightly")).toBe("nightly"); + expect(resolveSidebarStageBackdropVariant("Dev", false)).toBeNull(); + expect(resolveSidebarStageBackdropVariant("Alpha")).toBeNull(); + }); + it.each(["nightly", "dev"] as const)( "uses unique SVG definition ids when %s artwork is rendered more than once", (variant) => { diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx index ba3de64de15..6dd14684db3 100644 --- a/apps/web/src/components/SidebarStageBackdrop.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -13,7 +13,9 @@ const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; export function resolveSidebarStageBackdropVariant( stageLabel: string, + enabled = true, ): SidebarStageBackdropVariant | null { + if (!enabled) return null; const normalized = stageLabel.trim().toLowerCase(); if (normalized === "nightly") return "nightly"; if (normalized === "dev") return "dev"; diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index 942bd374d91..db218969329 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; +import { useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; import { Input } from "../ui/input"; import { Switch } from "../ui/switch"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; @@ -51,6 +52,10 @@ function AutoSettleDaysInput({ } export function BetaSettingsPanel() { + const sidebarStageBackdropVariant = useSidebarStageBackdropVariant(); + const sidebarStageArtworkEnabled = useClientSettings( + (settings) => settings.sidebarStageArtworkEnabled, + ); const sidebarV2Enabled = useClientSettings((settings) => settings.sidebarV2Enabled); const sidebarAutoSettleAfterDays = useClientSettings( (settings) => settings.sidebarAutoSettleAfterDays, @@ -60,6 +65,21 @@ export function BetaSettingsPanel() { return ( + {sidebarStageBackdropVariant ? ( + + updateSettings({ sidebarStageArtworkEnabled: Boolean(checked) }) + } + aria-label="Show sidebar header artwork" + /> + } + /> + ) : null} settings.sidebarStageArtworkEnabled, + ); + const backdropVariant = resolveSidebarStageBackdropVariant( + stageLabel, + sidebarStageArtworkEnabled, + ); return ( { }); }); +describe("ClientSettings sidebar stage artwork", () => { + it("defaults the artwork on and accepts opt-out patches", () => { + expect(decodeClientSettings({}).sidebarStageArtworkEnabled).toBe(true); + expect( + decodeClientSettingsPatch({ sidebarStageArtworkEnabled: false }).sidebarStageArtworkEnabled, + ).toBe(false); + }); +}); + describe("ClientSettings sidebar v2", () => { it("defaults the beta off with a three-day auto-settle threshold", () => { const settings = decodeClientSettings({}); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 06f7de3db67..ba8d1dc04ce 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -108,6 +108,7 @@ export const ClientSettingsSchema = Schema.Struct({ sidebarProjectSortOrder: SidebarProjectSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER)), ), + sidebarStageArtworkEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), ), @@ -601,6 +602,7 @@ export const ClientSettingsPatch = Schema.Struct({ Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), ), sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder), + sidebarStageArtworkEnabled: Schema.optionalKey(Schema.Boolean), sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder), sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount), sidebarV2Enabled: Schema.optionalKey(Schema.Boolean), From fb662677a6fa0bc21983cd39a657a368f8dbda67 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 28 Jul 2026 11:41:28 +0400 Subject: [PATCH 2/4] feat(web): add environment identification modes --- .../settings/DesktopClientSettings.test.ts | 2 +- apps/web/src/components/AppSidebarLayout.tsx | 9 +-- .../components/SidebarStageBackdrop.test.tsx | 8 +++ .../src/components/SidebarStageBackdrop.tsx | 26 +++++--- .../chat/ComposerPrimaryActions.tsx | 8 ++- .../components/settings/BetaSettingsPanel.tsx | 20 ------ .../components/settings/SettingsPanels.tsx | 63 +++++++++++++++++++ .../src/components/sidebar/SidebarChrome.tsx | 44 +++++++------ packages/contracts/src/settings.test.ts | 21 +++++-- packages/contracts/src/settings.ts | 9 ++- 10 files changed, 150 insertions(+), 60 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index fe941183399..4b86f9eedfe 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -18,6 +18,7 @@ const clientSettings: ClientSettings = { confirmThreadDelete: false, dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, + environmentIdentificationMode: "artwork", favorites: [], glassOpacity: 80, providerModelPreferences: {}, @@ -27,7 +28,6 @@ const clientSettings: ClientSettings = { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", - sidebarStageArtworkEnabled: true, sidebarThreadSortOrder: "created_at", sidebarThreadPreviewCount: 6, sidebarV2Enabled: false, diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 277f7dcfb01..585cef29b2f 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -47,9 +47,11 @@ function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); const isSidebarVisible = useSidebarVisibility(); - const stageBackdropVariant = useSidebarStageBackdropVariant(); - const sidebarStageArtworkEnabled = useClientSettings( - (settings) => settings.sidebarStageArtworkEnabled, + const environmentIdentificationMode = useClientSettings( + (settings) => settings.environmentIdentificationMode, + ); + const stageBackdropVariant = useSidebarStageBackdropVariant( + environmentIdentificationMode === "artwork", ); const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); @@ -86,7 +88,6 @@ function SidebarControl() { className={cn( "pointer-events-auto", isSidebarVisible && - sidebarStageArtworkEnabled && stageBackdropVariant && "[:hover,[data-pressed]]:bg-white/15 focus-visible:ring-white/90 focus-visible:ring-offset-blue-700 [&_svg]:stroke-white/90! [&_svg]:opacity-100! [&_svg]:hover:stroke-white!", )} diff --git a/apps/web/src/components/SidebarStageBackdrop.test.tsx b/apps/web/src/components/SidebarStageBackdrop.test.tsx index 22666dc21c6..114fd5f9241 100644 --- a/apps/web/src/components/SidebarStageBackdrop.test.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { renderToStaticMarkup } from "react-dom/server"; import { + resolveEnvironmentIdentificationPillLabel, resolveSidebarStageBackdropVariant, StageBackdropArt, StageBackdropButtonArt, @@ -15,6 +16,13 @@ describe("SidebarStageBackdrop", () => { expect(resolveSidebarStageBackdropVariant("Alpha")).toBeNull(); }); + it("resolves supported environment pill labels", () => { + expect(resolveEnvironmentIdentificationPillLabel("Dev")).toBe("Dev"); + expect(resolveEnvironmentIdentificationPillLabel("nightly")).toBe("Nightly"); + expect(resolveEnvironmentIdentificationPillLabel("Latest")).toBeNull(); + expect(resolveEnvironmentIdentificationPillLabel("Alpha")).toBeNull(); + }); + it.each(["nightly", "dev"] as const)( "uses unique SVG definition ids when %s artwork is rendered more than once", (variant) => { diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx index 6dd14684db3..9fb448e940d 100644 --- a/apps/web/src/components/SidebarStageBackdrop.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -6,6 +6,7 @@ import { resolveServerBackedAppStageLabel } from "../branding.logic"; import { primaryServerConfigAtom } from "../state/server"; export type SidebarStageBackdropVariant = "nightly" | "dev"; +export type EnvironmentIdentificationPillLabel = "Dev" | "Nightly"; // A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals // more horizontal canvas instead of zooming the scene. @@ -22,16 +23,27 @@ export function resolveSidebarStageBackdropVariant( return null; } -export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null { +export function resolveEnvironmentIdentificationPillLabel( + stageLabel: string, +): EnvironmentIdentificationPillLabel | null { + const normalized = stageLabel.trim().toLowerCase(); + if (normalized === "dev") return "Dev"; + if (normalized === "nightly") return "Nightly"; + return null; +} + +export function useEnvironmentStageLabel(): string { const primaryServerVersion = useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; - return resolveSidebarStageBackdropVariant( - resolveServerBackedAppStageLabel({ - primaryServerVersion, - fallbackStageLabel: APP_STAGE_LABEL, - }), - ); + return resolveServerBackedAppStageLabel({ + primaryServerVersion, + fallbackStageLabel: APP_STAGE_LABEL, + }); +} + +export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null { + return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled); } /** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index 3e664e226b7..a7fab1fa69d 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -1,5 +1,6 @@ import { memo, type PointerEventHandler } from "react"; import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react"; +import { useClientSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; import { Button } from "../ui/button"; @@ -72,7 +73,12 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ const pointerFocusProps = preserveComposerFocusOnPointerDown ? { onPointerDown: preventPointerFocus } : undefined; - const stageBackdropVariant = useSidebarStageBackdropVariant(); + const environmentIdentificationMode = useClientSettings( + (settings) => settings.environmentIdentificationMode, + ); + const stageBackdropVariant = useSidebarStageBackdropVariant( + environmentIdentificationMode === "artwork", + ); if (pendingAction) { return ( diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index db218969329..942bd374d91 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; import { useClientSettings, useUpdateClientSettings } from "../../hooks/useSettings"; -import { useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; import { Input } from "../ui/input"; import { Switch } from "../ui/switch"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; @@ -52,10 +51,6 @@ function AutoSettleDaysInput({ } export function BetaSettingsPanel() { - const sidebarStageBackdropVariant = useSidebarStageBackdropVariant(); - const sidebarStageArtworkEnabled = useClientSettings( - (settings) => settings.sidebarStageArtworkEnabled, - ); const sidebarV2Enabled = useClientSettings((settings) => settings.sidebarV2Enabled); const sidebarAutoSettleAfterDays = useClientSettings( (settings) => settings.sidebarAutoSettleAfterDays, @@ -65,21 +60,6 @@ export function BetaSettingsPanel() { return ( - {sidebarStageBackdropVariant ? ( - - updateSettings({ sidebarStageArtworkEnabled: Boolean(checked) }) - } - aria-label="Show sidebar header artwork" - /> - } - /> - ) : null} = { + artwork: "Artwork", + pill: "Version pill", + none: "None", +}; + const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", @@ -401,6 +413,10 @@ export function useSettingsRestore(onRestored?: () => void) { () => [ ...(theme !== "system" ? ["Theme"] : []), ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), + ...(settings.environmentIdentificationMode !== + DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode + ? ["Environment identification"] + : []), ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), @@ -456,6 +472,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.defaultThreadEnvMode, settings.newWorktreesStartFromOrigin, settings.diffIgnoreWhitespace, + settings.environmentIdentificationMode, settings.glassOpacity, settings.automaticGitFetchInterval, settings.enableAssistantStreaming, @@ -483,6 +500,7 @@ export function useSettingsRestore(onRestored?: () => void) { timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, + environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, @@ -510,6 +528,9 @@ export function GeneralSettingsPanel() { const { theme, setTheme } = useTheme(); const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); + const environmentStageLabel = useEnvironmentStageLabel(); + const showEnvironmentIdentification = + resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null; const lastEnabledProjectGroupingMode = useRef( readLastEnabledProjectGroupingMode(), ); @@ -634,6 +655,48 @@ export function GeneralSettingsPanel() { } /> + {showEnvironmentIdentification ? ( + + updateSettings({ + environmentIdentificationMode: DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, + }) + } + /> + ) : null + } + control={ + + } + /> + ) : null} + settings.sidebarStageArtworkEnabled, + const stageLabel = useEnvironmentStageLabel(); + const environmentIdentificationMode = useClientSettings( + (settings) => settings.environmentIdentificationMode, ); const backdropVariant = resolveSidebarStageBackdropVariant( stageLabel, - sidebarStageArtworkEnabled, + environmentIdentificationMode === "artwork", ); + const pillLabel = + environmentIdentificationMode === "pill" + ? resolveEnvironmentIdentificationPillLabel(stageLabel) + : null; return ( + {pillLabel ? ( + + {pillLabel} + + ) : null} ); }); @@ -78,16 +94,6 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { ); } -function useSidebarStageLabel() { - const primaryServerVersion = - useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; - - return resolveSidebarStageBadgeLabel({ - primaryServerVersion, - fallbackStageLabel: APP_STAGE_LABEL, - }); -} - function T3Wordmark() { return ( { }); }); -describe("ClientSettings sidebar stage artwork", () => { - it("defaults the artwork on and accepts opt-out patches", () => { - expect(decodeClientSettings({}).sidebarStageArtworkEnabled).toBe(true); - expect( - decodeClientSettingsPatch({ sidebarStageArtworkEnabled: false }).sidebarStageArtworkEnabled, - ).toBe(false); +describe("ClientSettings environment identification", () => { + it("defaults to artwork and accepts each presentation mode", () => { + expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); + + for (const mode of ["artwork", "pill", "none"] as const) { + expect( + decodeClientSettingsPatch({ environmentIdentificationMode: mode }) + .environmentIdentificationMode, + ).toBe(mode); + } + }); + + it("rejects unsupported presentation modes", () => { + expect(() => decodeClientSettings({ environmentIdentificationMode: "badge" })).toThrow(); + expect(() => decodeClientSettingsPatch({ environmentIdentificationMode: "badge" })).toThrow(); }); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index ba8d1dc04ce..c466eb690c1 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -58,6 +58,9 @@ export const GlassOpacity = Schema.Int.check( ); export type GlassOpacity = typeof GlassOpacity.Type; export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; +export const EnvironmentIdentificationMode = Schema.Literals(["artwork", "pill", "none"]); +export type EnvironmentIdentificationMode = typeof EnvironmentIdentificationMode.Type; +export const DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE: EnvironmentIdentificationMode = "artwork"; export const ClientSettingsSchema = Schema.Struct({ autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -67,6 +70,9 @@ export const ClientSettingsSchema = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed([])), ), diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), + environmentIdentificationMode: EnvironmentIdentificationMode.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE)), + ), glassOpacity: GlassOpacity.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), ), @@ -108,7 +114,6 @@ export const ClientSettingsSchema = Schema.Struct({ sidebarProjectSortOrder: SidebarProjectSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER)), ), - sidebarStageArtworkEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), ), @@ -574,6 +579,7 @@ export const ClientSettingsPatch = Schema.Struct({ confirmThreadArchive: Schema.optionalKey(Schema.Boolean), confirmThreadDelete: Schema.optionalKey(Schema.Boolean), diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), + environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), glassOpacity: Schema.optionalKey(GlassOpacity), favorites: Schema.optionalKey( Schema.Array( @@ -602,7 +608,6 @@ export const ClientSettingsPatch = Schema.Struct({ Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), ), sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder), - sidebarStageArtworkEnabled: Schema.optionalKey(Schema.Boolean), sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder), sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount), sidebarV2Enabled: Schema.optionalKey(Schema.Boolean), From f2c0896756938091778e7ce7a5d8434762c0a1df Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 28 Jul 2026 11:54:21 +0400 Subject: [PATCH 3/4] codex: fix CI failure on PR #4652 --- apps/web/src/components/AppSidebarLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 318ed51c370..1925f06ff7f 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -14,7 +14,7 @@ import { getLocalStorageItem } from "../hooks/useLocalStorage"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; -import { useSidebarV2Enabled } from "../hooks/useSettings"; +import { useClientSettings, useSidebarV2Enabled } from "../hooks/useSettings"; import ThreadSidebar from "./Sidebar"; import ThreadSidebarV2 from "./SidebarV2"; import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; From 5874328f7ac623e01b6f705d45c79b56a93e528e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 28 Jul 2026 12:04:58 +0400 Subject: [PATCH 4/4] codex: address PR review feedback (#4652) --- apps/web/src/components/AppSidebarLayout.tsx | 6 ++---- .../components/chat/ComposerPrimaryActions.tsx | 6 ++---- apps/web/src/components/sidebar/SidebarChrome.tsx | 6 ++---- apps/web/src/hooks/useSettings.test.ts | 13 ++++++++++++- apps/web/src/hooks/useSettings.ts | 15 +++++++++++++++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 1925f06ff7f..5c6acd62aea 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -14,7 +14,7 @@ import { getLocalStorageItem } from "../hooks/useLocalStorage"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; -import { useClientSettings, useSidebarV2Enabled } from "../hooks/useSettings"; +import { useEnvironmentIdentificationMode, useSidebarV2Enabled } from "../hooks/useSettings"; import ThreadSidebar from "./Sidebar"; import ThreadSidebarV2 from "./SidebarV2"; import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; @@ -62,9 +62,7 @@ function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); const isSidebarVisible = useSidebarVisibility(); - const environmentIdentificationMode = useClientSettings( - (settings) => settings.environmentIdentificationMode, - ); + const environmentIdentificationMode = useEnvironmentIdentificationMode(); const stageBackdropVariant = useSidebarStageBackdropVariant( environmentIdentificationMode === "artwork", ); diff --git a/apps/web/src/components/chat/ComposerPrimaryActions.tsx b/apps/web/src/components/chat/ComposerPrimaryActions.tsx index a7fab1fa69d..19c20e2abf2 100644 --- a/apps/web/src/components/chat/ComposerPrimaryActions.tsx +++ b/apps/web/src/components/chat/ComposerPrimaryActions.tsx @@ -1,6 +1,6 @@ import { memo, type PointerEventHandler } from "react"; import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react"; -import { useClientSettings } from "~/hooks/useSettings"; +import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; import { Button } from "../ui/button"; @@ -73,9 +73,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ const pointerFocusProps = preserveComposerFocusOnPointerDown ? { onPointerDown: preventPointerFocus } : undefined; - const environmentIdentificationMode = useClientSettings( - (settings) => settings.environmentIdentificationMode, - ); + const environmentIdentificationMode = useEnvironmentIdentificationMode(); const stageBackdropVariant = useSidebarStageBackdropVariant( environmentIdentificationMode === "artwork", ); diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 8c235438cab..09fcd773128 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -2,7 +2,7 @@ import { SettingsIcon } from "lucide-react"; import { memo, useCallback } from "react"; import { Link, useNavigate } from "@tanstack/react-router"; -import { useClientSettings } from "../../hooks/useSettings"; +import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; import { cn } from "../../lib/utils"; import { resolveEnvironmentIdentificationPillLabel, @@ -29,9 +29,7 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ isElectron: boolean; }) { const stageLabel = useEnvironmentStageLabel(); - const environmentIdentificationMode = useClientSettings( - (settings) => settings.environmentIdentificationMode, - ); + const environmentIdentificationMode = useEnvironmentIdentificationMode(); const backdropVariant = resolveSidebarStageBackdropVariant( stageLabel, environmentIdentificationMode === "artwork", diff --git a/apps/web/src/hooks/useSettings.test.ts b/apps/web/src/hooks/useSettings.test.ts index 7132c84c9d3..741579661e7 100644 --- a/apps/web/src/hooks/useSettings.test.ts +++ b/apps/web/src/hooks/useSettings.test.ts @@ -6,7 +6,18 @@ import { import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import { describe, expect, it } from "vite-plus/test"; -import { mergeEnvironmentSettings } from "./useSettings"; +import { mergeEnvironmentSettings, resolveEnvironmentIdentificationMode } from "./useSettings"; + +describe("resolveEnvironmentIdentificationMode", () => { + it("keeps identification hidden until client settings hydrate", () => { + expect(resolveEnvironmentIdentificationMode({ mode: "artwork", settingsHydrated: false })).toBe( + "none", + ); + expect(resolveEnvironmentIdentificationMode({ mode: "pill", settingsHydrated: true })).toBe( + "pill", + ); + }); +}); describe("mergeEnvironmentSettings", () => { it("combines the selected environment's server settings with client preferences", () => { diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index 9f78e8a8527..739f6cfcea5 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -21,6 +21,7 @@ import { type ClientSettingsPatch, type ClientSettings, DEFAULT_CLIENT_SETTINGS, + type EnvironmentIdentificationMode, type UnifiedSettings, } from "@t3tools/contracts/settings"; import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; @@ -222,6 +223,20 @@ export function useClientSettings( return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); } +export function resolveEnvironmentIdentificationMode(input: { + mode: EnvironmentIdentificationMode; + settingsHydrated: boolean; +}): EnvironmentIdentificationMode { + // Avoid briefly rendering the default artwork before a persisted pill/none choice loads. + return input.settingsHydrated ? input.mode : "none"; +} + +export function useEnvironmentIdentificationMode(): EnvironmentIdentificationMode { + const settingsHydrated = useClientSettingsHydrated(); + const mode = useClientSettingsValue().environmentIdentificationMode; + return resolveEnvironmentIdentificationMode({ mode, settingsHydrated }); +} + /** * Resolved sidebar v2 state: an explicit choice in Settings → Beta if the user * has made one, otherwise the default for this build stage (on for nightly and