authorgravatar for me@maxkatz.meMax Katz <me@maxkatz.me> 2026-07-28 13:28:29+04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-28 11:28:29+02:00
log4f584da0f84666bda0cd399721115b0001cc0308
treec9eeac23b5b3cd9d7de9d71b6bf2f8cce16098df
parent362f12792254335bfe2a015787d7cbbaa214654e
signature Signed by PGP key B5690EEEBB952194

feat(web): add appearance settings category (#4715)


4 files changed, 107 insertions(+), 61 deletions(-)

apps/web/src/components/settings/SettingsPanels.tsx+72-61
......@@ -524,58 +524,23 @@ export function useSettingsRestore(onRestored?: () => void) {
524524 };
525525}
526526
527export function GeneralSettingsPanel() {
527export function AppearanceSettingsPanel() {
528528 const { theme, setTheme } = useTheme();
529529 const settings = usePrimarySettings();
530530 const updateSettings = useUpdatePrimarySettings();
531531 const environmentStageLabel = useEnvironmentStageLabel();
532532 const showEnvironmentIdentification =
533533 resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null;
534 const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>(
535 readLastEnabledProjectGroupingMode(),
536 );
537 const observability = useAtomValue(primaryServerObservabilityAtom);
538 const serverProviders = useAtomValue(primaryServerProvidersAtom);
539534 const glassOpacityRatio =
540535 (settings.glassOpacity - MIN_GLASS_OPACITY) / (MAX_GLASS_OPACITY - MIN_GLASS_OPACITY);
541536 const glassOpacitySliderStyle = {
542537 "--glass-slider-progress": `${glassOpacityRatio * 100}%`,
543538 "--glass-slider-fill-offset": `${0.5 - glassOpacityRatio}rem`,
544539 } as CSSProperties;
545 const diagnosticsDescription = formatDiagnosticsDescription({
546 localTracingEnabled: observability?.localTracingEnabled ?? false,
547 otlpTracesEnabled: observability?.otlpTracesEnabled ?? false,
548 otlpTracesUrl: observability?.otlpTracesUrl,
549 otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false,
550 otlpMetricsUrl: observability?.otlpMetricsUrl,
551 });
552
553 const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders);
554 const textGenInstanceId = textGenerationModelSelection.instanceId;
555 const textGenModel = textGenerationModelSelection.model;
556 const textGenModelOptions = textGenerationModelSelection.options;
557 const textGenerationModelInstanceEntries = sortProviderInstanceEntries(
558 applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings),
559 );
560 const textGenInstanceEntry = textGenerationModelInstanceEntries.find(
561 (entry) => entry.instanceId === textGenInstanceId,
562 );
563 const textGenProvider: ProviderDriverKind =
564 textGenInstanceEntry?.driverKind ?? DEFAULT_DRIVER_KIND;
565 const textGenerationModelOptionsByInstance = getCustomModelOptionsByInstance(
566 settings,
567 serverProviders,
568 textGenInstanceId,
569 textGenModel,
570 );
571 const isTextGenerationModelDirty = !Equal.equals(
572 settings.textGenerationModelSelection ?? null,
573 DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null,
574 );
575540
576541 return (
577542 <SettingsPageContainer>
578 <SettingsSection title="General">
543 <SettingsSection title="Appearance">
579544 <SettingsRow
580545 title="Theme"
581546 description="Choose how T3 Code looks across the app."
......@@ -697,6 +662,76 @@ export function GeneralSettingsPanel() {
697662 />
698663 ) : null}
699664
665 <SettingsRow
666 title="Word wrap"
667 description="Wrap long lines in code blocks, tables, diffs, and file previews by default."
668 resetAction={
669 settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? (
670 <SettingResetButton
671 label="word wrapping"
672 onClick={() =>
673 updateSettings({
674 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
675 })
676 }
677 />
678 ) : null
679 }
680 control={
681 <Switch
682 checked={settings.wordWrap}
683 onCheckedChange={(checked) => updateSettings({ wordWrap: Boolean(checked) })}
684 aria-label="Wrap code, tables, diffs, and file previews by default"
685 />
686 }
687 />
688 </SettingsSection>
689 </SettingsPageContainer>
690 );
691}
692
693export function GeneralSettingsPanel() {
694 const settings = usePrimarySettings();
695 const updateSettings = useUpdatePrimarySettings();
696 const lastEnabledProjectGroupingMode = useRef<SidebarProjectGroupingMode>(
697 readLastEnabledProjectGroupingMode(),
698 );
699 const observability = useAtomValue(primaryServerObservabilityAtom);
700 const serverProviders = useAtomValue(primaryServerProvidersAtom);
701 const diagnosticsDescription = formatDiagnosticsDescription({
702 localTracingEnabled: observability?.localTracingEnabled ?? false,
703 otlpTracesEnabled: observability?.otlpTracesEnabled ?? false,
704 otlpTracesUrl: observability?.otlpTracesUrl,
705 otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false,
706 otlpMetricsUrl: observability?.otlpMetricsUrl,
707 });
708
709 const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders);
710 const textGenInstanceId = textGenerationModelSelection.instanceId;
711 const textGenModel = textGenerationModelSelection.model;
712 const textGenModelOptions = textGenerationModelSelection.options;
713 const textGenerationModelInstanceEntries = sortProviderInstanceEntries(
714 applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings),
715 );
716 const textGenInstanceEntry = textGenerationModelInstanceEntries.find(
717 (entry) => entry.instanceId === textGenInstanceId,
718 );
719 const textGenProvider: ProviderDriverKind =
720 textGenInstanceEntry?.driverKind ?? DEFAULT_DRIVER_KIND;
721 const textGenerationModelOptionsByInstance = getCustomModelOptionsByInstance(
722 settings,
723 serverProviders,
724 textGenInstanceId,
725 textGenModel,
726 );
727 const isTextGenerationModelDirty = !Equal.equals(
728 settings.textGenerationModelSelection ?? null,
729 DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null,
730 );
731
732 return (
733 <SettingsPageContainer>
734 <SettingsSection title="General">
700735 <SettingsRow
701736 title="Project Grouping"
702737 description="Combine matching repositories across environments."
......@@ -775,30 +810,6 @@ export function GeneralSettingsPanel() {
775810 }
776811 />
777812
778 <SettingsRow
779 title="Word wrap"
780 description="Wrap long lines in code blocks, tables, diffs, and file previews by default."
781 resetAction={
782 settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? (
783 <SettingResetButton
784 label="word wrapping"
785 onClick={() =>
786 updateSettings({
787 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
788 })
789 }
790 />
791 ) : null
792 }
793 control={
794 <Switch
795 checked={settings.wordWrap}
796 onCheckedChange={(checked) => updateSettings({ wordWrap: Boolean(checked) })}
797 aria-label="Wrap code, tables, diffs, and file previews by default"
798 />
799 }
800 />
801
802813 <SettingsRow
803814 title="Hide whitespace changes"
804815 description="Set whether the diff panel ignores whitespace-only edits by default."
apps/web/src/components/settings/SettingsSidebarNav.tsx+3
......@@ -7,6 +7,7 @@ import {
77 GitBranchIcon,
88 KeyboardIcon,
99 Link2Icon,
10 PaletteIcon,
1011 Settings2Icon,
1112} from "lucide-react";
1213import { useCanGoBack, useNavigate } from "@tanstack/react-router";
......@@ -24,6 +25,7 @@ import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3Conne
2425
2526export type SettingsSectionPath =
2627 | "/settings/general"
28 | "/settings/appearance"
2729 | "/settings/keybindings"
2830 | "/settings/providers"
2931 | "/settings/source-control"
......@@ -37,6 +39,7 @@ export const SETTINGS_NAV_ITEMS: ReadonlyArray<{
3739 icon: ComponentType<{ className?: string }>;
3840}> = [
3941 { label: "General", to: "/settings/general", icon: Settings2Icon },
42 { label: "Appearance", to: "/settings/appearance", icon: PaletteIcon },
4043 { label: "Keybindings", to: "/settings/keybindings", icon: KeyboardIcon },
4144 { label: "Providers", to: "/settings/providers", icon: BotIcon },
4245 { label: "Source Control", to: "/settings/source-control", icon: GitBranchIcon },
apps/web/src/routeTree.gen.ts+21
......@@ -22,6 +22,7 @@ import { Route as SettingsDiagnosticsRouteImport } from './routes/settings.diagn
2222import { Route as SettingsConnectionsRouteImport } from './routes/settings.connections'
2323import { Route as SettingsBetaRouteImport } from './routes/settings.beta'
2424import { Route as SettingsArchivedRouteImport } from './routes/settings.archived'
25import { Route as SettingsAppearanceRouteImport } from './routes/settings.appearance'
2526import { Route as ConnectCallbackRouteImport } from './routes/connect_.callback'
2627import { Route as ChatDraftDraftIdRouteImport } from './routes/_chat.draft.$draftId'
2728import { Route as ChatEnvironmentIdThreadIdRouteImport } from './routes/_chat.$environmentId.$threadId'
......@@ -90,6 +91,11 @@ const SettingsArchivedRoute = SettingsArchivedRouteImport.update({
9091 path: '/archived',
9192 getParentRoute: () => SettingsRoute,
9293} as any)
94const SettingsAppearanceRoute = SettingsAppearanceRouteImport.update({
95 id: '/appearance',
96 path: '/appearance',
97 getParentRoute: () => SettingsRoute,
98} as any)
9399const ConnectCallbackRoute = ConnectCallbackRouteImport.update({
94100 id: '/connect_/callback',
95101 path: '/connect/callback',
......@@ -113,6 +119,7 @@ export interface FileRoutesByFullPath {
113119 '/pair': typeof PairRoute
114120 '/settings': typeof SettingsRouteWithChildren
115121 '/connect/callback': typeof ConnectCallbackRoute
122 '/settings/appearance': typeof SettingsAppearanceRoute
116123 '/settings/archived': typeof SettingsArchivedRoute
117124 '/settings/beta': typeof SettingsBetaRoute
118125 '/settings/connections': typeof SettingsConnectionsRoute
......@@ -129,6 +136,7 @@ export interface FileRoutesByTo {
129136 '/pair': typeof PairRoute
130137 '/settings': typeof SettingsRouteWithChildren
131138 '/connect/callback': typeof ConnectCallbackRoute
139 '/settings/appearance': typeof SettingsAppearanceRoute
132140 '/settings/archived': typeof SettingsArchivedRoute
133141 '/settings/beta': typeof SettingsBetaRoute
134142 '/settings/connections': typeof SettingsConnectionsRoute
......@@ -148,6 +156,7 @@ export interface FileRoutesById {
148156 '/pair': typeof PairRoute
149157 '/settings': typeof SettingsRouteWithChildren
150158 '/connect_/callback': typeof ConnectCallbackRoute
159 '/settings/appearance': typeof SettingsAppearanceRoute
151160 '/settings/archived': typeof SettingsArchivedRoute
152161 '/settings/beta': typeof SettingsBetaRoute
153162 '/settings/connections': typeof SettingsConnectionsRoute
......@@ -168,6 +177,7 @@ export interface FileRouteTypes {
168177 | '/pair'
169178 | '/settings'
170179 | '/connect/callback'
180 | '/settings/appearance'
171181 | '/settings/archived'
172182 | '/settings/beta'
173183 | '/settings/connections'
......@@ -184,6 +194,7 @@ export interface FileRouteTypes {
184194 | '/pair'
185195 | '/settings'
186196 | '/connect/callback'
197 | '/settings/appearance'
187198 | '/settings/archived'
188199 | '/settings/beta'
189200 | '/settings/connections'
......@@ -202,6 +213,7 @@ export interface FileRouteTypes {
202213 | '/pair'
203214 | '/settings'
204215 | '/connect_/callback'
216 | '/settings/appearance'
205217 | '/settings/archived'
206218 | '/settings/beta'
207219 | '/settings/connections'
......@@ -316,6 +328,13 @@ declare module '@tanstack/react-router' {
316328 preLoaderRoute: typeof SettingsArchivedRouteImport
317329 parentRoute: typeof SettingsRoute
318330 }
331 '/settings/appearance': {
332 id: '/settings/appearance'
333 path: '/appearance'
334 fullPath: '/settings/appearance'
335 preLoaderRoute: typeof SettingsAppearanceRouteImport
336 parentRoute: typeof SettingsRoute
337 }
319338 '/connect_/callback': {
320339 id: '/connect_/callback'
321340 path: '/connect/callback'
......@@ -355,6 +374,7 @@ const ChatRouteChildren: ChatRouteChildren = {
355374const ChatRouteWithChildren = ChatRoute._addFileChildren(ChatRouteChildren)
356375
357376interface SettingsRouteChildren {
377 SettingsAppearanceRoute: typeof SettingsAppearanceRoute
358378 SettingsArchivedRoute: typeof SettingsArchivedRoute
359379 SettingsBetaRoute: typeof SettingsBetaRoute
360380 SettingsConnectionsRoute: typeof SettingsConnectionsRoute
......@@ -366,6 +386,7 @@ interface SettingsRouteChildren {
366386}
367387
368388const SettingsRouteChildren: SettingsRouteChildren = {
389 SettingsAppearanceRoute: SettingsAppearanceRoute,
369390 SettingsArchivedRoute: SettingsArchivedRoute,
370391 SettingsBetaRoute: SettingsBetaRoute,
371392 SettingsConnectionsRoute: SettingsConnectionsRoute,
apps/web/src/routes/settings.appearance.tsx created+11
......@@ -0,0 +1,11 @@
1import { createFileRoute } from "@tanstack/react-router";
2
3import { AppearanceSettingsPanel } from "../components/settings/SettingsPanels";
4
5function SettingsAppearanceRoute() {
6 return <AppearanceSettingsPanel />;
7}
8
9export const Route = createFileRoute("/settings/appearance")({
10 component: SettingsAppearanceRoute,
11});