| author | |
| committer | |
| log | 362f12792254335bfe2a015787d7cbbaa214654e |
| tree | e1871e0fd30bf60cd335184098a130b2f1455113 |
| parent | 9cf9fc9c5c0f48d42f407f86cf83d0f04fbdf5a8 |
| signature | Signed by PGP key B5690EEEBB952194 |
11 files changed, 195 insertions(+), 29 deletions(-)
apps/desktop/src/settings/DesktopClientSettings.test.ts+1| ... | ... | @@ -18,6 +18,7 @@ const clientSettings: ClientSettings = { |
| 18 | 18 | confirmThreadDelete: false, |
| 19 | 19 | dismissedProviderUpdateNotificationKeys: [], |
| 20 | 20 | diffIgnoreWhitespace: true, |
| 21 | environmentIdentificationMode: "artwork", | |
| 21 | 22 | favorites: [], |
| 22 | 23 | glassOpacity: 80, |
| 23 | 24 | providerModelPreferences: {}, |
apps/web/src/components/AppSidebarLayout.tsx+5-2| ... | ... | @@ -14,7 +14,7 @@ import { getLocalStorageItem } from "../hooks/useLocalStorage"; |
| 14 | 14 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 15 | 15 | import { cn, isMacPlatform } from "../lib/utils"; |
| 16 | 16 | import { primaryServerKeybindingsAtom } from "../state/server"; |
| 17 | import { useSidebarV2Enabled } from "../hooks/useSettings"; | |
| 17 | import { useEnvironmentIdentificationMode, useSidebarV2Enabled } from "../hooks/useSettings"; | |
| 18 | 18 | import ThreadSidebar from "./Sidebar"; |
| 19 | 19 | import ThreadSidebarV2 from "./SidebarV2"; |
| 20 | 20 | import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; |
| ... | ... | @@ -62,7 +62,10 @@ function SidebarControl() { |
| 62 | 62 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 63 | 63 | const { toggleSidebar } = useSidebar(); |
| 64 | 64 | const isSidebarVisible = useSidebarVisibility(); |
| 65 | const stageBackdropVariant = useSidebarStageBackdropVariant(); | |
| 65 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); | |
| 66 | const stageBackdropVariant = useSidebarStageBackdropVariant( | |
| 67 | environmentIdentificationMode === "artwork", | |
| 68 | ); | |
| 66 | 69 | const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); |
| 67 | 70 | |
| 68 | 71 | useEffect(() => { |
apps/web/src/components/SidebarStageBackdrop.test.tsx+20-1| ... | ... | @@ -1,9 +1,28 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | import { renderToStaticMarkup } from "react-dom/server"; |
| 3 | 3 | |
| 4 | import { StageBackdropArt, StageBackdropButtonArt } from "./SidebarStageBackdrop"; | |
| 4 | import { | |
| 5 | resolveEnvironmentIdentificationPillLabel, | |
| 6 | resolveSidebarStageBackdropVariant, | |
| 7 | StageBackdropArt, | |
| 8 | StageBackdropButtonArt, | |
| 9 | } from "./SidebarStageBackdrop"; | |
| 5 | 10 | |
| 6 | 11 | describe("SidebarStageBackdrop", () => { |
| 12 | it("resolves stage artwork only when enabled", () => { | |
| 13 | expect(resolveSidebarStageBackdropVariant("Dev")).toBe("dev"); | |
| 14 | expect(resolveSidebarStageBackdropVariant("Nightly")).toBe("nightly"); | |
| 15 | expect(resolveSidebarStageBackdropVariant("Dev", false)).toBeNull(); | |
| 16 | expect(resolveSidebarStageBackdropVariant("Alpha")).toBeNull(); | |
| 17 | }); | |
| 18 | ||
| 19 | it("resolves supported environment pill labels", () => { | |
| 20 | expect(resolveEnvironmentIdentificationPillLabel("Dev")).toBe("Dev"); | |
| 21 | expect(resolveEnvironmentIdentificationPillLabel("nightly")).toBe("Nightly"); | |
| 22 | expect(resolveEnvironmentIdentificationPillLabel("Latest")).toBeNull(); | |
| 23 | expect(resolveEnvironmentIdentificationPillLabel("Alpha")).toBeNull(); | |
| 24 | }); | |
| 25 | ||
| 7 | 26 | it.each(["nightly", "dev"] as const)( |
| 8 | 27 | "uses unique SVG definition ids when %s artwork is rendered more than once", |
| 9 | 28 | (variant) => { |
apps/web/src/components/SidebarStageBackdrop.tsx+21-7| ... | ... | @@ -6,6 +6,7 @@ import { resolveServerBackedAppStageLabel } from "../branding.logic"; |
| 6 | 6 | import { primaryServerConfigAtom } from "../state/server"; |
| 7 | 7 | |
| 8 | 8 | export type SidebarStageBackdropVariant = "nightly" | "dev"; |
| 9 | export type EnvironmentIdentificationPillLabel = "Dev" | "Nightly"; | |
| 9 | 10 | |
| 10 | 11 | // A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals |
| 11 | 12 | // more horizontal canvas instead of zooming the scene. |
| ... | ... | @@ -13,23 +14,36 @@ const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; |
| 13 | 14 | |
| 14 | 15 | export function resolveSidebarStageBackdropVariant( |
| 15 | 16 | stageLabel: string, |
| 17 | enabled = true, | |
| 16 | 18 | ): SidebarStageBackdropVariant | null { |
| 19 | if (!enabled) return null; | |
| 17 | 20 | const normalized = stageLabel.trim().toLowerCase(); |
| 18 | 21 | if (normalized === "nightly") return "nightly"; |
| 19 | 22 | if (normalized === "dev") return "dev"; |
| 20 | 23 | return null; |
| 21 | 24 | } |
| 22 | 25 | |
| 23 | export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null { | |
| 26 | export function resolveEnvironmentIdentificationPillLabel( | |
| 27 | stageLabel: string, | |
| 28 | ): EnvironmentIdentificationPillLabel | null { | |
| 29 | const normalized = stageLabel.trim().toLowerCase(); | |
| 30 | if (normalized === "dev") return "Dev"; | |
| 31 | if (normalized === "nightly") return "Nightly"; | |
| 32 | return null; | |
| 33 | } | |
| 34 | ||
| 35 | export function useEnvironmentStageLabel(): string { | |
| 24 | 36 | const primaryServerVersion = |
| 25 | 37 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; |
| 26 | 38 | |
| 27 | return resolveSidebarStageBackdropVariant( | |
| 28 | resolveServerBackedAppStageLabel({ | |
| 29 | primaryServerVersion, | |
| 30 | fallbackStageLabel: APP_STAGE_LABEL, | |
| 31 | }), | |
| 32 | ); | |
| 39 | return resolveServerBackedAppStageLabel({ | |
| 40 | primaryServerVersion, | |
| 41 | fallbackStageLabel: APP_STAGE_LABEL, | |
| 42 | }); | |
| 43 | } | |
| 44 | ||
| 45 | export function useSidebarStageBackdropVariant(enabled = true): SidebarStageBackdropVariant | null { | |
| 46 | return resolveSidebarStageBackdropVariant(useEnvironmentStageLabel(), enabled); | |
| 33 | 47 | } |
| 34 | 48 | |
| 35 | 49 | /** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ |
apps/web/src/components/chat/ComposerPrimaryActions.tsx+5-1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import { memo, type PointerEventHandler } from "react"; |
| 2 | 2 | import { ChevronDownIcon, ChevronLeftIcon } from "lucide-react"; |
| 3 | import { useEnvironmentIdentificationMode } from "~/hooks/useSettings"; | |
| 3 | 4 | import { cn } from "~/lib/utils"; |
| 4 | 5 | import { StageBackdropButtonArt, useSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; |
| 5 | 6 | import { Button } from "../ui/button"; |
| ... | ... | @@ -72,7 +73,10 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({ |
| 72 | 73 | const pointerFocusProps = preserveComposerFocusOnPointerDown |
| 73 | 74 | ? { onPointerDown: preventPointerFocus } |
| 74 | 75 | : undefined; |
| 75 | const stageBackdropVariant = useSidebarStageBackdropVariant(); | |
| 76 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); | |
| 77 | const stageBackdropVariant = useSidebarStageBackdropVariant( | |
| 78 | environmentIdentificationMode === "artwork", | |
| 79 | ); | |
| 76 | 80 | |
| 77 | 81 | if (pendingAction) { |
| 78 | 82 | return ( |
apps/web/src/components/settings/SettingsPanels.tsx+63| ... | ... | @@ -21,7 +21,9 @@ import { |
| 21 | 21 | squashAtomCommandFailure, |
| 22 | 22 | } from "@t3tools/client-runtime/state/runtime"; |
| 23 | 23 | import { |
| 24 | DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, | |
| 24 | 25 | DEFAULT_UNIFIED_SETTINGS, |
| 26 | type EnvironmentIdentificationMode, | |
| 25 | 27 | MAX_GLASS_OPACITY, |
| 26 | 28 | MIN_GLASS_OPACITY, |
| 27 | 29 | } from "@t3tools/contracts/settings"; |
| ... | ... | @@ -40,6 +42,10 @@ import { |
| 40 | 42 | } from "../../components/desktopUpdate.logic"; |
| 41 | 43 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; |
| 42 | 44 | import { TraitsPicker } from "../chat/TraitsPicker"; |
| 45 | import { | |
| 46 | resolveEnvironmentIdentificationPillLabel, | |
| 47 | useEnvironmentStageLabel, | |
| 48 | } from "../SidebarStageBackdrop"; | |
| 43 | 49 | import { isElectron } from "../../env"; |
| 44 | 50 | import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing"; |
| 45 | 51 | import { useTheme } from "../../hooks/useTheme"; |
| ... | ... | @@ -114,6 +120,12 @@ const THEME_OPTIONS = [ |
| 114 | 120 | }, |
| 115 | 121 | ] as const; |
| 116 | 122 | |
| 123 | const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { | |
| 124 | artwork: "Artwork", | |
| 125 | pill: "Version pill", | |
| 126 | none: "None", | |
| 127 | }; | |
| 128 | ||
| 117 | 129 | const TIMESTAMP_FORMAT_LABELS = { |
| 118 | 130 | locale: "System default", |
| 119 | 131 | "12-hour": "12-hour", |
| ... | ... | @@ -401,6 +413,10 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 401 | 413 | () => [ |
| 402 | 414 | ...(theme !== "system" ? ["Theme"] : []), |
| 403 | 415 | ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), |
| 416 | ...(settings.environmentIdentificationMode !== | |
| 417 | DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode | |
| 418 | ? ["Environment identification"] | |
| 419 | : []), | |
| 404 | 420 | ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat |
| 405 | 421 | ? ["Time format"] |
| 406 | 422 | : []), |
| ... | ... | @@ -456,6 +472,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 456 | 472 | settings.defaultThreadEnvMode, |
| 457 | 473 | settings.newWorktreesStartFromOrigin, |
| 458 | 474 | settings.diffIgnoreWhitespace, |
| 475 | settings.environmentIdentificationMode, | |
| 459 | 476 | settings.glassOpacity, |
| 460 | 477 | settings.automaticGitFetchInterval, |
| 461 | 478 | settings.enableAssistantStreaming, |
| ... | ... | @@ -483,6 +500,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 483 | 500 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 484 | 501 | wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, |
| 485 | 502 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, |
| 503 | environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, | |
| 486 | 504 | glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, |
| 487 | 505 | sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, |
| 488 | 506 | sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, |
| ... | ... | @@ -510,6 +528,9 @@ export function GeneralSettingsPanel() { |
| 510 | 528 | const { theme, setTheme } = useTheme(); |
| 511 | 529 | const settings = usePrimarySettings(); |
| 512 | 530 | const updateSettings = useUpdatePrimarySettings(); |
| 531 | const environmentStageLabel = useEnvironmentStageLabel(); | |
| 532 | const showEnvironmentIdentification = | |
| 533 | resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null; | |
| 513 | 534 | const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>( |
| 514 | 535 | readLastEnabledProjectGroupingMode(), |
| 515 | 536 | ); |
| ... | ... | @@ -634,6 +655,48 @@ export function GeneralSettingsPanel() { |
| 634 | 655 | } |
| 635 | 656 | /> |
| 636 | 657 | |
| 658 | {showEnvironmentIdentification ? ( | |
| 659 | <SettingsRow | |
| 660 | title="Environment identification" | |
| 661 | description="Choose how Dev and Nightly environments are identified." | |
| 662 | resetAction={ | |
| 663 | settings.environmentIdentificationMode !== DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE ? ( | |
| 664 | <SettingResetButton | |
| 665 | label="environment identification" | |
| 666 | onClick={() => | |
| 667 | updateSettings({ | |
| 668 | environmentIdentificationMode: DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE, | |
| 669 | }) | |
| 670 | } | |
| 671 | /> | |
| 672 | ) : null | |
| 673 | } | |
| 674 | control={ | |
| 675 | <Select | |
| 676 | value={settings.environmentIdentificationMode} | |
| 677 | onValueChange={(value) => { | |
| 678 | if (value === "artwork" || value === "pill" || value === "none") { | |
| 679 | updateSettings({ environmentIdentificationMode: value }); | |
| 680 | } | |
| 681 | }} | |
| 682 | > | |
| 683 | <SelectTrigger className="w-full sm:w-40" aria-label="Environment identification"> | |
| 684 | <SelectValue> | |
| 685 | {ENVIRONMENT_IDENTIFICATION_LABELS[settings.environmentIdentificationMode]} | |
| 686 | </SelectValue> | |
| 687 | </SelectTrigger> | |
| 688 | <SelectPopup align="end" alignItemWithTrigger={false}> | |
| 689 | {Object.entries(ENVIRONMENT_IDENTIFICATION_LABELS).map(([value, label]) => ( | |
| 690 | <SelectItem hideIndicator key={value} value={value}> | |
| 691 | {label} | |
| 692 | </SelectItem> | |
| 693 | ))} | |
| 694 | </SelectPopup> | |
| 695 | </Select> | |
| 696 | } | |
| 697 | /> | |
| 698 | ) : null} | |
| 699 | ||
| 637 | 700 | <SettingsRow |
| 638 | 701 | title="Project Grouping" |
| 639 | 702 | description="Combine matching repositories across environments." |
apps/web/src/components/sidebar/SidebarChrome.tsx+28-17| ... | ... | @@ -1,13 +1,16 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | |
| 2 | 1 | import { SettingsIcon } from "lucide-react"; |
| 3 | 2 | import { memo, useCallback } from "react"; |
| 4 | 3 | import { Link, useNavigate } from "@tanstack/react-router"; |
| 5 | 4 | |
| 6 | import { APP_STAGE_LABEL } from "../../branding"; | |
| 5 | import { useEnvironmentIdentificationMode } from "../../hooks/useSettings"; | |
| 7 | 6 | import { cn } from "../../lib/utils"; |
| 8 | import { primaryServerConfigAtom } from "../../state/server"; | |
| 9 | import { resolveSidebarStageBadgeLabel } from "../Sidebar.logic"; | |
| 10 | import { SidebarStageBackdrop, resolveSidebarStageBackdropVariant } from "../SidebarStageBackdrop"; | |
| 7 | import { | |
| 8 | resolveEnvironmentIdentificationPillLabel, | |
| 9 | resolveSidebarStageBackdropVariant, | |
| 10 | SidebarStageBackdrop, | |
| 11 | useEnvironmentStageLabel, | |
| 12 | } from "../SidebarStageBackdrop"; | |
| 13 | import { Badge } from "../ui/badge"; | |
| 11 | 14 | import { |
| 12 | 15 | SidebarFooter, |
| 13 | 16 | SidebarHeader, |
| ... | ... | @@ -25,8 +28,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 25 | 28 | }: { |
| 26 | 29 | isElectron: boolean; |
| 27 | 30 | }) { |
| 28 | const stageLabel = useSidebarStageLabel(); | |
| 29 | const backdropVariant = resolveSidebarStageBackdropVariant(stageLabel); | |
| 31 | const stageLabel = useEnvironmentStageLabel(); | |
| 32 | const environmentIdentificationMode = useEnvironmentIdentificationMode(); | |
| 33 | const backdropVariant = resolveSidebarStageBackdropVariant( | |
| 34 | stageLabel, | |
| 35 | environmentIdentificationMode === "artwork", | |
| 36 | ); | |
| 37 | const pillLabel = | |
| 38 | environmentIdentificationMode === "pill" | |
| 39 | ? resolveEnvironmentIdentificationPillLabel(stageLabel) | |
| 40 | : null; | |
| 30 | 41 | |
| 31 | 42 | return ( |
| 32 | 43 | <SidebarHeader |
| ... | ... | @@ -44,6 +55,16 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ |
| 44 | 55 | )} |
| 45 | 56 | /> |
| 46 | 57 | <SidebarBrand onBackdrop={backdropVariant !== null} /> |
| 58 | {pillLabel ? ( | |
| 59 | <Badge | |
| 60 | className="relative z-10 ml-1 rounded-full px-1.5 text-muted-foreground" | |
| 61 | data-environment-identification="pill" | |
| 62 | size="sm" | |
| 63 | variant="secondary" | |
| 64 | > | |
| 65 | {pillLabel} | |
| 66 | </Badge> | |
| 67 | ) : null} | |
| 47 | 68 | </SidebarHeader> |
| 48 | 69 | ); |
| 49 | 70 | }); |
| ... | ... | @@ -71,16 +92,6 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { |
| 71 | 92 | ); |
| 72 | 93 | } |
| 73 | 94 | |
| 74 | function useSidebarStageLabel() { | |
| 75 | const primaryServerVersion = | |
| 76 | useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; | |
| 77 | ||
| 78 | return resolveSidebarStageBadgeLabel({ | |
| 79 | primaryServerVersion, | |
| 80 | fallbackStageLabel: APP_STAGE_LABEL, | |
| 81 | }); | |
| 82 | } | |
| 83 | ||
| 84 | 95 | function T3Wordmark() { |
| 85 | 96 | return ( |
| 86 | 97 | <svg |
apps/web/src/hooks/useSettings.test.ts+12-1| ... | ... | @@ -6,7 +6,18 @@ import { |
| 6 | 6 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; |
| 7 | 7 | import { describe, expect, it } from "vite-plus/test"; |
| 8 | 8 | |
| 9 | import { mergeEnvironmentSettings } from "./useSettings"; | |
| 9 | import { mergeEnvironmentSettings, resolveEnvironmentIdentificationMode } from "./useSettings"; | |
| 10 | ||
| 11 | describe("resolveEnvironmentIdentificationMode", () => { | |
| 12 | it("keeps identification hidden until client settings hydrate", () => { | |
| 13 | expect(resolveEnvironmentIdentificationMode({ mode: "artwork", settingsHydrated: false })).toBe( | |
| 14 | "none", | |
| 15 | ); | |
| 16 | expect(resolveEnvironmentIdentificationMode({ mode: "pill", settingsHydrated: true })).toBe( | |
| 17 | "pill", | |
| 18 | ); | |
| 19 | }); | |
| 20 | }); | |
| 10 | 21 | |
| 11 | 22 | describe("mergeEnvironmentSettings", () => { |
| 12 | 23 | it("combines the selected environment's server settings with client preferences", () => { |
apps/web/src/hooks/useSettings.ts+15| ... | ... | @@ -21,6 +21,7 @@ import { |
| 21 | 21 | type ClientSettingsPatch, |
| 22 | 22 | type ClientSettings, |
| 23 | 23 | DEFAULT_CLIENT_SETTINGS, |
| 24 | type EnvironmentIdentificationMode, | |
| 24 | 25 | type UnifiedSettings, |
| 25 | 26 | } from "@t3tools/contracts/settings"; |
| 26 | 27 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| ... | ... | @@ -222,6 +223,20 @@ export function useClientSettings<T = ClientSettings>( |
| 222 | 223 | return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); |
| 223 | 224 | } |
| 224 | 225 | |
| 226 | export function resolveEnvironmentIdentificationMode(input: { | |
| 227 | mode: EnvironmentIdentificationMode; | |
| 228 | settingsHydrated: boolean; | |
| 229 | }): EnvironmentIdentificationMode { | |
| 230 | // Avoid briefly rendering the default artwork before a persisted pill/none choice loads. | |
| 231 | return input.settingsHydrated ? input.mode : "none"; | |
| 232 | } | |
| 233 | ||
| 234 | export function useEnvironmentIdentificationMode(): EnvironmentIdentificationMode { | |
| 235 | const settingsHydrated = useClientSettingsHydrated(); | |
| 236 | const mode = useClientSettingsValue().environmentIdentificationMode; | |
| 237 | return resolveEnvironmentIdentificationMode({ mode, settingsHydrated }); | |
| 238 | } | |
| 239 | ||
| 225 | 240 | /** |
| 226 | 241 | * Resolved sidebar v2 state: an explicit choice in Settings → Beta if the user |
| 227 | 242 | * has made one, otherwise the default for this build stage (on for nightly and |
packages/contracts/src/settings.test.ts+18| ... | ... | @@ -49,6 +49,24 @@ describe("ClientSettings glass opacity", () => { |
| 49 | 49 | }); |
| 50 | 50 | }); |
| 51 | 51 | |
| 52 | describe("ClientSettings environment identification", () => { | |
| 53 | it("defaults to artwork and accepts each presentation mode", () => { | |
| 54 | expect(decodeClientSettings({}).environmentIdentificationMode).toBe("artwork"); | |
| 55 | ||
| 56 | for (const mode of ["artwork", "pill", "none"] as const) { | |
| 57 | expect( | |
| 58 | decodeClientSettingsPatch({ environmentIdentificationMode: mode }) | |
| 59 | .environmentIdentificationMode, | |
| 60 | ).toBe(mode); | |
| 61 | } | |
| 62 | }); | |
| 63 | ||
| 64 | it("rejects unsupported presentation modes", () => { | |
| 65 | expect(() => decodeClientSettings({ environmentIdentificationMode: "badge" })).toThrow(); | |
| 66 | expect(() => decodeClientSettingsPatch({ environmentIdentificationMode: "badge" })).toThrow(); | |
| 67 | }); | |
| 68 | }); | |
| 69 | ||
| 52 | 70 | describe("ClientSettings sidebar v2", () => { |
| 53 | 71 | it("defaults the beta off with a three-day auto-settle threshold", () => { |
| 54 | 72 | const settings = decodeClientSettings({}); |
packages/contracts/src/settings.ts+7| ... | ... | @@ -58,6 +58,9 @@ export const GlassOpacity = Schema.Int.check( |
| 58 | 58 | ); |
| 59 | 59 | export type GlassOpacity = typeof GlassOpacity.Type; |
| 60 | 60 | export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; |
| 61 | export const EnvironmentIdentificationMode = Schema.Literals(["artwork", "pill", "none"]); | |
| 62 | export type EnvironmentIdentificationMode = typeof EnvironmentIdentificationMode.Type; | |
| 63 | export const DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE: EnvironmentIdentificationMode = "artwork"; | |
| 61 | 64 | |
| 62 | 65 | export const ClientSettingsSchema = Schema.Struct({ |
| 63 | 66 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| ... | ... | @@ -67,6 +70,9 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 67 | 70 | Schema.withDecodingDefault(Effect.succeed([])), |
| 68 | 71 | ), |
| 69 | 72 | diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 73 | environmentIdentificationMode: EnvironmentIdentificationMode.pipe( | |
| 74 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE)), | |
| 75 | ), | |
| 70 | 76 | glassOpacity: GlassOpacity.pipe( |
| 71 | 77 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), |
| 72 | 78 | ), |
| ... | ... | @@ -611,6 +617,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 611 | 617 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), |
| 612 | 618 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), |
| 613 | 619 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), |
| 620 | environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), | |
| 614 | 621 | glassOpacity: Schema.optionalKey(GlassOpacity), |
| 615 | 622 | favorites: Schema.optionalKey( |
| 616 | 623 | Schema.Array( |