| 1 | import { |
| 2 | DEFAULT_SERVER_SETTINGS, |
| 3 | type ModelSelection, |
| 4 | type ProviderInstanceId, |
| 5 | type WorktreeSubmodules, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { createModelSelection } from "@t3tools/shared/model"; |
| 8 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; |
| 9 | import { useNavigate } from "@tanstack/react-router"; |
| 10 | |
| 11 | import { mergeEnvironmentSettings } from "../../hooks/useSettings"; |
| 12 | import { getCustomModelOptionsByInstance } from "../../modelSelection"; |
| 13 | import { |
| 14 | applyProviderInstanceSettings, |
| 15 | deriveProviderInstanceEntries, |
| 16 | resolveDefaultProviderModelSelection, |
| 17 | sortProviderInstanceEntries, |
| 18 | } from "../../providerInstances"; |
| 19 | import { useEnvironments } from "../../state/environments"; |
| 20 | import { EMPTY_SERVER_PROVIDERS } from "../../state/server"; |
| 21 | import { resolveEnvModeLabel, WORKTREE_SUBMODULES_LABELS } from "../BranchToolbar.logic"; |
| 22 | import { ProviderModelPicker } from "../chat/ProviderModelPicker"; |
| 23 | import { runtimeModeConfig, runtimeModeOptions } from "../chat/runtimeModeConfig"; |
| 24 | import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic"; |
| 25 | import { TraitsPicker } from "../chat/TraitsPicker"; |
| 26 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; |
| 27 | import { toastManager } from "../ui/toast"; |
| 28 | import { Switch } from "../ui/switch"; |
| 29 | import type { ProjectSettingsCategory } from "./ProjectSettingsPanel"; |
| 30 | import { searchableSetting } from "./settingsSearch"; |
| 31 | import { useSettingsScope } from "./SettingsScopeContext"; |
| 32 | import { |
| 33 | SETTINGS_PICKER_TRIGGER_CLASSNAME, |
| 34 | SettingResetButton, |
| 35 | SettingsRow, |
| 36 | SettingsSection, |
| 37 | } from "./settingsLayout"; |
| 38 | import { |
| 39 | useScopedSettings, |
| 40 | useScopedSettingsMixed, |
| 41 | useScopedSettingSource, |
| 42 | useUpdateScopedSettings, |
| 43 | } from "./useScopedSettings"; |
| 44 | |
| 45 | /** |
| 46 | * Rows for the settings a project may override. The same rows edit |
| 47 | * environment defaults at an environment scope and project overrides at a |
| 48 | * project or checkout scope; the scoped hooks route the write. |
| 49 | */ |
| 50 | const WORKTREE_SUBMODULES_OPTIONS = ["recursive", "top-level", "none"] as const; |
| 51 | function isWorktreeSubmodules(value: string | null): value is WorktreeSubmodules { |
| 52 | return value !== null && (WORKTREE_SUBMODULES_OPTIONS as readonly string[]).includes(value); |
| 53 | } |
| 54 | |
| 55 | export function ProjectDefaultsSettings({ category }: { category: ProjectSettingsCategory }) { |
| 56 | const { scope, target, targets, connectedEnvironments } = useSettingsScope(); |
| 57 | const settings = useScopedSettings(); |
| 58 | const updateSettings = useUpdateScopedSettings(); |
| 59 | const navigate = useNavigate(); |
| 60 | const { environments } = useEnvironments(); |
| 61 | const representative = target |
| 62 | ? environments.find((environment) => environment.environmentId === target.environmentId) |
| 63 | : undefined; |
| 64 | const providers = representative?.serverConfig?.providers ?? EMPTY_SERVER_PROVIDERS; |
| 65 | const selection = resolveDefaultProviderModelSelection(providers, settings.defaultModelSelection); |
| 66 | const entries = sortProviderInstanceEntries( |
| 67 | applyProviderInstanceSettings( |
| 68 | deriveProviderInstanceEntries(providers), |
| 69 | settings, |
| 70 | target?.projectId ?? null, |
| 71 | ), |
| 72 | ); |
| 73 | const modelOptions = getCustomModelOptionsByInstance( |
| 74 | settings, |
| 75 | providers, |
| 76 | selection?.instanceId, |
| 77 | selection?.model, |
| 78 | ); |
| 79 | const activeEntry = entries.find((entry) => entry.instanceId === selection?.instanceId); |
| 80 | const mixedModel = useScopedSettingsMixed(["defaultModelSelection"]); |
| 81 | const mixedPermissions = useScopedSettingsMixed(["defaultRuntimeMode"]); |
| 82 | const PermissionIcon = runtimeModeConfig[settings.defaultRuntimeMode].icon; |
| 83 | const mixedWorkspace = useScopedSettingsMixed(["defaultThreadEnvMode"]); |
| 84 | const mixedSubmodules = useScopedSettingsMixed(["worktreeSubmodules"]); |
| 85 | const mixedBrowser = useScopedSettingsMixed(["enableAgentBrowserAccess"]); |
| 86 | const mixedAutoPull = useScopedSettingsMixed(["defaultAutoPull"]); |
| 87 | const mixedAgentCredits = useScopedSettingsMixed(["removeAgentCreditsOnMerge"]); |
| 88 | const mixedMergeMethod = useScopedSettingsMixed(["pullRequestMergeMethod"]); |
| 89 | const modelSource = useScopedSettingSource(["defaultModelSelection"]); |
| 90 | const isProjectScope = scope.kind === "project" || scope.kind === "checkout"; |
| 91 | const unavailable = connectedEnvironments.length === 0; |
| 92 | // File-backed keys show their effective value; the target already carries |
| 93 | // the checkout's t3.json, and a null file here only fills the built-in. |
| 94 | // The reset arrow beside the title clears the tier (SettingsRow handles a |
| 95 | // project override, the environment value is cleared here), so the picker |
| 96 | // has no "inherit" item. |
| 97 | const effective = target |
| 98 | ? resolveProjectSettings(target.settings, null, null, null).settings |
| 99 | : null; |
| 100 | |
| 101 | function modelDisabledReason(instanceId: ProviderInstanceId, model: string): string | null { |
| 102 | const sourceEntry = entries.find((entry) => entry.instanceId === instanceId); |
| 103 | for (const candidate of targets) { |
| 104 | const environment = environments.find( |
| 105 | (entry) => entry.environmentId === candidate.environmentId, |
| 106 | ); |
| 107 | const config = environment?.serverConfig; |
| 108 | if (!config) continue; |
| 109 | const entry = applyProviderInstanceSettings( |
| 110 | deriveProviderInstanceEntries(config.providers), |
| 111 | candidate.settings, |
| 112 | candidate.projectId, |
| 113 | ).find((option) => option.instanceId === instanceId); |
| 114 | const options = getCustomModelOptionsByInstance( |
| 115 | mergeEnvironmentSettings(candidate.settings, settings), |
| 116 | config.providers, |
| 117 | ).get(instanceId); |
| 118 | if ( |
| 119 | !entry?.enabled || |
| 120 | !entry.isAvailable || |
| 121 | entry.driverKind !== sourceEntry?.driverKind || |
| 122 | !options?.some((option) => option.slug === model && !option.isUnavailable) |
| 123 | ) { |
| 124 | return `This model is unavailable on ${environment?.label ?? "a selected environment"}. Select that environment to choose its model separately.`; |
| 125 | } |
| 126 | } |
| 127 | return null; |
| 128 | } |
| 129 | |
| 130 | const setModel = (value: ModelSelection | null) => { |
| 131 | const reason = value ? modelDisabledReason(value.instanceId, value.model) : null; |
| 132 | if (reason) { |
| 133 | toastManager.add({ type: "error", title: "Default model not saved", description: reason }); |
| 134 | return; |
| 135 | } |
| 136 | updateSettings({ defaultModelSelection: value }); |
| 137 | }; |
| 138 | |
| 139 | const modelRow = ( |
| 140 | <SettingsRow |
| 141 | serverScoped |
| 142 | settingKeys={["defaultModelSelection"]} |
| 143 | mixed={mixedModel} |
| 144 | id="default-model" |
| 145 | title="Model" |
| 146 | description={ |
| 147 | isProjectScope |
| 148 | ? "Model for new threads in this project." |
| 149 | : "Default model for new threads. Projects can override it." |
| 150 | } |
| 151 | status={ |
| 152 | unavailable || mixedModel || modelSource === "project" |
| 153 | ? undefined |
| 154 | : settings.defaultModelSelection === null |
| 155 | ? "Automatic" |
| 156 | : undefined |
| 157 | } |
| 158 | resetAction={ |
| 159 | settings.defaultModelSelection !== null ? ( |
| 160 | <SettingResetButton label="default model" onClick={() => setModel(null)} /> |
| 161 | ) : null |
| 162 | } |
| 163 | control={ |
| 164 | selection && activeEntry ? ( |
| 165 | <div className="flex min-w-0 flex-wrap items-center justify-end gap-1.5"> |
| 166 | <ProviderModelPicker |
| 167 | environmentId={representative?.environmentId ?? null} |
| 168 | activeInstanceId={selection.instanceId} |
| 169 | model={selection.model} |
| 170 | lockedProvider={null} |
| 171 | instanceEntries={entries} |
| 172 | modelOptionsByInstance={modelOptions} |
| 173 | triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} |
| 174 | {...(mixedModel ? { triggerLabel: "Mixed" } : {})} |
| 175 | getModelDisabledReason={modelDisabledReason} |
| 176 | onOpenProviderSetup={(instanceId) => { |
| 177 | if (representative) |
| 178 | void navigate({ |
| 179 | to: "/settings/providers", |
| 180 | search: { environmentId: representative.environmentId, instanceId }, |
| 181 | }); |
| 182 | }} |
| 183 | onInstanceModelChange={(instanceId, model) => |
| 184 | setModel(createModelSelection(instanceId, model)) |
| 185 | } |
| 186 | /> |
| 187 | {!mixedModel ? ( |
| 188 | <TraitsPicker |
| 189 | provider={activeEntry.driverKind} |
| 190 | models={activeEntry.models} |
| 191 | model={selection.model} |
| 192 | prompt="" |
| 193 | onPromptChange={() => {}} |
| 194 | modelOptions={selection.options ?? []} |
| 195 | allowPromptInjectedEffort={false} |
| 196 | planModeEnabled={settings.planModeEnabled} |
| 197 | triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME} |
| 198 | onModelOptionsChange={(options) => |
| 199 | setModel(createModelSelection(selection.instanceId, selection.model, options)) |
| 200 | } |
| 201 | /> |
| 202 | ) : null} |
| 203 | </div> |
| 204 | ) : ( |
| 205 | <span className="text-sm text-muted-foreground">No providers available</span> |
| 206 | ) |
| 207 | } |
| 208 | /> |
| 209 | ); |
| 210 | const workspaceRow = ( |
| 211 | <SettingsRow |
| 212 | serverScoped |
| 213 | settingKeys={["defaultThreadEnvMode"]} |
| 214 | mixed={mixedWorkspace} |
| 215 | id={searchableSetting("new-threads").id} |
| 216 | title="Workspace" |
| 217 | description={ |
| 218 | isProjectScope |
| 219 | ? "Where new threads in this project start." |
| 220 | : "Where new threads start. Projects and their t3.json can override it." |
| 221 | } |
| 222 | resetAction={ |
| 223 | !isProjectScope && settings.defaultThreadEnvMode !== null ? ( |
| 224 | <SettingResetButton |
| 225 | label="default workspace" |
| 226 | onClick={() => updateSettings({ defaultThreadEnvMode: null })} |
| 227 | /> |
| 228 | ) : null |
| 229 | } |
| 230 | control={ |
| 231 | <Select |
| 232 | value={mixedWorkspace ? null : (effective?.defaultThreadEnvMode ?? null)} |
| 233 | onValueChange={(value) => { |
| 234 | if (value === "local" || value === "worktree") |
| 235 | updateSettings({ defaultThreadEnvMode: value }); |
| 236 | }} |
| 237 | > |
| 238 | <SelectTrigger size="sm" aria-label="Default workspace"> |
| 239 | <SelectValue> |
| 240 | {(value: string | null) => |
| 241 | value === "local" || value === "worktree" |
| 242 | ? resolveEnvModeLabel(value) |
| 243 | : unavailable |
| 244 | ? "Unavailable" |
| 245 | : "Mixed" |
| 246 | } |
| 247 | </SelectValue> |
| 248 | </SelectTrigger> |
| 249 | <SelectPopup align="end" alignItemWithTrigger={false}> |
| 250 | <SelectItem value="local">{resolveEnvModeLabel("local")}</SelectItem> |
| 251 | <SelectItem value="worktree">{resolveEnvModeLabel("worktree")}</SelectItem> |
| 252 | </SelectPopup> |
| 253 | </Select> |
| 254 | } |
| 255 | /> |
| 256 | ); |
| 257 | |
| 258 | return ( |
| 259 | <SettingsSection |
| 260 | id={ |
| 261 | category === "general" || category === "project" |
| 262 | ? "project-defaults" |
| 263 | : category === "integrations" |
| 264 | ? "browser-access" |
| 265 | : "source-control-defaults" |
| 266 | } |
| 267 | title={ |
| 268 | category === "general" || category === "project" |
| 269 | ? "New threads" |
| 270 | : category === "integrations" |
| 271 | ? "Browser" |
| 272 | : "Repositories" |
| 273 | } |
| 274 | > |
| 275 | {category === "project" ? ( |
| 276 | <> |
| 277 | {modelRow} |
| 278 | {workspaceRow} |
| 279 | </> |
| 280 | ) : category === "general" ? ( |
| 281 | <> |
| 282 | {modelRow} |
| 283 | <SettingsRow |
| 284 | serverScoped |
| 285 | settingKeys={["defaultRuntimeMode"]} |
| 286 | mixed={mixedPermissions} |
| 287 | {...searchableSetting("default-permissions")} |
| 288 | description={ |
| 289 | isProjectScope |
| 290 | ? "Permissions for new threads in this project." |
| 291 | : "Default permissions for new threads. Projects can override them." |
| 292 | } |
| 293 | resetAction={ |
| 294 | settings.defaultRuntimeMode !== DEFAULT_SERVER_SETTINGS.defaultRuntimeMode ? ( |
| 295 | <SettingResetButton |
| 296 | label="default permissions" |
| 297 | onClick={() => |
| 298 | updateSettings({ |
| 299 | defaultRuntimeMode: DEFAULT_SERVER_SETTINGS.defaultRuntimeMode, |
| 300 | }) |
| 301 | } |
| 302 | /> |
| 303 | ) : null |
| 304 | } |
| 305 | control={ |
| 306 | <Select |
| 307 | value={mixedPermissions ? null : settings.defaultRuntimeMode} |
| 308 | onValueChange={(value) => { |
| 309 | if (value) updateSettings({ defaultRuntimeMode: value }); |
| 310 | }} |
| 311 | > |
| 312 | <SelectTrigger size="sm" aria-label="Default permissions"> |
| 313 | {!mixedPermissions && ( |
| 314 | <PermissionIcon className="size-3.5 shrink-0 text-muted-foreground" /> |
| 315 | )} |
| 316 | <SelectValue> |
| 317 | {mixedPermissions |
| 318 | ? "Mixed" |
| 319 | : runtimeModeConfig[settings.defaultRuntimeMode].label} |
| 320 | </SelectValue> |
| 321 | </SelectTrigger> |
| 322 | <SelectPopup align="end" alignItemWithTrigger={false}> |
| 323 | {runtimeModeOptions.map((mode) => { |
| 324 | const option = runtimeModeConfig[mode]; |
| 325 | const Icon = option.icon; |
| 326 | return ( |
| 327 | <SelectItem key={mode} value={mode} className="min-w-64"> |
| 328 | <div className="grid gap-0.5"> |
| 329 | <span className="inline-flex items-center gap-1.5 font-medium"> |
| 330 | <Icon className="size-3.5 shrink-0 text-muted-foreground" /> |
| 331 | {option.label} |
| 332 | </span> |
| 333 | <span className="text-xs leading-4 text-muted-foreground"> |
| 334 | {option.description} |
| 335 | </span> |
| 336 | </div> |
| 337 | </SelectItem> |
| 338 | ); |
| 339 | })} |
| 340 | </SelectPopup> |
| 341 | </Select> |
| 342 | } |
| 343 | /> |
| 344 | {workspaceRow} |
| 345 | <SettingsRow |
| 346 | serverScoped |
| 347 | settingKeys={["worktreeSubmodules"]} |
| 348 | mixed={mixedSubmodules} |
| 349 | {...searchableSetting("worktree-submodules")} |
| 350 | description={ |
| 351 | isProjectScope |
| 352 | ? "How new worktrees in this project populate git submodules." |
| 353 | : "How new worktrees populate git submodules. Projects and their t3.json can override it." |
| 354 | } |
| 355 | resetAction={ |
| 356 | !isProjectScope && settings.worktreeSubmodules !== null ? ( |
| 357 | <SettingResetButton |
| 358 | label="worktree submodules" |
| 359 | onClick={() => updateSettings({ worktreeSubmodules: null })} |
| 360 | /> |
| 361 | ) : null |
| 362 | } |
| 363 | control={ |
| 364 | <Select |
| 365 | value={mixedSubmodules ? null : (effective?.worktreeSubmodules ?? null)} |
| 366 | onValueChange={(value) => { |
| 367 | if (isWorktreeSubmodules(value)) updateSettings({ worktreeSubmodules: value }); |
| 368 | }} |
| 369 | > |
| 370 | <SelectTrigger size="sm" aria-label="Worktree submodules"> |
| 371 | <SelectValue> |
| 372 | {(value: string | null) => |
| 373 | isWorktreeSubmodules(value) |
| 374 | ? WORKTREE_SUBMODULES_LABELS[value] |
| 375 | : unavailable |
| 376 | ? "Unavailable" |
| 377 | : "Mixed" |
| 378 | } |
| 379 | </SelectValue> |
| 380 | </SelectTrigger> |
| 381 | <SelectPopup align="end" alignItemWithTrigger={false}> |
| 382 | {WORKTREE_SUBMODULES_OPTIONS.map((option) => ( |
| 383 | <SelectItem key={option} value={option}> |
| 384 | {WORKTREE_SUBMODULES_LABELS[option]} |
| 385 | </SelectItem> |
| 386 | ))} |
| 387 | </SelectPopup> |
| 388 | </Select> |
| 389 | } |
| 390 | /> |
| 391 | </> |
| 392 | ) : category === "source-control" ? ( |
| 393 | <> |
| 394 | <SettingsRow |
| 395 | serverScoped |
| 396 | settingKeys={["defaultAutoPull"]} |
| 397 | mixed={mixedAutoPull} |
| 398 | id="automatic-pull" |
| 399 | title="Automatically pull" |
| 400 | description={ |
| 401 | isProjectScope |
| 402 | ? "Keeps this project's default branch current when the checkout has no local changes or commits." |
| 403 | : "Keeps the default branch current when the checkout has no local changes or commits. Projects can override it." |
| 404 | } |
| 405 | resetAction={ |
| 406 | settings.defaultAutoPull ? ( |
| 407 | <SettingResetButton |
| 408 | label="default automatic pull" |
| 409 | tooltip="Reset automatic pull to off" |
| 410 | onClick={() => updateSettings({ defaultAutoPull: false })} |
| 411 | /> |
| 412 | ) : null |
| 413 | } |
| 414 | control={ |
| 415 | <Switch |
| 416 | aria-label="Default automatic pull" |
| 417 | mixed={mixedAutoPull} |
| 418 | checked={mixedAutoPull ? false : settings.defaultAutoPull} |
| 419 | onCheckedChange={(enabled) => updateSettings({ defaultAutoPull: enabled })} |
| 420 | /> |
| 421 | } |
| 422 | /> |
| 423 | <SettingsRow |
| 424 | serverScoped |
| 425 | settingKeys={["removeAgentCreditsOnMerge"]} |
| 426 | mixed={mixedAgentCredits} |
| 427 | {...searchableSetting("remove-agent-credits-on-merge")} |
| 428 | description="Remove recognized agent credit lines from GitHub merge and squash messages, keeping human co-authors. Includes auto-merge. Excludes merge queues, stack merges, and existing commits." |
| 429 | resetAction={ |
| 430 | settings.removeAgentCreditsOnMerge ? ( |
| 431 | <SettingResetButton |
| 432 | label="agent credit removal" |
| 433 | tooltip="Keep agent credits" |
| 434 | onClick={() => updateSettings({ removeAgentCreditsOnMerge: false })} |
| 435 | /> |
| 436 | ) : null |
| 437 | } |
| 438 | control={ |
| 439 | <Switch |
| 440 | aria-label="Remove agent credits when merging" |
| 441 | mixed={mixedAgentCredits} |
| 442 | checked={mixedAgentCredits ? false : settings.removeAgentCreditsOnMerge} |
| 443 | onCheckedChange={(enabled) => |
| 444 | updateSettings({ removeAgentCreditsOnMerge: enabled }) |
| 445 | } |
| 446 | /> |
| 447 | } |
| 448 | /> |
| 449 | <SettingsRow |
| 450 | serverScoped |
| 451 | settingKeys={["pullRequestMergeMethod"]} |
| 452 | mixed={mixedMergeMethod} |
| 453 | {...searchableSetting("pull-request-merge-method")} |
| 454 | description={ |
| 455 | isProjectScope |
| 456 | ? "Pull requests in this project start with this method." |
| 457 | : "Pull requests start with this method. Last selected reuses whatever you chose most recently on this device." |
| 458 | } |
| 459 | resetAction={ |
| 460 | settings.pullRequestMergeMethod !== null ? ( |
| 461 | <SettingResetButton |
| 462 | label="default merge method" |
| 463 | tooltip="Reset to last selected" |
| 464 | onClick={() => updateSettings({ pullRequestMergeMethod: null })} |
| 465 | /> |
| 466 | ) : null |
| 467 | } |
| 468 | control={ |
| 469 | <Select |
| 470 | value={mixedMergeMethod ? null : (settings.pullRequestMergeMethod ?? "last")} |
| 471 | onValueChange={(value) => { |
| 472 | if (value === "last") updateSettings({ pullRequestMergeMethod: null }); |
| 473 | else if (value === "merge" || value === "squash" || value === "rebase") |
| 474 | updateSettings({ pullRequestMergeMethod: value }); |
| 475 | }} |
| 476 | > |
| 477 | <SelectTrigger size="sm" aria-label="Default pull request merge method"> |
| 478 | <SelectValue> |
| 479 | {(value: string | null) => |
| 480 | value === "merge" || value === "squash" || value === "rebase" |
| 481 | ? PULL_REQUEST_MERGE_METHOD_LABELS[value] |
| 482 | : value === "last" |
| 483 | ? "Last selected" |
| 484 | : "Mixed" |
| 485 | } |
| 486 | </SelectValue> |
| 487 | </SelectTrigger> |
| 488 | <SelectPopup align="end" alignItemWithTrigger={false}> |
| 489 | <SelectItem value="last">Last selected</SelectItem> |
| 490 | <SelectItem value="merge">{PULL_REQUEST_MERGE_METHOD_LABELS.merge}</SelectItem> |
| 491 | <SelectItem value="squash">{PULL_REQUEST_MERGE_METHOD_LABELS.squash}</SelectItem> |
| 492 | <SelectItem value="rebase">{PULL_REQUEST_MERGE_METHOD_LABELS.rebase}</SelectItem> |
| 493 | </SelectPopup> |
| 494 | </Select> |
| 495 | } |
| 496 | /> |
| 497 | </> |
| 498 | ) : ( |
| 499 | <> |
| 500 | <SettingsRow |
| 501 | serverScoped |
| 502 | settingKeys={["enableAgentBrowserAccess"]} |
| 503 | mixed={mixedBrowser} |
| 504 | id={searchableSetting("agent-browser-access").id} |
| 505 | title="Agent browser access" |
| 506 | description={ |
| 507 | isProjectScope |
| 508 | ? "Allow agents in this project to use the shared browser. Applies when the agent session next starts." |
| 509 | : "Allow agents to use the shared browser. Projects can override it." |
| 510 | } |
| 511 | resetAction={ |
| 512 | settings.enableAgentBrowserAccess !== |
| 513 | DEFAULT_SERVER_SETTINGS.enableAgentBrowserAccess ? ( |
| 514 | <SettingResetButton |
| 515 | label="default browser access" |
| 516 | onClick={() => |
| 517 | updateSettings({ |
| 518 | enableAgentBrowserAccess: DEFAULT_SERVER_SETTINGS.enableAgentBrowserAccess, |
| 519 | }) |
| 520 | } |
| 521 | /> |
| 522 | ) : null |
| 523 | } |
| 524 | control={ |
| 525 | <Switch |
| 526 | aria-label="Agent browser access" |
| 527 | mixed={mixedBrowser} |
| 528 | checked={mixedBrowser ? false : settings.enableAgentBrowserAccess} |
| 529 | onCheckedChange={(enabled) => updateSettings({ enableAgentBrowserAccess: enabled })} |
| 530 | /> |
| 531 | } |
| 532 | /> |
| 533 | </> |
| 534 | )} |
| 535 | </SettingsSection> |
| 536 | ); |
| 537 | } |