Skip to content
Source
Sign inSign up
Open History to see the last commit

_chat.settings.tsx

185 lines6.9 KB

1import { workspaceAreaProps } from "../lib/workspaceFocus";
2import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router";
3import { useState, type ReactNode } from "react";
4import { RotateCcwIcon } from "lucide-react";
5import { Button } from "../components/ui/button";
6import { useSettingsRestore } from "../components/settings/SettingsPanels";
7​
8import { AppRouteModal, AppRouteModalClose } from "../components/AppRouteModal";
9import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";
10import { SettingsSidebarNav } from "../components/settings/SettingsSidebarNav";
11import {
12 SettingsScopeProvider,
13 useSettingsScope,
14} from "../components/settings/SettingsScopeContext";
15import { useEnvironments } from "../state/environments";
16import { SettingsScopeNotice } from "../components/settings/SettingsScopeNotice";
17import { SETTINGS_DEVICE_ONLY_PATHS } from "../components/settings/SettingsScopeSentence";
18import { SettingsPageContainer } from "../components/settings/settingsLayout";
19import {
20 retainSettingsScope,
21 validateSettingsRouteSearch,
22} from "../components/settings/settingsScopeNavigation";
23import {
24 getSettingsSearchTargetScope,
25 getThreadAutoSettlementSearchAvailability,
26 isSettingsSearchScopeAvailable,
27 movedSettingsPath,
28} from "../components/settings/settingsSearch";
29​
30function RestoreDeviceDefaultsButton({ onRestored }: { onRestored: () => void }) {
31 const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);
32 return (
33 <Button
34 size="xs"
35 variant="ghost"
36 disabled={changedSettingLabels.length === 0}
37 onClick={() => void restoreDefaults()}
38 >
39 <RotateCcwIcon className="mx-1 size-3.5" />
40 <span className="max-sm:sr-only">Restore device defaults</span>
41 </Button>
42 );
44​
45function SettingsScopeBoundary({ pathname, children }: { pathname: string; children: ReactNode }) {
46 const { scope, connectedEnvironments } = useSettingsScope();
47 const { environments } = useEnvironments();
48 const hash = useLocation({ select: (location) => location.hash });
49 const searchTarget = getSettingsSearchTargetScope(hash);
50 const autoSettlementAvailability = searchTarget?.requiresThreadAutoSettlement
51 ? getThreadAutoSettlementSearchAvailability(environments, scope)
52 : null;
53 if (
54 scope.kind !== "unavailable" &&
55 searchTarget &&
56 autoSettlementAvailability &&
57 !autoSettlementAvailability.isTargetAvailable
58 ) {
59 return (
60 <SettingsScopeNotice
61 target="environment"
62 targetId={hash}
63 eligibleEnvironmentIds={autoSettlementAvailability.eligibleEnvironmentIds}
64 >
65 {autoSettlementAvailability.eligibleEnvironmentIds.length > 0
66 ? `${searchTarget.title} requires a supporting environment. Choose one to continue.`
67 : `${searchTarget.title} requires a supporting environment. Connect or update an environment to continue.`}
68 </SettingsScopeNotice>
69 );
70 }
71 if (
72 scope.kind !== "unavailable" &&
73 searchTarget &&
74 !isSettingsSearchScopeAvailable(searchTarget.scope, scope.kind)
75 ) {
76 const target =
77 searchTarget.scope === "environment" ||
78 searchTarget.scope === "project" ||
79 searchTarget.scope === "checkout"
80 ? searchTarget.scope
81 : "all";
82 return (
83 <SettingsScopeNotice target={target} targetId={hash}>
84 {`${searchTarget.title} is not available for the selected target. Choose its owning scope to continue.`}
85 </SettingsScopeNotice>
86 );
87 }
88 if (SETTINGS_DEVICE_ONLY_PATHS.has(pathname) || pathname === "/settings/projects") {
89 return children;
90 }
91 if (scope.kind === "unavailable")
92 return (
93 <SettingsPageContainer>
94 <p className="text-sm text-muted-foreground">{scope.message}</p>
95 </SettingsPageContainer>
96 );
97 if (scope.kind === "environment" && connectedEnvironments.length === 0) {
98 return (
99 <SettingsPageContainer>
100 <p className="text-sm text-muted-foreground">
101 Reconnect {scope.label} to change its settings.
102 </p>
103 </SettingsPageContainer>
104 );
105 }
106 return children;
108​
109function SettingsContentLayout() {
110 const contentArea = workspaceAreaProps("settings", 1, "Settings content");
111 const location = useLocation();
112 const { search, environment } = useSettingsScope();
113 const contentScopeKey =
114 location.pathname === "/settings/diagnostics" || location.pathname === "/settings/providers"
115 ? environment?.environmentId
116 : JSON.stringify(search);
117 const [restoreSignal, setRestoreSignal] = useState(0);
118​
119 return (
120 <AppRouteModal label="Settings">
121 <div className="relative flex min-h-0 flex-1">
122 <SettingsSidebarNav pathname={location.pathname} />
123 <div {...contentArea} className="flex min-h-0 min-w-0 flex-1 flex-col">
124 <header className="flex h-12 shrink-0 items-center gap-3 border-b ps-5 pe-2">
125 <SettingsBreadcrumb pathname={location.pathname} />
126 <div className="ms-auto flex shrink-0 items-center gap-1">
127 {location.pathname === "/settings/general" ? (
128 <RestoreDeviceDefaultsButton
129 onRestored={() => setRestoreSignal((value) => value + 1)}
130 />
131 ) : null}
132 <AppRouteModalClose />
133 </div>
134 </header>
135 <div key={`${contentScopeKey}:${restoreSignal}`} className="min-h-0 flex flex-1 flex-col">
136 <SettingsScopeBoundary pathname={location.pathname}>
137 <Outlet />
138 </SettingsScopeBoundary>
139 </div>
140 </div>
141 </div>
142 </AppRouteModal>
143 );
145​
146function SettingsRouteLayout() {
147 const rawSearch = Route.useSearch();
148 const navigate = Route.useNavigate();
149 const pathname = useLocation({ select: (location) => location.pathname });
150 return (
151 <SettingsScopeProvider
152 search={rawSearch}
153 singleEnvironment={pathname === "/settings/providers" || pathname === "/settings/secrets"}
154 deviceOnly={SETTINGS_DEVICE_ONLY_PATHS.has(pathname)}
155 onChange={(next) => {
156 // Send every axis so retain middleware recognizes "all" as an explicit target.
157 void navigate({
158 to: pathname,
159 search: () => ({
160 project: next.project,
161 machine: next.machine,
162 checkout: next.checkout,
163 }),
164 hash: "",
165 resetScroll: false,
166 });
167 }}
168 >
169 <SettingsContentLayout />
170 </SettingsScopeProvider>
171 );
173​
174export const Route = createFileRoute("/_chat/settings")({
175 validateSearch: validateSettingsRouteSearch,
176 search: { middlewares: [retainSettingsScope] },
177 beforeLoad: async ({ location }) => {
178 if (location.pathname === "/settings") {
179 throw redirect({ to: "/settings/general", replace: true });
180 }
181 const moved = movedSettingsPath(location.pathname, location.hash);
182 if (moved) throw redirect({ to: moved, search: true, hash: true, replace: true });
183 },
184 component: SettingsRouteLayout,
185});