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