Open History to see the last commit
1import { useAuth } from "@clerk/react";
2import { Link } from "@tanstack/react-router";
3import { managedCapabilityDenial } from "@t3tools/contracts";
4
5import { hasCloudPublicConfig } from "~/cloud/publicConfig";
6import { useT3ConnectAuthPrompt } from "../clerk/useT3ConnectAuthPrompt";
7import { Button } from "../ui/button";
8import { Switch } from "../ui/switch";
9import { ScopedSwitch } from "./ScopedSwitch";
10import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout";
11import { searchableSetting } from "./settingsSearch";
12import { useScopedSettings, useUpdateScopedSettings } from "./useScopedSettings";
13import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext";
14import { firstManagedDenial, usePrimaryManagedPolicy } from "~/state/managedPolicy";
15
16/** Features still settling in. */
17export function LabsSettingsPanel() {
18 return (
19 <SettingsPageContainer>
20 <SettingsSection id="beta" title="Beta">
21 <BoringSidebarRow />
22 {/* Clerk is only mounted when this build has T3 Connect. */}
23 {hasCloudPublicConfig() ? <GuestLinksRowWithSignIn /> : <GuestLinksRow />}
24 <LinkedEnvironmentsRow />
25 <PromptSuggestionsRow />
26 <SimlockHostsRow />
27 <FastWorktreeCheckoutRow />
28 </SettingsSection>
29 </SettingsPageContainer>
30 );
31}
32
33function BoringSidebarRow() {
34 const settings = useScopedSettings();
35 const updateSettings = useUpdateScopedSettings();
36 return (
37 <SettingsRow
38 {...searchableSetting("boring-sidebar")}
39 description="Group sidebar threads into project folders. Pinned threads stay on top, and Snoozed and Settled stay below the folders."
40 control={
41 <Switch
42 checked={settings.boringSidebarEnabled}
43 onCheckedChange={(checked) => updateSettings({ boringSidebarEnabled: Boolean(checked) })}
44 aria-label="Boring sidebar (group by project)"
45 />
46 }
47 >
48 {settings.boringSidebarEnabled ? (
49 <div className="mt-2 mb-2 border-l-2 border-border/60 ps-3">
50 <BoringSidebarMultilineRow />
51 </div>
52 ) : null}
53 </SettingsRow>
54 );
55}
56
57function BoringSidebarMultilineRow() {
58 const settings = useScopedSettings();
59 const updateSettings = useUpdateScopedSettings();
60 return (
61 <SettingsRow
62 {...searchableSetting("boring-sidebar-multiline")}
63 className="px-0 py-2 sm:px-0"
64 description="Show folder threads on two lines: the title, then the branch, pull request, and agent."
65 control={
66 <Switch
67 checked={settings.boringSidebarMultilineRows}
68 onCheckedChange={(checked) =>
69 updateSettings({ boringSidebarMultilineRows: Boolean(checked) })
70 }
71 aria-label="Two-line folder rows"
72 />
73 }
74 />
75 );
76}
77
78/** Signed out of T3 Connect, the row offers to sign in instead of its switch. */
79function GuestLinksRowWithSignIn() {
80 const { isSignedIn } = useAuth();
81 const { openAuthPrompt } = useT3ConnectAuthPrompt();
82 // `isSignedIn` is undefined until Clerk loads.
83 if (isSignedIn !== false) return <GuestLinksRow />;
84 return (
85 <SettingsRow
86 {...searchableSetting("guest-links")}
87 description="Share a thread with a guest who watches it live in a browser and sends messages you approve. Sign in to T3 Connect to use guest links."
88 control={
89 <Button size="sm" variant="outline" onClick={openAuthPrompt}>
90 Sign in
91 </Button>
92 }
93 />
94 );
95}
96
97function GuestLinksRow() {
98 const settings = useScopedSettings();
99 const updateSettings = useUpdateScopedSettings();
100 const scope = useOptionalSettingsScope();
101 const primaryPolicy = usePrimaryManagedPolicy();
102 // Guests arrive through T3 Connect, so a policy that turns Connect off turns this off too.
103 const connectDenial = firstManagedDenial(
104 scope
105 ? scope.connectedEnvironments.map((environment) => environment.serverConfig?.managedPolicy)
106 : [primaryPolicy],
107 (policy) => managedCapabilityDenial(policy, "connect"),
108 );
109 return (
110 <SettingsRow
111 serverScoped
112 {...searchableSetting("guest-links")}
113 settingKeys={["guestSharingEnabled"]}
114 // Turning guest links off still works without T3 Connect.
115 managed={settings.guestSharingEnabled ? undefined : connectDenial}
116 description={
117 <>
118 Share a thread with a guest who watches it live in a browser and sends messages you
119 approve. Needs T3 Connect: sign in and link this machine in{" "}
120 <Link to="/settings/access" className="underline underline-offset-2">
121 Sharing & access
122 </Link>
123 . Turning it off stops every guest link.
124 </>
125 }
126 control={
127 <ScopedSwitch
128 settingKeys={["guestSharingEnabled"]}
129 checked={settings.guestSharingEnabled}
130 onCheckedChange={(checked) => updateSettings({ guestSharingEnabled: Boolean(checked) })}
131 aria-label="Guest links"
132 />
133 }
134 />
135 );
136}
137
138function LinkedEnvironmentsRow() {
139 const settings = useScopedSettings();
140 const updateSettings = useUpdateScopedSettings();
141 return (
142 <SettingsRow
143 serverScoped
144 {...searchableSetting("linked-environments-beta")}
145 settingKeys={["linkedEnvironmentsEnabled"]}
146 description={
147 <>
148 Let agents here work on your other machines, and theirs here. Turn it on for each machine,
149 then link them in{" "}
150 <Link to="/settings/connections" className="underline underline-offset-2">
151 Connections
152 </Link>
153 . While off, nothing new starts over a link; running tasks finish.
154 </>
155 }
156 control={
157 <ScopedSwitch
158 settingKeys={["linkedEnvironmentsEnabled"]}
159 checked={settings.linkedEnvironmentsEnabled}
160 onCheckedChange={(checked) =>
161 updateSettings({ linkedEnvironmentsEnabled: Boolean(checked) })
162 }
163 aria-label="Linked environments"
164 />
165 }
166 />
167 );
168}
169
170function PromptSuggestionsRow() {
171 const settings = useScopedSettings();
172 const updateSettings = useUpdateScopedSettings();
173 return (
174 <SettingsRow
175 serverScoped
176 {...searchableSetting("prompt-suggestions-beta")}
177 settingKeys={["promptSuggestionsEnabled"]}
178 description="Show a predicted next message in the empty composer after a Claude or Codex turn. Tab uses it. Each is an extra cached request on your plan. Needs Codex 0.165+. Claude threads update on restart."
179 control={
180 <ScopedSwitch
181 settingKeys={["promptSuggestionsEnabled"]}
182 checked={settings.promptSuggestionsEnabled}
183 onCheckedChange={(checked) =>
184 updateSettings({ promptSuggestionsEnabled: Boolean(checked) })
185 }
186 aria-label="Suggest next prompt"
187 />
188 }
189 />
190 );
191}
192
193function SimlockHostsRow() {
194 const settings = useScopedSettings();
195 const updateSettings = useUpdateScopedSettings();
196 return (
197 <SettingsRow
198 serverScoped
199 {...searchableSetting("simlock-hosts")}
200 settingKeys={["simlockDeviceHostsEnabled"]}
201 description={
202 <>
203 Let the Device panel lease simulators and emulators from your own{" "}
204 <a
205 href="https://incubator.callstack.com/simlock/"
206 target="_blank"
207 rel="noreferrer"
208 className="underline underline-offset-2"
209 >
210 Simlock
211 </a>{" "}
212 pool, added in{" "}
213 <Link to="/settings/devices" className="underline underline-offset-2">
214 Devices
215 </Link>
216 . An alpha that may change or break. T3 Code never installs Simlock.
217 </>
218 }
219 control={
220 <ScopedSwitch
221 settingKeys={["simlockDeviceHostsEnabled"]}
222 checked={settings.simlockDeviceHostsEnabled}
223 onCheckedChange={(checked) =>
224 updateSettings({ simlockDeviceHostsEnabled: Boolean(checked) })
225 }
226 aria-label="Simlock device hosts"
227 />
228 }
229 />
230 );
231}
232
233/** Shows only when every machine in scope is a Mac, since only macOS servers honor it. */
234function FastWorktreeCheckoutRow() {
235 const { scope, connectedEnvironments } = useSettingsScope();
236 const settings = useScopedSettings();
237 const updateSettings = useUpdateScopedSettings();
238 if (
239 scope.kind === "project" ||
240 scope.kind === "checkout" ||
241 connectedEnvironments.length === 0 ||
242 connectedEnvironments.some(
243 (environment) =>
244 environment.serverConfig?.environment.capabilities.fastWorktreeCheckout !== true,
245 )
246 )
247 return null;
248 return (
249 <SettingsRow
250 serverScoped
251 {...searchableSetting("worktree-fast-checkout")}
252 settingKeys={["fastWorktreeCheckout"]}
253 description="On a Mac, clone new worktrees from a checkout cached in the T3 home folder instead of writing every file. Uses Git’s normal checkout when it can’t. Turning it off deletes the cache."
254 control={
255 <ScopedSwitch
256 settingKeys={["fastWorktreeCheckout"]}
257 checked={settings.fastWorktreeCheckout}
258 onCheckedChange={(checked) => updateSettings({ fastWorktreeCheckout: Boolean(checked) })}
259 aria-label="Fast worktree checkout"
260 />
261 }
262 />
263 );
264}