Files
goodbuddy/src/renderer/src/SettingsPanel.tsx
T

4316 lines
160 KiB
TypeScript

import {
Copy,
KeyRound,
LockKeyhole,
Plus,
Save,
SunMoon,
TerminalSquare,
Trash2,
X
} from 'lucide-react'
import {
Fragment,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
memo
} from 'react'
import { useTranslation } from 'react-i18next'
import { InlineHelp } from './InlineHelp'
import { createPortal } from 'react-dom'
import { activateModalFocus, trapTabFocus } from './dialog-focus'
import type {
AssistantExpert,
AssistantProject,
ProjectCreateInput,
ProjectChannel
} from '../../shared/assistant-contracts'
import type {
AgentRuntimeDetection,
ContextCompressionSettings,
RuntimeConfigActionInput,
RuntimeFileSelectionKind,
RuntimeSettings,
RuntimeSettingsInput,
RuntimeModelSource
} from '../../shared/contracts'
import {
defaultContextCompressionSettings,
defaultAnthropicMaximumOutputTokens,
builtinEmbeddingConnectionId,
defaultModelProfileId as builtInDefaultModelProfileId,
defaultRuntimeSettings,
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile,
MODEL_REQUEST_CUSTOMIZATION_LIMITS,
modelConnectionUrlSchema,
modelRequestBodySchema,
modelRequestHeadersSchema
} from '../../shared/contracts'
import { RolePromptSettingsSection } from './RolePromptSettingsSection'
import { CapabilitiesAndToolsSettingsSection } from './CapabilitiesAndToolsSettingsSection'
import { ChannelSettingsSection } from './ChannelSettingsSection'
import { UpdateSettingsSection } from './UpdateSettingsSection'
import { PlatformFeaturesSettingsSection } from './PlatformFeaturesSettingsSection'
import { ModelDownloadSourceSettings } from './ModelDownloadSourceSettings'
import type { ModelDownloadSource } from '../../shared/application-settings-contracts'
import { SpeechModelSettingsSection } from './SpeechModelSettingsSection'
import { DocumentParsingSettingsSection } from './DocumentParsingSettingsSection'
import { DshMarketplaceSection } from './DshMarketplaceSection'
import { displayErrorMessage } from './error-message'
import { SshHostsSettingsSection } from './SshHostsSettingsSection'
import {
RuntimeCustomizationSection,
type RuntimeCustomizationSectionHandle
} from './RuntimeCustomizationSection'
import { PageHeader, SegmentedControl } from './WorkspacePrimitives'
import {
SettingsCategoryHeader,
SettingsWarningList
} from './SettingsPrimitives'
import {
getSettingsCategoryList,
type SettingsCategoryId
} from './settings-categories'
import type { AppearanceTheme } from './theme'
import type { AppNotificationInput } from './notifications'
import type {
EmbeddingDiagnosticResult,
EmbeddingModelSnapshot,
EmbeddingSettingsSnapshot
} from '../../shared/embedding-contracts'
import { EmbeddingSettingsSection } from './EmbeddingSettingsSection'
import { useUiLocale } from './i18n/UiLocaleProvider'
import type { GlobalShortcutSettingsSnapshot } from '../../shared/shortcut'
import { findPreferredCompatibleModelProfile } from './model-profile-selection'
import { BrandingSettingsSection } from './BrandingSettingsSection'
import {
defaultBrandingPreferences,
type BrandingPreferences
} from './branding'
type ModelType = 'llm' | 'embedding' | 'rerank' | 'speech'
type AgentRuntimeType =
| RuntimeConfigActionInput['runtime']
| 'deepseek-harness'
type ModelProfileDraft = Omit<
RuntimeSettings['modelProfiles'][number],
'requestHeaders' | 'requestBody'
> & {
supportsImageInput: boolean
requestHeadersText: string
requestBodyText: string
apiKey: string
clearApiKey: boolean
copyApiKeyFromProfileId?: string
}
type EmbeddingConnectionDraft =
Omit<
Exclude<
NonNullable<RuntimeSettings['embeddingConnections']>[number],
{ kind: 'builtin' }
>,
'apiKey'
> & {
apiKey: string
clearApiKey: boolean
}
function toEmbeddingConnectionDrafts(
settings: RuntimeSettings
): EmbeddingConnectionDraft[] {
return (settings.embeddingConnections ?? [])
.filter(
(
connection
): connection is Exclude<
NonNullable<RuntimeSettings['embeddingConnections']>[number],
{ kind: 'builtin' }
> => connection.kind === 'openai-compatible'
)
.map((connection) => ({
...connection,
apiKey: '',
clearApiKey: false
}))
}
const contextCompressionTokenScale = 1_000
const minimumContextCompressionTriggerThousands = 8
const maximumContextCompressionTriggerThousands = 1_000
const minimumContextCompressionRecentRawThousands = 4
const maximumContextCompressionRecentRawThousands = 256
type ContextCompressionTokenDrafts = {
trigger: string
recentRaw: string
}
function contextCompressionTokenDrafts(
settings: ContextCompressionSettings
): ContextCompressionTokenDrafts {
return {
trigger: String(
settings.triggerTokens / contextCompressionTokenScale
),
recentRaw: String(
settings.recentRawTokens / contextCompressionTokenScale
)
}
}
function parseContextCompressionTokenDraft(
value: string
): number | undefined {
if (!value.trim()) {
return undefined
}
const parsed = Number(value)
return Number.isFinite(parsed)
? Math.round(parsed * contextCompressionTokenScale)
: undefined
}
function maximumContextCompressionRecentRawThousandsFor(
triggerTokens: number
): number {
return Math.max(
minimumContextCompressionRecentRawThousands,
Math.min(
maximumContextCompressionRecentRawThousands,
triggerTokens / contextCompressionTokenScale - 1
)
)
}
function normalizeContextCompressionTokenDrafts(
settings: ContextCompressionSettings,
drafts: ContextCompressionTokenDrafts
): ContextCompressionSettings {
const triggerTokens = Math.min(
maximumContextCompressionTriggerThousands *
contextCompressionTokenScale,
Math.max(
minimumContextCompressionTriggerThousands *
contextCompressionTokenScale,
parseContextCompressionTokenDraft(drafts.trigger) ??
settings.triggerTokens
)
)
const maximumRecentRawTokens =
maximumContextCompressionRecentRawThousandsFor(triggerTokens) *
contextCompressionTokenScale
const recentRawTokens = Math.min(
maximumRecentRawTokens,
Math.max(
minimumContextCompressionRecentRawThousands *
contextCompressionTokenScale,
parseContextCompressionTokenDraft(drafts.recentRaw) ??
settings.recentRawTokens
)
)
return {
...settings,
triggerTokens,
recentRawTokens
}
}
export type SettingsLeaveRequester = (proceed: () => void) => void
type SettingsPanelProps = {
open: boolean
presentation?: 'modal' | 'page'
initialCategory?: SettingsCategoryId
initialChannel?: ProjectChannel
onClose: () => void
onProjectsDeleted?: (projectIds: string[]) => void
onSaved: (settings: RuntimeSettings) => void
onNotify?: (notification: AppNotificationInput) => void
onUpdateProject: (
projectId: string,
input: ProjectCreateInput
) => Promise<AssistantProject>
projects: AssistantProject[]
onExpertsChanged?: (experts: AssistantExpert[]) => void
onClearLocalData: () => Promise<void>
appearanceTheme?: AppearanceTheme
transparentFrostedEffectEnabled?: boolean
applicationSettingsPending?: boolean
applicationSettingsLocked?: boolean
applicationSettingsError?: string
onTransparentFrostedEffectEnabledChange?: (enabled: boolean) => Promise<boolean>
onRetryApplicationSettings?: () => void
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
brandingPreferences?: BrandingPreferences
brandingFallbackLogo?: string
onBrandingPreferencesChange?: (
preferences: BrandingPreferences
) => boolean
magicNotesEnabled?: boolean
remoteProjectsEnabled?: boolean
onConversationHtmlRenderingEnabledChange?: (
enabled: boolean
) => void
onRemoteProjectsEnabledChange?: (enabled: boolean) => void
onShortcutSettingsChanged?: (
snapshot: GlobalShortcutSettingsSnapshot
) => void
onLeaveRequestReady?: (
requester: SettingsLeaveRequester | undefined
) => void
}
function settingsErrorMessage(reason: unknown, fallback: string): string {
const message = displayErrorMessage(reason, fallback)
try {
const issues: unknown = JSON.parse(message)
if (
Array.isArray(issues) &&
typeof issues[0] === 'object' &&
issues[0] !== null &&
'message' in issues[0] &&
typeof issues[0].message === 'string'
) {
return issues[0].message
}
} catch {
// Keep provider and local errors as-is when they are not validation JSON.
}
return message
}
function toModelProfileDrafts(
settings: RuntimeSettings
): ModelProfileDraft[] {
return settings.modelProfiles.map(
({ requestHeaders, requestBody, ...profile }) => ({
...profile,
supportsImageInput: profile.supportsImageInput ?? false,
allowConversationInvocation: profile.allowConversationInvocation ?? false,
maximumOutputTokens:
profile.maximumOutputTokens ??
defaultAnthropicMaximumOutputTokens,
requestHeadersText: JSON.stringify(
requestHeaders ?? {},
null,
2
),
requestBodyText: JSON.stringify(requestBody ?? {}, null, 2),
apiKey: '',
clearApiKey: false
})
)
}
const maximumRequestHeadersDraftCharacters =
MODEL_REQUEST_CUSTOMIZATION_LIMITS.maximumHeaders *
(MODEL_REQUEST_CUSTOMIZATION_LIMITS.maximumHeaderNameLength +
MODEL_REQUEST_CUSTOMIZATION_LIMITS.maximumHeaderValueLength *
2 +
16) +
2
const maximumRequestBodyDraftCharacters =
MODEL_REQUEST_CUSTOMIZATION_LIMITS.maximumBodyBytes * 4
function parseRequestHeadersDraft(value: string) {
if (value.length > maximumRequestHeadersDraftCharacters) {
return { success: false } as const
}
try {
return modelRequestHeadersSchema.safeParse(JSON.parse(value))
} catch {
return { success: false } as const
}
}
function parseRequestBodyDraft(value: string) {
if (value.length > maximumRequestBodyDraftCharacters) {
return { success: false } as const
}
try {
return modelRequestBodySchema.safeParse(JSON.parse(value))
} catch {
return { success: false } as const
}
}
function configuredRuntimeSettings(
settings: RuntimeSettings
): NonNullable<RuntimeSettings['configured']> {
return settings.configured ?? {
modelProfiles: settings.modelProfiles,
opencodeBaseUrl: settings.opencodeBaseUrl,
opencodeBinaryPath: settings.opencodeBinaryPath,
opencodeConfigPath: settings.opencodeConfigPath,
continueBinaryPath: settings.continueBinaryPath,
continueConfigPath: settings.continueConfigPath,
workspacePath: settings.workspacePath,
opencodeModelSource: settings.opencodeModelSource,
continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource:
settings.deepseekHarnessModelSource
}
}
type RuntimeDraftSelection =
| string
| ((selectedId: string) => string)
function hydrateRuntimeSettings(
value: RuntimeSettings,
setters: {
settings: (value: RuntimeSettings) => void
provider: (value: RuntimeSettings['provider']) => void
modelProfiles: (value: ModelProfileDraft[]) => void
selectedModelProfileId: (value: RuntimeDraftSelection) => void
defaultModelProfileId: (value: string) => void
defaultImageModelProfileId: (value: string | null) => void
opencodeModelSource: (value: RuntimeModelSource) => void
continueModelSource: (value: RuntimeModelSource) => void
deepseekHarnessModelSource: (value: RuntimeModelSource) => void
opencodeBaseUrl: (value: string) => void
opencodeBinaryPath: (value: string) => void
opencodeConfigPath: (value: string) => void
continueBinaryPath: (value: string) => void
continueConfigPath: (value: string) => void
continueMode: (value: RuntimeSettings['continueMode']) => void
knowledgeEmbeddingEnabled: (value: boolean) => void
knowledgeEmbeddingBaseUrl: (value: string) => void
knowledgeEmbeddingModel: (value: string) => void
knowledgeEmbeddingApiKey: (value: string) => void
clearKnowledgeEmbeddingApiKey: (value: boolean) => void
embeddingConnections: (value: EmbeddingConnectionDraft[]) => void
activeEmbeddingConnectionId: (value: string) => void
knowledgeRerankEnabled: (value: boolean) => void
knowledgeRerankEndpoint: (value: string) => void
knowledgeRerankModel: (value: string) => void
knowledgeRerankApiKey: (value: string) => void
clearKnowledgeRerankApiKey: (value: boolean) => void
workspacePath: (value: string) => void
toolApproval: (value: RuntimeSettingsInput['toolApproval']) => void
subagentSmartRoutingEnabled: (value: boolean) => void
contextCompression: (value: ContextCompressionSettings) => void
},
preserveSelectedProfile = false
): void {
const configured = configuredRuntimeSettings(value)
setters.settings(value)
setters.provider(value.provider)
setters.modelProfiles(toModelProfileDrafts(value))
const fallbackProfileId = value.modelProfiles.some(
(profile) => profile.id === value.defaultModelProfileId
)
? value.defaultModelProfileId
: value.modelProfiles[0]?.id ?? ''
setters.selectedModelProfileId(
preserveSelectedProfile
? (selectedId) =>
value.modelProfiles.some(
(profile) => profile.id === selectedId
)
? selectedId
: fallbackProfileId
: fallbackProfileId
)
setters.defaultModelProfileId(value.defaultModelProfileId)
setters.defaultImageModelProfileId(value.defaultImageModelProfileId ?? null)
setters.opencodeModelSource(configured.opencodeModelSource)
setters.continueModelSource(configured.continueModelSource)
setters.deepseekHarnessModelSource(
configured.deepseekHarnessModelSource ?? { kind: 'platform' }
)
setters.opencodeBaseUrl(configured.opencodeBaseUrl)
setters.opencodeBinaryPath(configured.opencodeBinaryPath)
setters.opencodeConfigPath(configured.opencodeConfigPath)
setters.continueBinaryPath(configured.continueBinaryPath)
setters.continueConfigPath(configured.continueConfigPath)
setters.continueMode(value.continueMode)
setters.knowledgeEmbeddingEnabled(value.knowledgeEmbeddingEnabled)
setters.knowledgeEmbeddingBaseUrl(value.knowledgeEmbeddingBaseUrl)
setters.knowledgeEmbeddingModel(value.knowledgeEmbeddingModel)
setters.knowledgeEmbeddingApiKey('')
setters.clearKnowledgeEmbeddingApiKey(false)
setters.embeddingConnections(toEmbeddingConnectionDrafts(value))
setters.activeEmbeddingConnectionId(
value.activeEmbeddingConnectionId ?? builtinEmbeddingConnectionId
)
setters.knowledgeRerankEnabled(
value.knowledgeRerankEnabled ??
defaultRuntimeSettings.knowledgeRerankEnabled
)
setters.knowledgeRerankEndpoint(
value.knowledgeRerankEndpoint ??
defaultRuntimeSettings.knowledgeRerankEndpoint
)
setters.knowledgeRerankModel(
value.knowledgeRerankModel ??
defaultRuntimeSettings.knowledgeRerankModel
)
setters.knowledgeRerankApiKey('')
setters.clearKnowledgeRerankApiKey(false)
setters.workspacePath(configured.workspacePath)
setters.toolApproval(
value.toolApproval === 'policy' ? 'policy' : 'always'
)
setters.subagentSmartRoutingEnabled(
value.subagentSmartRoutingEnabled
)
setters.contextCompression(
value.contextCompression ?? defaultContextCompressionSettings
)
}
type RuntimeConfigCardProps = {
runtime: RuntimeConfigActionInput['runtime']
runtimeLabel: string
description: string
fileKind: Extract<
RuntimeFileSelectionKind,
'opencodeConfig' | 'continueConfig'
>
path: string
savedPath?: string
onPathChange: (value: string) => void
onSelect: (
kind: RuntimeFileSelectionKind,
setValue: (value: string) => void
) => Promise<void>
onOpen: (
runtime: RuntimeConfigActionInput['runtime'],
action: RuntimeConfigActionInput['action']
) => Promise<void>
}
function RuntimeConfigCard({
runtime,
runtimeLabel,
description,
fileKind,
path,
savedPath,
onPathChange,
onSelect,
onOpen
}: RuntimeConfigCardProps): React.JSX.Element {
const { t } = useTranslation('settings')
const saved = Boolean(path) && savedPath === path
return (
<div className="runtime-config-card">
<div>
<strong>
{t('runtime.configCard.title', { runtime: runtimeLabel })}
</strong>
<small>{description}</small>
</div>
<label className="field">
<span>{t('runtime.configCard.fileLabel')}</span>
<div className="workspace-picker">
<input
aria-label={t('runtime.configCard.pathAriaLabel', {
runtime: runtimeLabel
})}
onChange={(event) => onPathChange(event.target.value)}
placeholder={t('runtime.configCard.pathPlaceholder', {
runtime: runtimeLabel
})}
value={path}
/>
<button
className="secondary-button"
onClick={() => void onSelect(fileKind, onPathChange)}
type="button"
>
{t('actions.selectFile')}
</button>
<button
className="secondary-button"
disabled={!path}
onClick={() => onPathChange('')}
type="button"
>
{t('actions.clear')}
</button>
</div>
</label>
<div className="runtime-config-actions">
{saved ? (
<>
<button
className="secondary-button"
onClick={() => void onOpen(runtime, 'open-file')}
type="button"
>
{t('actions.openConfigFile')}
</button>
<button
className="secondary-button"
onClick={() => void onOpen(runtime, 'show-file')}
type="button"
>
{t('actions.revealInFolder')}
</button>
</>
) : (
<button
className="secondary-button"
onClick={() => void onOpen(runtime, 'open-directory')}
type="button"
>
{t('actions.openConfigDirectory', {
runtime: runtimeLabel
})}
</button>
)}
</div>
{path && !saved && (
<small className="runtime-config-card__hint">
{t('runtime.configCard.unsavedHint')}
</small>
)}
</div>
)
}
function RuntimeOverviewCard({
detection,
detectionLabel,
detecting,
modelConfiguration,
recommendation,
runtime
}: {
detection: AgentRuntimeDetection['opencode'] | undefined
detectionLabel: string
detecting: boolean
modelConfiguration: string
recommendation: string
runtime: string
}): React.JSX.Element {
const { t } = useTranslation('settings')
const localizedDetail =
detection?.available && detection.source
? t(`runtime.detection.details.${detection.source}`, {
runtime: detectionLabel,
versionSuffix: detection.version ? ` ${detection.version}` : ''
})
: detection?.detail
const status = detecting
? t('runtime.detection.detecting')
: detection?.available
? t('runtime.detection.ready')
: detection
? t('runtime.detection.unavailable')
: t('runtime.detection.notDetected')
return (
<div className="runtime-note runtime-overview">
<dl className="runtime-overview__details">
<dt>{t('runtime.runtimeLabel')}</dt>
<dd>{runtime}</dd>
<dt>{t('runtime.modelConfigurationLabel')}</dt>
<dd>{modelConfiguration}</dd>
<dt>{t('runtime.detection.statusLabel')}</dt>
<dd aria-atomic="true" aria-live="polite">
{status}
</dd>
{!detecting && detection?.available && (
<>
<dt>{t('runtime.detection.pathLabel')}</dt>
<dd className="runtime-overview__path">
{detection.path}
</dd>
{detection.version && (
<>
<dt>{t('runtime.detection.versionLabel')}</dt>
<dd>{detection.version}</dd>
</>
)}
</>
)}
{!detecting && localizedDetail && (
<>
<dt>{t('runtime.detection.detailLabel')}</dt>
<dd>{localizedDetail}</dd>
</>
)}
</dl>
<p>{recommendation}</p>
</div>
)
}
function SettingsPanelView({
open,
presentation = 'modal',
initialCategory,
initialChannel,
onClose,
onProjectsDeleted = () => {},
onSaved,
onNotify = () => {},
onUpdateProject,
projects,
onClearLocalData,
onExpertsChanged = () => {},
appearanceTheme = 'system',
transparentFrostedEffectEnabled = true,
applicationSettingsPending = false,
applicationSettingsLocked = false,
applicationSettingsError,
onTransparentFrostedEffectEnabledChange,
onRetryApplicationSettings,
onAppearanceThemeChange = () => {},
brandingPreferences = defaultBrandingPreferences,
brandingFallbackLogo,
onBrandingPreferencesChange = () => false,
magicNotesEnabled = false,
remoteProjectsEnabled = false,
onConversationHtmlRenderingEnabledChange = () => {},
onRemoteProjectsEnabledChange = () => {},
onShortcutSettingsChanged = () => {},
onLeaveRequestReady = () => {}
}: SettingsPanelProps): React.JSX.Element | null {
const { i18n, t } = useTranslation('settings')
const {
preference: uiLocalePreference,
setPreference: setUiLocalePreference
} = useUiLocale()
const [settings, setSettings] = useState<RuntimeSettings>()
const [brandingDirty, setBrandingDirty] = useState(false)
const [provider, setProvider] =
useState<RuntimeSettingsInput['provider']>(
defaultRuntimeSettings.provider
)
const [modelProfiles, setModelProfiles] = useState<ModelProfileDraft[]>([])
const [modelValidationAttempted, setModelValidationAttempted] = useState(false)
const [selectedModelProfileId, setSelectedModelProfileId] =
useState('')
const [defaultModelProfileId, setDefaultModelProfileId] = useState('')
const [defaultImageModelProfileId, setDefaultImageModelProfileId] =
useState<string | null>(null)
const [opencodeModelSource, setOpencodeModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [continueModelSource, setContinueModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [deepseekHarnessModelSource, setDeepseekHarnessModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(
defaultRuntimeSettings.opencodeBaseUrl
)
const [opencodeBinaryPath, setOpencodeBinaryPath] = useState<string>(
defaultRuntimeSettings.opencodeBinaryPath
)
const [opencodeConfigPath, setOpencodeConfigPath] = useState<string>(
defaultRuntimeSettings.opencodeConfigPath
)
const [continueBinaryPath, setContinueBinaryPath] = useState<string>(
defaultRuntimeSettings.continueBinaryPath
)
const [continueConfigPath, setContinueConfigPath] = useState<string>(
defaultRuntimeSettings.continueConfigPath
)
const [continueMode, setContinueMode] =
useState<RuntimeSettingsInput['continueMode']>(
defaultRuntimeSettings.continueMode
)
const [knowledgeEmbeddingEnabled, setKnowledgeEmbeddingEnabled] =
useState<boolean>(defaultRuntimeSettings.knowledgeEmbeddingEnabled)
const [knowledgeEmbeddingBaseUrl, setKnowledgeEmbeddingBaseUrl] =
useState<string>(defaultRuntimeSettings.knowledgeEmbeddingBaseUrl)
const [knowledgeEmbeddingModel, setKnowledgeEmbeddingModel] =
useState<string>(defaultRuntimeSettings.knowledgeEmbeddingModel)
const [knowledgeEmbeddingApiKey, setKnowledgeEmbeddingApiKey] =
useState('')
const [
clearKnowledgeEmbeddingApiKey,
setClearKnowledgeEmbeddingApiKey
] = useState(false)
const [embeddingConnections, setEmbeddingConnections] = useState<
EmbeddingConnectionDraft[]
>([])
const [
activeEmbeddingConnectionId,
setActiveEmbeddingConnectionId
] = useState(builtinEmbeddingConnectionId)
const [editingEmbeddingConnectionId, setEditingEmbeddingConnectionId] =
useState<string>()
const [knowledgeRerankEnabled, setKnowledgeRerankEnabled] =
useState<boolean>(defaultRuntimeSettings.knowledgeRerankEnabled)
const [knowledgeRerankEndpoint, setKnowledgeRerankEndpoint] =
useState<string>(defaultRuntimeSettings.knowledgeRerankEndpoint)
const [knowledgeRerankModel, setKnowledgeRerankModel] =
useState<string>(defaultRuntimeSettings.knowledgeRerankModel)
const [knowledgeRerankApiKey, setKnowledgeRerankApiKey] =
useState('')
const [clearKnowledgeRerankApiKey, setClearKnowledgeRerankApiKey] =
useState(false)
const [workspacePath, setWorkspacePath] = useState<string>(
defaultRuntimeSettings.workspacePath
)
const [toolApproval, setToolApproval] =
useState<RuntimeSettingsInput['toolApproval']>(
defaultRuntimeSettings.toolApproval
)
const [
subagentSmartRoutingEnabled,
setSubagentSmartRoutingEnabled
] = useState(false)
const [contextCompression, setContextCompression] =
useState<ContextCompressionSettings>(
defaultContextCompressionSettings
)
const [
contextCompressionTokenInput,
setContextCompressionTokenInput
] = useState<ContextCompressionTokenDrafts>(() =>
contextCompressionTokenDrafts(
defaultContextCompressionSettings
)
)
const modelProfileDisplayName = (
profile: Pick<ModelProfileDraft, 'id' | 'name'>
): string =>
profile.id === builtInDefaultModelProfileId &&
profile.name === '默认模型'
? t('model.profile.seededDefaultName')
: profile.name
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [embeddingSnapshot, setEmbeddingSnapshot] =
useState<EmbeddingSettingsSnapshot>()
const [embeddingModelSnapshot, setEmbeddingModels] =
useState<EmbeddingModelSnapshot>()
const [embeddingDiagnostic, setEmbeddingDiagnostic] =
useState<EmbeddingDiagnosticResult>()
const [embeddingDiagnosticRunning, setEmbeddingDiagnosticRunning] =
useState(false)
const [embeddingModelActionRunning, setEmbeddingModelActionRunning] =
useState(false)
const [embeddingModelPendingOperation, setEmbeddingModelPendingOperation] =
useState<{
modelId: string
kind: EmbeddingModelSnapshot['operations'][number]['kind']
}>()
const [error, setError] = useState<string>()
const [confirmingClear, setConfirmingClear] = useState(false)
const [clearingLocalData, setClearingLocalData] = useState(false)
const [detection, setDetection] = useState<AgentRuntimeDetection>()
const [detecting, setDetecting] = useState(false)
const [selectedTab, setActiveTab] =
useState<SettingsCategoryId>(
initialCategory === 'ssh-hosts' && !remoteProjectsEnabled
? 'platform-features'
: initialCategory ?? 'runtime'
)
const activeTab =
selectedTab === 'ssh-hosts' && !remoteProjectsEnabled
? 'platform-features'
: selectedTab
const [modelType, setModelType] = useState<ModelType>('llm')
const [modelDownloadSourceOpen, setModelDownloadSourceOpen] = useState(false)
const [modelDownloadSource, setModelDownloadSource] = useState<ModelDownloadSource>()
const embeddingModels = embeddingModelSnapshot && modelDownloadSource
? { ...embeddingModelSnapshot, selectedDownloadSource: modelDownloadSource }
: embeddingModelSnapshot
const [speechModelDraftId, setSpeechModelDraftId] = useState<
string | null | undefined
>()
const [
persistedSpeechModelId,
setPersistedSpeechModelId
] = useState<string | null | undefined>()
const [speechModelSelectionDirty, setSpeechModelSelectionDirty] =
useState(false)
const [agentRuntimeType, setAgentRuntimeType] =
useState<AgentRuntimeType>('opencode')
const [runtimeCustomizationDirty, setRuntimeCustomizationDirty] =
useState(false)
const [platformFeaturesDirty, setPlatformFeaturesDirty] =
useState(false)
const [documentParsingDirty, setDocumentParsingDirty] =
useState(false)
const [sshHostsDirty, setSshHostsDirty] = useState(false)
const [pendingLeave, setPendingLeave] = useState<
| { kind: 'close' }
| { kind: 'navigate'; category: SettingsCategoryId; openModelDownloadSource?: boolean }
| { kind: 'external'; proceed: () => void }
>()
const runtimeCustomizationRef =
useRef<RuntimeCustomizationSectionHandle>(null)
const keepEditingRef = useRef<HTMLButtonElement>(null)
const dialogRef = useRef<HTMLElement>(null)
const closeRef = useRef<HTMLButtonElement>(null)
const leaveTriggerRef = useRef<HTMLElement | undefined>(undefined)
const handleRuntimeCustomizationDirtyChange = useCallback(
(dirty: boolean): void => {
setRuntimeCustomizationDirty(dirty)
if (!dirty) {
setError((current) =>
current === t('runtime.customization.unsavedClose')
? undefined
: current
)
}
},
[t]
)
const settingsBodyRef = useRef<HTMLDivElement>(null)
const hydrateSettings = useCallback(
(
value: RuntimeSettings,
preserveSelectedProfile = false
): void => {
setModelValidationAttempted(false)
hydrateRuntimeSettings(
value,
{
settings: setSettings,
provider: setProvider,
modelProfiles: setModelProfiles,
selectedModelProfileId: setSelectedModelProfileId,
defaultModelProfileId: setDefaultModelProfileId,
defaultImageModelProfileId: setDefaultImageModelProfileId,
opencodeModelSource: setOpencodeModelSource,
continueModelSource: setContinueModelSource,
deepseekHarnessModelSource: setDeepseekHarnessModelSource,
opencodeBaseUrl: setOpencodeBaseUrl,
opencodeBinaryPath: setOpencodeBinaryPath,
opencodeConfigPath: setOpencodeConfigPath,
continueBinaryPath: setContinueBinaryPath,
continueConfigPath: setContinueConfigPath,
continueMode: setContinueMode,
knowledgeEmbeddingEnabled: setKnowledgeEmbeddingEnabled,
knowledgeEmbeddingBaseUrl: setKnowledgeEmbeddingBaseUrl,
knowledgeEmbeddingModel: setKnowledgeEmbeddingModel,
knowledgeEmbeddingApiKey: setKnowledgeEmbeddingApiKey,
clearKnowledgeEmbeddingApiKey: setClearKnowledgeEmbeddingApiKey,
embeddingConnections: setEmbeddingConnections,
activeEmbeddingConnectionId:
setActiveEmbeddingConnectionId,
knowledgeRerankEnabled: setKnowledgeRerankEnabled,
knowledgeRerankEndpoint: setKnowledgeRerankEndpoint,
knowledgeRerankModel: setKnowledgeRerankModel,
knowledgeRerankApiKey: setKnowledgeRerankApiKey,
clearKnowledgeRerankApiKey: setClearKnowledgeRerankApiKey,
workspacePath: setWorkspacePath,
toolApproval: setToolApproval,
subagentSmartRoutingEnabled: setSubagentSmartRoutingEnabled,
contextCompression: setContextCompression
},
preserveSelectedProfile
)
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
value.contextCompression ??
defaultContextCompressionSettings
)
)
},
[]
)
const configurationTab =
activeTab === 'model' ||
activeTab === 'context-control' ||
activeTab === 'runtime' ||
activeTab === 'security' ||
activeTab === 'roles'
const categoryRendersOwnHeader =
activeTab === 'platform-features' ||
activeTab === 'document-parsing' ||
activeTab === 'ssh-hosts' ||
activeTab === 'channels' ||
activeTab === 'capabilities' ||
activeTab === 'about'
const savedConfiguredSettings = settings
? configuredRuntimeSettings(settings)
: undefined
const runtimeDraftDirty =
settings !== undefined &&
JSON.stringify({
provider,
modelProfiles,
defaultModelProfileId,
defaultImageModelProfileId,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource,
opencodeBaseUrl,
opencodeBinaryPath,
opencodeConfigPath,
continueBinaryPath,
continueConfigPath,
continueMode,
knowledgeEmbeddingEnabled,
knowledgeEmbeddingBaseUrl,
knowledgeEmbeddingModel,
knowledgeEmbeddingApiKey,
clearKnowledgeEmbeddingApiKey,
embeddingConnections,
activeEmbeddingConnectionId,
knowledgeRerankEnabled,
knowledgeRerankEndpoint,
knowledgeRerankModel,
knowledgeRerankApiKey,
clearKnowledgeRerankApiKey,
workspacePath,
toolApproval,
subagentSmartRoutingEnabled,
contextCompression,
contextCompressionTokenInput
}) !==
JSON.stringify({
provider: settings.provider,
modelProfiles: toModelProfileDrafts(settings),
defaultModelProfileId: settings.defaultModelProfileId,
defaultImageModelProfileId: settings.defaultImageModelProfileId ?? null,
opencodeModelSource:
savedConfiguredSettings?.opencodeModelSource,
continueModelSource:
savedConfiguredSettings?.continueModelSource,
deepseekHarnessModelSource:
savedConfiguredSettings?.deepseekHarnessModelSource ?? {
kind: 'platform'
},
opencodeBaseUrl: savedConfiguredSettings?.opencodeBaseUrl,
opencodeBinaryPath:
savedConfiguredSettings?.opencodeBinaryPath,
opencodeConfigPath:
savedConfiguredSettings?.opencodeConfigPath,
continueBinaryPath:
savedConfiguredSettings?.continueBinaryPath,
continueConfigPath:
savedConfiguredSettings?.continueConfigPath,
continueMode: settings.continueMode,
knowledgeEmbeddingEnabled:
settings.knowledgeEmbeddingEnabled,
knowledgeEmbeddingBaseUrl:
settings.knowledgeEmbeddingBaseUrl,
knowledgeEmbeddingModel: settings.knowledgeEmbeddingModel,
knowledgeEmbeddingApiKey: '',
clearKnowledgeEmbeddingApiKey: false,
embeddingConnections: toEmbeddingConnectionDrafts(settings),
activeEmbeddingConnectionId:
settings.activeEmbeddingConnectionId ??
builtinEmbeddingConnectionId,
knowledgeRerankEnabled:
settings.knowledgeRerankEnabled ??
defaultRuntimeSettings.knowledgeRerankEnabled,
knowledgeRerankEndpoint:
settings.knowledgeRerankEndpoint ??
defaultRuntimeSettings.knowledgeRerankEndpoint,
knowledgeRerankModel:
settings.knowledgeRerankModel ??
defaultRuntimeSettings.knowledgeRerankModel,
knowledgeRerankApiKey: '',
clearKnowledgeRerankApiKey: false,
workspacePath: savedConfiguredSettings?.workspacePath,
toolApproval:
settings.toolApproval === 'policy' ? 'policy' : 'always',
subagentSmartRoutingEnabled:
settings.subagentSmartRoutingEnabled,
contextCompression:
settings.contextCompression ??
defaultContextCompressionSettings,
contextCompressionTokenInput: contextCompressionTokenDrafts(
settings.contextCompression ??
defaultContextCompressionSettings
)
})
const hasUnsavedDrafts =
runtimeDraftDirty ||
speechModelSelectionDirty ||
runtimeCustomizationDirty ||
brandingDirty ||
platformFeaturesDirty ||
documentParsingDirty ||
sshHostsDirty
const navigationWouldLoseDraft =
runtimeCustomizationDirty ||
brandingDirty ||
platformFeaturesDirty ||
documentParsingDirty ||
sshHostsDirty
const unsavedCloseMessage =
runtimeCustomizationDirty &&
!runtimeDraftDirty &&
!speechModelSelectionDirty &&
!brandingDirty &&
!platformFeaturesDirty &&
!documentParsingDirty &&
!sshHostsDirty
? t('runtime.customization.unsavedClose')
: t('unsaved.close')
useEffect(() => {
if (!open || !hasUnsavedDrafts) {
return
}
const preventUnload = (event: BeforeUnloadEvent): void => {
event.preventDefault()
event.returnValue = ''
}
window.addEventListener('beforeunload', preventUnload)
return () =>
window.removeEventListener('beforeunload', preventUnload)
}, [hasUnsavedDrafts, open])
const requestTabChange = (
category: SettingsCategoryId,
trigger?: HTMLElement,
openModelDownloadSource = false
): boolean => {
if (category === activeTab) {
return true
}
if (navigationWouldLoseDraft) {
leaveTriggerRef.current =
trigger ??
(document.activeElement instanceof HTMLElement
? document.activeElement
: undefined)
setPendingLeave({ kind: 'navigate', category, openModelDownloadSource })
return false
}
setPendingLeave(undefined)
setError(undefined)
setActiveTab(category)
return true
}
const openModelDownloadSource = (): void => {
if (requestTabChange('model', undefined, true)) setModelDownloadSourceOpen(true)
}
const visibleSettingsCategories = getSettingsCategoryList(
remoteProjectsEnabled
)
const settingsTabs = visibleSettingsCategories.map(({ id }) => id)
const handleTabKeyDown = (
event: React.KeyboardEvent<HTMLButtonElement>,
tab: SettingsCategoryId
): void => {
const currentIndex = settingsTabs.indexOf(tab)
let nextIndex: number | undefined
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % settingsTabs.length
} else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
nextIndex =
(currentIndex - 1 + settingsTabs.length) %
settingsTabs.length
} else if (event.key === 'Home') {
nextIndex = 0
} else if (event.key === 'End') {
nextIndex = settingsTabs.length - 1
}
if (nextIndex === undefined) {
return
}
event.preventDefault()
const nextTab = settingsTabs[nextIndex]!
if (!requestTabChange(nextTab, event.currentTarget)) {
return
}
event.currentTarget.parentElement
?.querySelector<HTMLButtonElement>(
`#settings-tab-${nextTab}`
)
?.focus()
}
useEffect(() => {
if (!open) {
return
}
void window.goodbuddy.settings
.getRuntime()
.then((value) => {
setError(undefined)
setConfirmingClear(false)
setClearingLocalData(false)
setEmbeddingModelPendingOperation(undefined)
setModelType('llm')
setSpeechModelDraftId(undefined)
setPersistedSpeechModelId(undefined)
setSpeechModelSelectionDirty(false)
setPlatformFeaturesDirty(false)
setDocumentParsingDirty(false)
setSshHostsDirty(false)
setPendingLeave(undefined)
setAgentRuntimeType('opencode')
hydrateSettings(value)
})
.catch((reason: unknown) => {
setError(
settingsErrorMessage(
reason,
i18n.t('errors.readSettings', { ns: 'settings' })
)
)
})
void window.goodbuddy.settings
.detectAgentRuntimes()
.then(setDetection)
.catch((reason: unknown) => {
setError(
settingsErrorMessage(
reason,
i18n.t('errors.detectRuntimes', { ns: 'settings' })
)
)
})
}, [hydrateSettings, i18n, open])
useEffect(() => {
if (open && settingsBodyRef.current) {
settingsBodyRef.current.scrollTop = 0
}
}, [activeTab, open])
useEffect(() => {
const embeddings = window.goodbuddy.embeddings
if (!open || !embeddings) {
return
}
let active = true
void embeddings
.getSnapshot()
.then((snapshot) => {
if (active) {
setEmbeddingDiagnostic(undefined)
setEmbeddingSnapshot(snapshot)
setEmbeddingModels(snapshot.models)
}
})
.catch((reason: unknown) => {
if (active) {
setError(
settingsErrorMessage(
reason,
i18n.t('errors.readEmbeddingStatus', {
ns: 'settings'
})
)
)
}
})
return () => {
active = false
}
}, [i18n, open])
const refreshEmbeddingModelProgress =
useCallback(async (): Promise<void> => {
const embeddings = window.goodbuddy.embeddings
if (!embeddings) {
return
}
const progress = await embeddings.getModelProgress()
setEmbeddingModels((current) =>
current
? { ...current, operations: progress.operations }
: current
)
}, [])
useEffect(() => {
if (
!open ||
(!embeddingModelPendingOperation &&
!embeddingModels?.operations.length)
) {
return
}
let active = true
let timer: number | undefined
const poll = async (): Promise<void> => {
try {
await refreshEmbeddingModelProgress()
} catch {
// The final operation result reports actionable errors.
} finally {
if (active) {
timer = window.setTimeout(() => void poll(), 300)
}
}
}
timer = window.setTimeout(() => void poll(), 300)
return () => {
active = false
if (timer !== undefined) {
window.clearTimeout(timer)
}
}
}, [
embeddingModelPendingOperation,
embeddingModels?.operations.length,
open,
refreshEmbeddingModelProgress
])
const normalizedContextCompression =
normalizeContextCompressionTokenDrafts(
contextCompression,
contextCompressionTokenInput
)
const commitContextCompressionTokenInput =
(): ContextCompressionSettings => {
setContextCompression(normalizedContextCompression)
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
normalizedContextCompression
)
)
return normalizedContextCompression
}
const close = (trigger?: HTMLElement): void => {
if (saving || testing || clearingLocalData) return
if (hasUnsavedDrafts) {
leaveTriggerRef.current =
trigger ??
(document.activeElement instanceof HTMLElement
? document.activeElement
: undefined)
setPendingLeave({ kind: 'close' })
return
}
closeImmediately()
}
const resetSensitiveDrafts = (): void => {
setModelProfiles((profiles) =>
profiles.map((profile) => ({
...profile,
apiKey: '',
clearApiKey: false
}))
)
setKnowledgeEmbeddingApiKey('')
setClearKnowledgeEmbeddingApiKey(false)
setEmbeddingConnections((connections) =>
connections.map((connection) => ({
...connection,
apiKey: '',
clearApiKey: false
}))
)
setEditingEmbeddingConnectionId(undefined)
setKnowledgeRerankApiKey('')
setClearKnowledgeRerankApiKey(false)
setSpeechModelDraftId(undefined)
setPersistedSpeechModelId(undefined)
setSpeechModelSelectionDirty(false)
setBrandingDirty(false)
setPlatformFeaturesDirty(false)
setDocumentParsingDirty(false)
setSshHostsDirty(false)
setPendingLeave(undefined)
setError(undefined)
}
const closeImmediately = (): void => {
resetSensitiveDrafts()
onClose()
}
const leaveSettings = (proceed: () => void): void => {
resetSensitiveDrafts()
proceed()
}
const discardAndLeave = (): void => {
if (settings) {
hydrateSettings(settings)
}
setSpeechModelDraftId(persistedSpeechModelId)
setSpeechModelSelectionDirty(false)
runtimeCustomizationRef.current?.discard()
setRuntimeCustomizationDirty(false)
setBrandingDirty(false)
setPlatformFeaturesDirty(false)
setDocumentParsingDirty(false)
setSshHostsDirty(false)
const leave = pendingLeave
setPendingLeave(undefined)
setError(undefined)
if (leave?.kind === 'navigate') {
setActiveTab(leave.category)
if (leave.openModelDownloadSource) {
setModelDownloadSourceOpen(true)
return
}
requestAnimationFrame(() =>
document
.getElementById(`settings-tab-${leave.category}`)
?.focus()
)
return
}
if (leave?.kind === 'external') {
setActiveTab('runtime')
leaveSettings(leave.proceed)
return
}
if (leave?.kind === 'close') {
closeImmediately()
}
}
const keepEditing = (): void => {
const trigger = leaveTriggerRef.current
setPendingLeave(undefined)
setError(undefined)
requestAnimationFrame(() => {
if (trigger?.isConnected) {
trigger.focus()
}
})
}
useLayoutEffect(() => {
if (!pendingLeave) {
return
}
if (settingsBodyRef.current) {
settingsBodyRef.current.scrollTop = 0
}
keepEditingRef.current?.focus()
}, [pendingLeave])
useEffect(() => {
if (open && presentation === 'modal') {
return activateModalFocus(() => closeRef.current)
}
return undefined
}, [open, presentation])
useLayoutEffect(() => {
const requestLeave: SettingsLeaveRequester = (proceed) => {
if (hasUnsavedDrafts) {
leaveTriggerRef.current =
document.activeElement instanceof HTMLElement
? document.activeElement
: undefined
setPendingLeave({ kind: 'external', proceed })
return
}
resetSensitiveDrafts()
proceed()
}
onLeaveRequestReady(requestLeave)
return () => onLeaveRequestReady(undefined)
}, [hasUnsavedDrafts, onLeaveRequestReady])
if (!open) {
return null
}
const save = async (
notifySuccess = true
): Promise<RuntimeSettings | undefined> => {
setSaving(true)
setError(undefined)
try {
const contextCompressionInput =
commitContextCompressionTokenInput()
const defaultProfile =
modelProfiles.find(
(profile) => profile.id === defaultModelProfileId
) ?? modelProfiles[0]
if (!defaultProfile) {
throw new Error(t('errors.requireModelConnection'))
}
const configuredProfiles = new Map(
settings?.configured?.modelProfiles.map((profile) => [
profile.id,
profile
])
)
setModelValidationAttempted(true)
const invalidProfiles = modelProfiles.filter((profile) =>
!modelConnectionUrlSchema.safeParse(
profile.credentialSource === 'environment'
? configuredProfiles.get(profile.id)?.baseUrl ?? profile.baseUrl
: profile.baseUrl
).success
)
if (invalidProfiles.length) {
setActiveTab('model')
setModelType('llm')
setSelectedModelProfileId(invalidProfiles[0]!.id)
if (settingsBodyRef.current) settingsBodyRef.current.scrollTop = 0
throw new Error(invalidProfiles.map((profile) =>
t('errors.invalidModelUrl', { name: modelProfileDisplayName(profile) })
).join('\n'))
}
const profileInputs = modelProfiles.map((profile) => {
const configured = configuredProfiles.get(profile.id)
const environmentManaged =
profile.credentialSource === 'environment' &&
configured !== undefined
const requestHeaders = parseRequestHeadersDraft(
profile.requestHeadersText
)
if (!requestHeaders.success) {
setActiveTab('model')
setModelType('llm')
setSelectedModelProfileId(profile.id)
throw new Error(
t('errors.invalidModelRequestHeaders', {
name: modelProfileDisplayName(profile)
})
)
}
const requestBody = parseRequestBodyDraft(
profile.requestBodyText
)
if (!requestBody.success) {
setActiveTab('model')
setModelType('llm')
setSelectedModelProfileId(profile.id)
throw new Error(
t('errors.invalidModelRequestBody', {
name: modelProfileDisplayName(profile)
})
)
}
return {
id: profile.id,
name: profile.name,
baseUrl: environmentManaged
? configured.baseUrl
: profile.baseUrl,
modelName: environmentManaged
? configured.modelName
: profile.modelName,
protocol: profile.protocol,
authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput,
allowConversationInvocation: profile.allowConversationInvocation ?? false,
contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality,
requestHeaders: requestHeaders.data,
requestBody: requestBody.data,
apiKey: profile.clearApiKey
? ({ action: 'clear' } as const)
: profile.authentication === 'api-key' &&
profile.apiKey.trim()
? ({
action: 'replace',
value: profile.apiKey.trim()
} as const)
: profile.copyApiKeyFromProfileId
? ({
action: 'copy',
sourceProfileId: profile.copyApiKeyFromProfileId
} as const)
: ({ action: 'keep' } as const)
}
})
const defaultProfileInput =
profileInputs.find(
(profile) => profile.id === defaultProfile.id
) ?? profileInputs[0]!
const value = await window.goodbuddy.settings.updateRuntime({
provider,
modelBaseUrl: defaultProfileInput.baseUrl,
modelName: defaultProfileInput.modelName,
modelProtocol: defaultProfileInput.protocol,
modelAuthentication: defaultProfileInput.authentication,
imageGenerationQuality:
defaultProfileInput.imageGenerationQuality,
opencodeBaseUrl,
opencodeEmbedded: !opencodeBaseUrl,
opencodeBinaryPath,
opencodeConfigPath,
continueBinaryPath,
continueConfigPath,
continueMode,
knowledgeEmbeddingEnabled,
knowledgeEmbeddingBaseUrl,
knowledgeEmbeddingModel,
knowledgeEmbeddingApiKey: clearKnowledgeEmbeddingApiKey
? { action: 'clear' }
: knowledgeEmbeddingApiKey.trim()
? {
action: 'replace',
value: knowledgeEmbeddingApiKey.trim()
}
: { action: 'keep' },
embeddingConnections: [
{
id: builtinEmbeddingConnectionId,
name: 'GoodBuddy 内置向量模型',
kind: 'builtin'
},
...embeddingConnections.map((connection) => ({
id: connection.id,
name: connection.name,
kind: connection.kind,
baseUrl: connection.baseUrl,
modelName: connection.modelName,
authentication: connection.authentication,
apiKey: connection.clearApiKey
? ({ action: 'clear' } as const)
: connection.authentication === 'api-key' &&
connection.apiKey.trim()
? ({
action: 'replace',
value: connection.apiKey.trim()
} as const)
: ({ action: 'keep' } as const)
}))
],
activeEmbeddingConnectionId,
knowledgeRerankEnabled,
knowledgeRerankEndpoint,
knowledgeRerankModel,
knowledgeRerankApiKey: clearKnowledgeRerankApiKey
? { action: 'clear' }
: knowledgeRerankApiKey.trim()
? {
action: 'replace',
value: knowledgeRerankApiKey.trim()
}
: { action: 'keep' },
workspacePath,
apiKey: defaultProfileInput.apiKey.action === 'copy'
? { action: 'keep' }
: defaultProfileInput.apiKey,
modelProfiles: profileInputs,
defaultModelProfileId: defaultProfile.id,
defaultImageModelProfileId,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource,
contextCompression: contextCompressionInput,
toolApproval,
subagentSmartRoutingEnabled
})
let selectedSpeechModelId = speechModelDraftId
if (speechModelSelectionDirty) {
const speechModels = window.goodbuddy.speechModels
if (!speechModels) {
throw new Error(t('errors.speechModelsUnavailable'))
}
const speechSnapshot = await speechModels.select(
speechModelDraftId ?? null
)
selectedSpeechModelId = speechSnapshot.selectedModelId
}
hydrateSettings(value, true)
setPendingLeave(undefined)
if (speechModelSelectionDirty) {
setSpeechModelDraftId(selectedSpeechModelId)
setPersistedSpeechModelId(selectedSpeechModelId)
setSpeechModelSelectionDirty(false)
}
const embeddings = window.goodbuddy.embeddings
if (embeddings) {
try {
const snapshot = await embeddings.getSnapshot()
setEmbeddingSnapshot(snapshot)
setEmbeddingModels(snapshot.models)
} catch (reason) {
setError(
settingsErrorMessage(
reason,
t('errors.refreshEmbeddingAfterSave')
)
)
}
}
onSaved(value)
if (activeTab === 'runtime') {
const customizationSaved =
(await runtimeCustomizationRef.current?.save()) ?? true
if (!customizationSaved) {
return undefined
}
}
if (notifySuccess) {
onNotify({
tone: 'success',
message: t('notifications.settingsSaved'),
dedupeKey: 'runtime-settings-saved'
})
}
return value
} catch (reason) {
setError(settingsErrorMessage(reason, t('errors.saveSettings')))
return undefined
} finally {
setSaving(false)
}
}
const testConnection = async (): Promise<void> => {
const testingModel = activeTab === 'model' && modelType === 'llm'
const profileId = selectedModelProfileId
setTesting(true)
const savedSettings = await save(false)
if (!savedSettings) {
setTesting(false)
return
}
try {
const runtimeSource =
agentRuntimeType === 'opencode'
? savedSettings.opencodeModelSource
: agentRuntimeType === 'continue'
? savedSettings.continueModelSource
: savedSettings.deepseekHarnessModelSource ?? {
kind: 'platform'
}
const runtimeSelection =
runtimeSource.kind === 'profile'
? {
provider: agentRuntimeType,
profileId: runtimeSource.profileId
}
: runtimeSource.kind === 'platform'
? { provider: agentRuntimeType, runtimeConfig: true as const }
: { provider: agentRuntimeType }
const status = testingModel
? await window.goodbuddy.settings.testModelConnection(profileId)
: await window.goodbuddy.settings.testRuntime(runtimeSelection)
if (!status.available) {
throw new Error(status.detail)
}
onNotify({
tone: 'success',
message: testingModel
? t('notifications.modelGenerationTestSucceeded', {
label: status.label
})
: t('notifications.connectionSucceeded', {
label: status.label
}),
dedupeKey: testingModel
? 'model-connection-tested'
: `runtime-connection-tested-${agentRuntimeType}`
})
} catch (reason) {
setError(
settingsErrorMessage(
reason,
testingModel
? t('errors.testModel')
: t('errors.testRuntime')
)
)
} finally {
setTesting(false)
}
}
const runEmbeddingDiagnostic = async (
connectionId: string
): Promise<void> => {
const embeddings = window.goodbuddy.embeddings
if (!embeddings) {
setError(t('errors.embeddingDiagnosticUnavailable'))
return
}
setEmbeddingDiagnosticRunning(true)
setEmbeddingDiagnostic(undefined)
setError(undefined)
try {
if (!(await save(false))) {
return
}
const diagnostic = await embeddings.diagnose(connectionId)
setEmbeddingDiagnostic(diagnostic)
const snapshot = await embeddings.getSnapshot()
setEmbeddingSnapshot(snapshot)
setEmbeddingModels(snapshot.models)
} catch (reason) {
setError(settingsErrorMessage(reason, t('errors.testEmbedding')))
} finally {
setEmbeddingDiagnosticRunning(false)
}
}
const runEmbeddingModelOperation = async (
modelId: string,
kind:
| EmbeddingModelSnapshot['operations'][number]['kind']
| undefined,
operation: (
api: NonNullable<typeof window.goodbuddy.embeddings>
) => Promise<EmbeddingModelSnapshot | undefined>,
successMessage: string
): Promise<void> => {
const embeddings = window.goodbuddy.embeddings
if (!embeddings) {
setError(t('errors.embeddingDiagnosticUnavailable'))
return
}
setEmbeddingModelActionRunning(true)
setEmbeddingModelPendingOperation(
kind ? { modelId, kind } : undefined
)
setError(undefined)
let reconcileSnapshot = false
try {
const models = await operation(embeddings)
if (models) {
setEmbeddingModels(models)
setEmbeddingSnapshot((current) =>
current ? { ...current, models } : current
)
onNotify({
tone: 'success',
message: successMessage,
dedupeKey: `embedding-model-${modelId}`
})
} else {
reconcileSnapshot = true
}
} catch (reason) {
reconcileSnapshot = true
setError(
settingsErrorMessage(reason, t('errors.manageEmbeddingModel'))
)
} finally {
setEmbeddingModelActionRunning(false)
setEmbeddingModelPendingOperation(undefined)
if (reconcileSnapshot) {
void embeddings.getSnapshot().then(
(snapshot) => {
setEmbeddingSnapshot(snapshot)
setEmbeddingModels(snapshot.models)
},
() => undefined
)
}
}
}
const cancelEmbeddingModelOperation = async (
modelId: string
): Promise<void> => {
const embeddings = window.goodbuddy.embeddings
if (!embeddings) {
setError(t('errors.embeddingDiagnosticUnavailable'))
return
}
setError(undefined)
try {
await embeddings.cancelModelOperation(modelId)
const snapshot = await embeddings.getSnapshot()
setEmbeddingSnapshot(snapshot)
setEmbeddingModels(snapshot.models)
} catch (reason) {
setError(
settingsErrorMessage(reason, t('errors.manageEmbeddingModel'))
)
}
}
const selectRuntimeFile = async (
kind: RuntimeFileSelectionKind,
setValue: (value: string) => void
): Promise<void> => {
try {
const selected =
await window.goodbuddy.settings.selectRuntimeFile(kind)
if (selected) {
setValue(selected)
}
} catch (reason) {
setError(settingsErrorMessage(reason, t('errors.selectFile')))
}
}
const openRuntimeConfig = async (
runtime: RuntimeConfigActionInput['runtime'],
action: RuntimeConfigActionInput['action']
): Promise<void> => {
try {
await window.goodbuddy.settings.openRuntimeConfig({
runtime,
action
})
} catch (reason) {
setError(
settingsErrorMessage(reason, t('errors.openRuntimeConfig'))
)
}
}
const detectRuntimes = async (): Promise<void> => {
setDetecting(true)
setError(undefined)
try {
setDetection(
await window.goodbuddy.settings.detectAgentRuntimes()
)
} catch (reason) {
setError(settingsErrorMessage(reason, t('errors.detectRuntimes')))
} finally {
setDetecting(false)
}
}
const updateModelProfile = (
id: string,
update: Partial<ModelProfileDraft>
): void => {
setModelProfiles((profiles) =>
profiles.map((profile) =>
profile.id === id ? { ...profile, ...update } : profile
)
)
}
const addModelProfile = (): void => {
if (modelProfiles.length >= 20) return
const id = crypto.randomUUID()
setModelProfiles((profiles) => [
...profiles,
{
id,
name: t('model.profile.generatedName', {
count: profiles.length + 1
}),
baseUrl: '',
modelName: '',
protocol: 'openai-chat-completions',
authentication: defaultRuntimeSettings.modelAuthentication,
supportsImageInput: defaultRuntimeSettings.supportsImageInput,
allowConversationInvocation: false,
maximumOutputTokens: defaultAnthropicMaximumOutputTokens,
imageGenerationQuality:
defaultRuntimeSettings.imageGenerationQuality,
requestHeadersText: '{}',
requestBodyText: '{}',
apiKeyConfigured: false,
credentialSource: 'none',
apiKey: '',
clearApiKey: false
}
])
if (!defaultModelProfileId) {
setDefaultModelProfileId(id)
}
setSelectedModelProfileId(id)
}
const copyModelProfile = (source: ModelProfileDraft): void => {
if (modelProfiles.length >= 20) return
const id = crypto.randomUUID()
let name = ''
let index = 1
do {
const suffix = t('model.profile.copySuffix', { index })
name = `${modelProfileDisplayName(source).trim().slice(0, 64 - suffix.length)}${suffix}`
index += 1
} while (modelProfiles.some((profile) =>
profile.name.trim().toLowerCase() === name.toLowerCase()
))
const copyApiKeyFromProfileId = !source.clearApiKey && source.apiKeyConfigured
? source.copyApiKeyFromProfileId ?? source.id
: undefined
setModelProfiles((profiles) => [...profiles, {
...source,
id,
name,
apiKey: source.clearApiKey ? '' : source.apiKey,
clearApiKey: false,
copyApiKeyFromProfileId,
apiKeyConfigured: Boolean(copyApiKeyFromProfileId),
credentialSource: copyApiKeyFromProfileId ? 'encrypted' : 'none'
}])
setSelectedModelProfileId(id)
}
const removeModelProfile = (id: string): void => {
if (modelProfiles.length <= 1) {
setError(t('errors.retainModelConnection'))
return
}
const removedIndex = modelProfiles.findIndex(
(profile) => profile.id === id
)
const remaining = modelProfiles.filter((profile) => profile.id !== id)
const compatibleFallback = findPreferredCompatibleModelProfile(
remaining,
defaultModelProfileId,
(profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
const runtimeFallback: RuntimeModelSource = compatibleFallback
? { kind: 'profile', profileId: compatibleFallback.id }
: { kind: 'platform' }
setModelProfiles(remaining)
if (selectedModelProfileId === id) {
setSelectedModelProfileId(
remaining[Math.min(removedIndex, remaining.length - 1)]?.id ??
remaining[0]!.id
)
}
if (defaultModelProfileId === id) {
setDefaultModelProfileId(
compatibleFallback?.id ?? remaining[0]!.id
)
}
if (
opencodeModelSource.kind === 'profile' &&
opencodeModelSource.profileId === id
) {
setOpencodeModelSource(runtimeFallback)
}
if (
continueModelSource.kind === 'profile' &&
continueModelSource.profileId === id
) {
setContinueModelSource(runtimeFallback)
}
if (
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === id
) {
const harnessFallback = findPreferredCompatibleModelProfile(
remaining,
defaultModelProfileId,
(profile) => profile.protocol === 'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? { kind: 'profile', profileId: harnessFallback.id }
: { kind: 'platform' }
)
}
if (
contextCompression.modelSource.kind === 'profile' &&
contextCompression.modelSource.profileId === id
) {
setContextCompression((current) => ({
...current,
modelSource: { kind: 'current' }
}))
}
}
const selectDefaultModelProfile = (
profile: ModelProfileDraft
): void => {
setDefaultModelProfileId(profile.id)
}
const parseModelSource = (value: string): RuntimeModelSource =>
value === 'platform' || value === 'default'
? { kind: value }
: { kind: 'profile', profileId: value }
const isOpenCodeCompatible = (
profile: ModelProfileDraft
): boolean => isAgentRuntimeModelProtocol(profile.protocol)
const isContinueCompatible = (
profile: ModelProfileDraft
): boolean => isAgentRuntimeModelProtocol(profile.protocol)
const isDeepseekHarnessCompatible = (
profile: ModelProfileDraft
): boolean => isDeepSeekHarnessModelProfile(profile)
const selectedModelProfile =
modelProfiles.find(
(profile) => profile.id === selectedModelProfileId
) ?? modelProfiles[0]
const defaultTextModelProfile =
findPreferredCompatibleModelProfile(
modelProfiles,
defaultModelProfileId,
(profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
const defaultDeepseekHarnessModelProfile =
findPreferredCompatibleModelProfile(
modelProfiles,
defaultModelProfileId,
isDeepseekHarnessCompatible
)
const harnessPlatformModel = settings?.deepseekHarnessPlatformModel
const harnessDraftFallbackChanged =
harnessPlatformModel &&
harnessPlatformModel.source !== 'environment' &&
(harnessPlatformModel.source === 'profile'
? harnessPlatformModel.profileId !== defaultDeepseekHarnessModelProfile?.id ||
harnessPlatformModel.name !== defaultDeepseekHarnessModelProfile?.name ||
harnessPlatformModel.modelName !== defaultDeepseekHarnessModelProfile?.modelName
: Boolean(defaultDeepseekHarnessModelProfile))
const harnessPlatformSummary = [
!harnessPlatformModel
? t('runtime.deepseekHarness.platformSourceUnavailable')
: harnessPlatformModel.source === 'environment'
? t('runtime.deepseekHarness.environmentSource', {
model: harnessPlatformModel.modelName
})
: harnessPlatformModel.source === 'profile'
? t('runtime.deepseekHarness.platformFallback', {
name: modelProfileDisplayName({
id: harnessPlatformModel.profileId,
name: harnessPlatformModel.name
}),
model: harnessPlatformModel.modelName
})
: t('runtime.deepseekHarness.platformNoFallback'),
harnessDraftFallbackChanged
? defaultDeepseekHarnessModelProfile
? t('runtime.deepseekHarness.platformDraftFallback', {
name: modelProfileDisplayName(defaultDeepseekHarnessModelProfile),
model: defaultDeepseekHarnessModelProfile.modelName
})
: t('runtime.deepseekHarness.platformDraftNoFallback')
: ''
].filter(Boolean).join(' ')
const activeRuntimeModelSource =
agentRuntimeType === 'opencode'
? opencodeModelSource
: agentRuntimeType === 'continue'
? continueModelSource
: deepseekHarnessModelSource
const activeRuntimeModelProfile =
activeRuntimeModelSource.kind === 'profile'
? modelProfiles.find(
(profile) =>
profile.id === activeRuntimeModelSource.profileId &&
(agentRuntimeType === 'deepseek-harness'
? isDeepseekHarnessCompatible(profile)
: isAgentRuntimeModelProtocol(profile.protocol))
)
: activeRuntimeModelSource.kind === 'default'
? agentRuntimeType === 'deepseek-harness'
? defaultDeepseekHarnessModelProfile
: defaultTextModelProfile
: undefined
const savedRoleModelProfiles = (settings?.modelProfiles ?? [])
.filter((profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
.map(({ id, name }) => ({
id,
name: modelProfileDisplayName({ id, name })
}))
const savedRoleDefaultModelProfileId =
savedRoleModelProfiles.some(
(profile) => profile.id === settings?.defaultModelProfileId
)
? settings?.defaultModelProfileId
: undefined
const panel = (
<div
className={
presentation === 'page'
? 'settings-page'
: 'settings-backdrop'
}
role="presentation"
>
<section
aria-labelledby="settings-title"
aria-modal={presentation === 'modal' ? 'true' : undefined}
className="settings-panel"
ref={dialogRef}
onKeyDown={(event) => {
if (
presentation !== 'modal' ||
event.defaultPrevented ||
(event.target as HTMLElement).closest('[role="dialog"]') !== dialogRef.current
) return
event.stopPropagation()
if (event.key === 'Escape') {
event.preventDefault()
if (pendingLeave) keepEditing()
else close()
}
trapTabFocus(event, dialogRef.current)
}}
role={presentation === 'modal' ? 'dialog' : 'region'}
>
<div className="settings-panel__header">
<div>
<PageHeader
actions={
<button
aria-label={t('center.close')}
className="icon-button"
disabled={saving || testing || clearingLocalData}
ref={closeRef}
onClick={(event) => close(event.currentTarget)}
type="button"
>
<X aria-hidden="true" size={19} />
</button>
}
headingId="settings-title"
title={t('center.title')}
/>
</div>
</div>
<div
aria-busy={saving || testing}
className="settings-panel__body"
inert={saving || testing ? true : undefined}
>
<nav
aria-label={t('center.categoriesAriaLabel')}
aria-orientation="vertical"
className="settings-tabs"
role="tablist"
>
{visibleSettingsCategories.map((category, index) => (
<Fragment key={category.id}>
{visibleSettingsCategories[index - 1]?.group !== category.group && (
<span className="settings-tabs__group-label" aria-hidden="true">
{t(`categoryGroups.${category.group}`)}
</span>
)}
<button
aria-controls={`settings-panel-${category.id}`}
aria-label={t(
`categories.${category.translationKey}.label`
)}
aria-selected={activeTab === category.id}
id={`settings-tab-${category.id}`}
onClick={(event) => {
requestTabChange(category.id, event.currentTarget)
}}
onKeyDown={(event) =>
handleTabKeyDown(event, category.id)
}
role="tab"
tabIndex={activeTab === category.id ? 0 : -1}
type="button"
>
<strong>
{t(`categories.${category.translationKey}.label`)}
</strong>
</button>
</Fragment>
))}
</nav>
<div
aria-labelledby={`settings-tab-${activeTab}`}
className="settings-panel__content"
id={`settings-panel-${activeTab}`}
ref={settingsBodyRef}
role="tabpanel"
>
{pendingLeave && (
<div
className="settings-warning"
role="alert"
>
<p>
{pendingLeave.kind !== 'navigate'
? unsavedCloseMessage
: t('unsaved.navigation')}
</p>
<div className="update-settings__actions">
<button
className="secondary-button"
onClick={keepEditing}
ref={keepEditingRef}
type="button"
>
{t('unsaved.keepEditing')}
</button>
<button
className="danger-button"
onClick={discardAndLeave}
type="button"
>
{pendingLeave.kind !== 'navigate'
? t('unsaved.discardAndClose')
: t('unsaved.discardAndNavigate')}
</button>
</div>
</div>
)}
{!categoryRendersOwnHeader && (
<SettingsCategoryHeader
actions={
configurationTab ? (
<>
{(activeTab === 'runtime' ||
(activeTab === 'model' && modelType === 'llm')) && (
<button
className="secondary-button"
disabled={saving || testing}
onClick={() => void testConnection()}
type="button"
>
{testing
? t('actions.testing')
: activeTab === 'model'
? t('actions.saveAndTestModel')
: t('actions.saveAndTestRuntime', {
runtime:
agentRuntimeType === 'opencode'
? 'OpenCode'
: agentRuntimeType === 'continue'
? 'Continue'
: 'DeepSeek Harness'
})}
</button>
)}
<button
className="primary-button"
disabled={saving || testing}
onClick={() => void save()}
type="button"
>
<Save aria-hidden="true" size={13} />
{saving
? t('actions.saving')
: t('actions.saveSettings')}
</button>
</>
) : undefined
}
category={activeTab}
error={error}
navigation={
activeTab === 'runtime' ? (
<div className="model-type-navigation agent-runtime-navigation">
<SegmentedControl
ariaLabel="Agent Runtime"
onChange={setAgentRuntimeType}
options={[
{ label: 'OpenCode', value: 'opencode' },
{ label: 'Continue', value: 'continue' },
{
label: t('runtime.deepseekHarness.selectorLabel'),
value: 'deepseek-harness'
}
]}
value={agentRuntimeType}
/>
</div>
) : activeTab === 'model' ? (
<div className="model-type-navigation model-connections-navigation">
<SegmentedControl
ariaLabel={t('model.typeAriaLabel')}
onChange={setModelType}
options={[
{ label: t('model.types.llm.label'), value: 'llm' },
{ label: t('model.types.embedding.label'), value: 'embedding' },
{ label: t('model.types.rerank.label'), value: 'rerank' },
{ label: t('model.types.speech.label'), value: 'speech' }
]}
value={modelType}
/>
<ModelDownloadSourceSettings
open={modelDownloadSourceOpen}
onSourceChanged={setModelDownloadSource}
onOpen={openModelDownloadSource}
onClose={() => setModelDownloadSourceOpen(false)}
onNotify={onNotify}
/>
</div>
) : undefined
}
/>
)}
{activeTab === 'appearance' && (
<>
<div className="settings-section appearance-settings">
<div className="settings-section__title">
<SunMoon size={17} />
<span className="inline-help-label">
<strong>{t('appearance.theme.title')}</strong>
<InlineHelp label={t('appearance.theme.title')}>{t('appearance.theme.description')}</InlineHelp>
</span>
</div>
<div
aria-label={t('appearance.theme.ariaLabel')}
className="appearance-options"
role="radiogroup"
>
{(['system', 'light', 'dark'] as const).map(
(value) => (
<label key={value}>
<input
checked={appearanceTheme === value}
name="appearance-theme"
onChange={() =>
onAppearanceThemeChange(value)
}
type="radio"
value={value}
/>
<span
aria-hidden="true"
className={`appearance-options__preview appearance-options__preview--${value}`}
>
<i />
<i />
<i />
</span>
<strong>
{t(
`appearance.theme.options.${value}.label`
)}
</strong>
<small>
{t(
`appearance.theme.options.${value}.description`
)}
</small>
</label>
)
)}
</div>
</div>
<div className="settings-section">
<label className="toggle-row">
<span>{t('appearance.frostedGlass.label')}</span>
<input
type="checkbox"
role="switch"
checked={transparentFrostedEffectEnabled}
disabled={applicationSettingsPending || applicationSettingsLocked || !onTransparentFrostedEffectEnabledChange}
aria-describedby="frosted-glass-description"
onChange={(event) => {
void onTransparentFrostedEffectEnabledChange?.(event.target.checked)
}}
/>
</label>
<p id="frosted-glass-description" className="settings-panel__description">
{t('appearance.frostedGlass.description')}
</p>
{applicationSettingsError && (
<div role="alert">
<p>{applicationSettingsError}</p>
<button className="secondary-button" disabled={applicationSettingsPending} onClick={onRetryApplicationSettings} type="button">
{t('appearance.frostedGlass.retry')}
</button>
</div>
)}
</div>
<div className="settings-section appearance-settings">
<div className="settings-section__title">
<span className="inline-help-label">
<strong>{t('appearance.language.title')}</strong>
<InlineHelp label={t('appearance.language.title')}>{t('appearance.language.description')}</InlineHelp>
</span>
</div>
<div
aria-label={t('appearance.language.ariaLabel')}
className="appearance-options"
role="radiogroup"
>
{(
[
['system', 'system'],
['zh-CN', 'chinese'],
['en-US', 'english']
] as const
).map(([value, translationKey]) => (
<label key={value}>
<input
checked={uiLocalePreference === value}
name="interface-language"
onChange={() => setUiLocalePreference(value)}
type="radio"
value={value}
/>
<strong>
{t(
`appearance.language.options.${translationKey}.label`
)}
</strong>
<small>
{t(
`appearance.language.options.${translationKey}.description`
)}
</small>
</label>
))}
</div>
</div>
<BrandingSettingsSection
fallbackLogo={brandingFallbackLogo}
onChange={onBrandingPreferencesChange}
onDirtyChange={setBrandingDirty}
preferences={brandingPreferences}
/>
</>
)}
{activeTab === 'platform-features' && (
<PlatformFeaturesSettingsSection
onDirtyChange={setPlatformFeaturesDirty}
onConversationHtmlRenderingEnabledChange={
onConversationHtmlRenderingEnabledChange
}
onRemoteProjectsEnabledChange={
onRemoteProjectsEnabledChange
}
onWorkspacePathChange={setWorkspacePath}
workspacePath={workspacePath}
onNotify={onNotify}
onShortcutSettingsChanged={onShortcutSettingsChanged}
/>
)}
{activeTab === 'runtime' && (
<>
<SettingsWarningList warnings={settings?.warnings} />
{agentRuntimeType === 'opencode' && (
<div className="settings-section">
<div className="settings-section__title">
<TerminalSquare size={17} />
<div>
<strong>{t('runtime.opencode.title')}</strong>
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<RuntimeOverviewCard
detection={detection?.opencode}
detectionLabel="OpenCode"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
})
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.opencode.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'OpenCode'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
{t('runtime.opencode.advancedDescription')}
</p>
<fieldset className="runtime-source-options">
<legend>{t('runtime.sourceLegend')}</legend>
<label>
<input
checked={opencodeModelSource.kind !== 'platform'}
disabled={!defaultTextModelProfile}
name="opencode-model-source"
onChange={() => {
if (defaultTextModelProfile) {
setOpencodeModelSource({
kind: 'default'
})
setOpencodeBaseUrl('')
}
}}
type="radio"
/>
<span>
<strong>{t('runtime.goodBuddyModelSource')}</strong>
<small>
{t('runtime.opencode.followDescription')}
</small>
</span>
</label>
<label>
<input
checked={opencodeModelSource.kind === 'platform'}
name="opencode-model-source"
onChange={() =>
setOpencodeModelSource({ kind: 'platform' })
}
type="radio"
/>
<span>
<strong>
{t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})}
</strong>
<small>
{t('runtime.opencode.ownDescription')}
</small>
</span>
</label>
</fieldset>
{opencodeModelSource.kind !== 'platform' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`OpenCode ${t(
'runtime.goodBuddyConnection'
)}`}
value={opencodeModelSource.kind === 'default' ? 'default' : opencodeModelSource.profileId}
onChange={(event) => {
setOpencodeModelSource(
parseModelSource(event.target.value)
)
setOpencodeBaseUrl('')
}}
>
<option value="default">{t('runtime.followRecommended')}</option>
{modelProfiles.map((profile) => (
<option
disabled={!isOpenCodeCompatible(profile)}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isOpenCodeCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
<small>
{t('runtime.pinConnectionDescription')}
</small>
</label>
)}
{opencodeModelSource.kind === 'platform' &&
!opencodeBaseUrl.trim() && (
<RuntimeConfigCard
description={t(
'runtime.opencode.configDescription'
)}
fileKind="opencodeConfig"
onOpen={openRuntimeConfig}
onPathChange={setOpencodeConfigPath}
onSelect={selectRuntimeFile}
path={opencodeConfigPath}
runtime="opencode"
runtimeLabel="OpenCode"
savedPath={
settings
? configuredRuntimeSettings(settings)
.opencodeConfigPath
: undefined
}
/>
)}
{opencodeModelSource.kind === 'platform' &&
Boolean(opencodeBaseUrl.trim()) && (
<p className="settings-warning">
{t('runtime.opencode.externalServerWarning')}
</p>
)}
<label className="field">
<span>{t('runtime.serverAddress')}</span>
<input
aria-label={t('runtime.opencode.serverAriaLabel')}
inputMode="url"
onChange={(event) => {
const value = event.target.value
setOpencodeBaseUrl(value)
if (value.trim()) {
setOpencodeModelSource({ kind: 'platform' })
}
}}
placeholder={t(
'runtime.opencode.serverPlaceholder'
)}
value={opencodeBaseUrl}
/>
<small>
{t('runtime.opencode.serverDescription')}
</small>
</label>
<label className="field">
<span>{t('runtime.opencode.binaryPath')}</span>
<div className="workspace-picker">
<input
aria-label={t('runtime.opencode.binaryPath')}
onChange={(event) =>
setOpencodeBinaryPath(event.target.value)
}
placeholder={t(
'runtime.bundledProgramPlaceholder'
)}
value={opencodeBinaryPath}
/>
<button
className="secondary-button"
onClick={() =>
void selectRuntimeFile(
'opencodeBinary',
setOpencodeBinaryPath
)
}
type="button"
>
{t('actions.select')}
</button>
<button
className="secondary-button"
disabled={!opencodeBinaryPath}
onClick={() => setOpencodeBinaryPath('')}
type="button"
>
{t('actions.clear')}
</button>
</div>
</label>
{opencodeBinaryPath && (
<p className="settings-warning">
{t('runtime.customBinaryWarning', {
runtime: 'OpenCode'
})}
</p>
)}
<button
className="secondary-button"
disabled={detecting}
onClick={() => void detectRuntimes()}
type="button"
>
{detecting
? t('actions.detecting')
: t('actions.redetectRuntime', {
runtime: 'OpenCode'
})}
</button>
</details>
</div>
)}
{agentRuntimeType === 'continue' && (
<div className="settings-section">
<div className="settings-section__title">
<TerminalSquare size={17} />
<div>
<strong>{t('runtime.continue.title')}</strong>
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<RuntimeOverviewCard
detection={detection?.continue}
detectionLabel="Continue"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'Continue'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
})
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.continue.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'Continue'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
{t('runtime.continue.advancedDescription')}
</p>
<fieldset className="runtime-source-options">
<legend>{t('runtime.sourceLegend')}</legend>
<label>
<input
checked={continueModelSource.kind !== 'platform'}
disabled={!defaultTextModelProfile}
name="continue-model-source"
onChange={() => {
if (defaultTextModelProfile) {
setContinueModelSource({
kind: 'default'
})
}
}}
type="radio"
/>
<span>
<strong>{t('runtime.goodBuddyModelSource')}</strong>
<small>
{t('runtime.continue.followDescription')}
</small>
</span>
</label>
<label>
<input
checked={continueModelSource.kind === 'platform'}
name="continue-model-source"
onChange={() =>
setContinueModelSource({ kind: 'platform' })
}
type="radio"
/>
<span>
<strong>
{t('runtime.ownConfiguration', {
runtime: 'Continue'
})}
</strong>
<small>
{t('runtime.continue.ownDescription')}
</small>
</span>
</label>
</fieldset>
{continueModelSource.kind !== 'platform' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`Continue ${t(
'runtime.goodBuddyConnection'
)}`}
value={continueModelSource.kind === 'default' ? 'default' : continueModelSource.profileId}
onChange={(event) =>
setContinueModelSource(
parseModelSource(event.target.value)
)
}
>
<option value="default">{t('runtime.followRecommended')}</option>
{modelProfiles.map((profile) => (
<option
disabled={!isContinueCompatible(profile)}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isContinueCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
<small>
{t('runtime.pinConnectionDescription')}
</small>
</label>
)}
{continueModelSource.kind === 'platform' && (
<RuntimeConfigCard
description={t(
'runtime.continue.configDescription'
)}
fileKind="continueConfig"
onOpen={openRuntimeConfig}
onPathChange={setContinueConfigPath}
onSelect={selectRuntimeFile}
path={continueConfigPath}
runtime="continue"
runtimeLabel="Continue"
savedPath={
settings
? configuredRuntimeSettings(settings)
.continueConfigPath
: undefined
}
/>
)}
<label className="field">
<span>{t('runtime.continue.binaryPath')}</span>
<div className="workspace-picker">
<input
aria-label={t('runtime.continue.binaryPath')}
onChange={(event) =>
setContinueBinaryPath(event.target.value)
}
placeholder={t(
'runtime.bundledProgramPlaceholder'
)}
value={continueBinaryPath}
/>
<button
className="secondary-button"
onClick={() =>
void selectRuntimeFile(
'continueBinary',
setContinueBinaryPath
)
}
type="button"
>
{t('actions.select')}
</button>
<button
className="secondary-button"
disabled={!continueBinaryPath}
onClick={() => setContinueBinaryPath('')}
type="button"
>
{t('actions.clear')}
</button>
</div>
</label>
{continueModelSource.kind === 'platform' &&
!continueConfigPath && (
<p className="settings-warning">
{t('runtime.continue.missingConfigWarning')}
</p>
)}
{continueBinaryPath && (
<p className="settings-warning">
{t('runtime.customBinaryWarning', {
runtime: 'Continue'
})}
</p>
)}
<button
className="secondary-button"
disabled={detecting}
onClick={() => void detectRuntimes()}
type="button"
>
{detecting
? t('actions.detecting')
: t('actions.redetectRuntime', {
runtime: 'Continue'
})}
</button>
</details>
</div>
)}
{agentRuntimeType === 'deepseek-harness' && (
<div className="settings-section">
<div className="settings-section__title">
<TerminalSquare size={17} />
<div>
<strong>{t('runtime.deepseekHarness.title')}</strong>
<small>
{t('runtime.deepseekHarness.previewDescription')}
</small>
</div>
</div>
<RuntimeOverviewCard
detection={detection?.deepseekHarness}
detectionLabel="DeepSeek Harness"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? harnessPlatformSummary
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t(
'runtime.deepseekHarness.description'
)}
runtime={t('runtime.bundledRuntime', {
runtime: 'DeepSeek Harness'
})}
/>
<div className="field">
<span className="inline-help-label">
<label htmlFor="deepseek-harness-connection">{t('runtime.deepseekHarness.connection')}</label>
<InlineHelp label={t('runtime.deepseekHarness.connection')} id="deepseek-harness-connection-description">
{t('runtime.deepseekHarness.connectionDescription')}
</InlineHelp>
</span>
<select
id="deepseek-harness-connection"
aria-label={`DeepSeek Harness ${t(
'runtime.deepseekHarness.connection'
)}`}
aria-describedby="deepseek-harness-connection-description"
onChange={(event) =>
setDeepseekHarnessModelSource(
parseModelSource(event.target.value)
)
}
value={
deepseekHarnessModelSource.kind === 'profile'
? deepseekHarnessModelSource.profileId
: deepseekHarnessModelSource.kind
}
>
<option disabled={!defaultDeepseekHarnessModelProfile} value="default">{t('runtime.followRecommended')}</option>
<option value="platform">{t('runtime.deepseekHarness.platformSource')}</option>
{modelProfiles.map((profile) => (
<option
disabled={!isDeepseekHarnessCompatible(profile)}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isDeepseekHarnessCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
</div>
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
{t(
'runtime.deepseekHarness.advancedDescription'
)}
</p>
<button
className="secondary-button"
disabled={detecting}
onClick={() => void detectRuntimes()}
type="button"
>
{detecting
? t('actions.detecting')
: t('actions.redetectRuntime', {
runtime: 'DeepSeek Harness'
})}
</button>
</details>
</div>
)}
</>
)}
<div hidden={activeTab !== 'runtime'}>
<RuntimeCustomizationSection
onDirtyChange={handleRuntimeCustomizationDirtyChange}
profileId={
activeRuntimeModelSource.kind === 'profile'
? activeRuntimeModelSource.profileId
: undefined
}
provider={agentRuntimeType}
ref={runtimeCustomizationRef}
/>
</div>
{activeTab === 'runtime' &&
agentRuntimeType === 'deepseek-harness' && (
<DshMarketplaceSection onNotify={onNotify} />
)}
{activeTab === 'model' && (
<>
{modelType === 'llm' && (
<div className="settings-section">
<div className="settings-section__title settings-section__title--actions">
<KeyRound size={17} />
<div>
<strong>{t('model.profile.title')}</strong>
<small>
{t('model.profile.description')}
</small>
</div>
<button
className="secondary-button model-connection-add"
disabled={modelProfiles.length >= 20}
onClick={addModelProfile}
type="button"
>
<Plus size={14} />
{t('actions.addCustom')}
</button>
</div>
{defaultImageModelProfileId && !modelProfiles.some((profile) =>
profile.id === defaultImageModelProfileId &&
profile.protocol === 'openai-images-generations' &&
profile.allowConversationInvocation
) && (
<p className="settings-warning" role="status">
{t('conversationImages.unavailable')}
</p>
)}
<div className="model-connection-manager">
<aside
aria-label={t('model.profile.listAriaLabel')}
className="model-connection-list"
>
<div className="model-connection-list__header">
<strong>{t('model.profile.listTitle')}</strong>
<span>{modelProfiles.length}</span>
</div>
<div role="list">
{modelProfiles.map((profile) => (
<div key={profile.id} role="listitem">
<button
aria-current={
selectedModelProfile?.id === profile.id
? 'page'
: undefined
}
aria-label={t('model.profile.editAriaLabel', {
name: modelProfileDisplayName(profile)
})}
onClick={() =>
setSelectedModelProfileId(profile.id)
}
type="button"
>
<span className="model-connection-list__name">
<strong>{modelProfileDisplayName(profile)}</strong>
<small>{profile.modelName}</small>
</span>
<span className="model-connection-list__badges">
{defaultModelProfileId === profile.id && (
<span>{t('model.profile.defaultBadge')}</span>
)}
{profile.protocol === 'openai-images-generations' && profile.allowConversationInvocation && (
<span>{t('conversationImages.callable')}</span>
)}
{defaultImageModelProfileId === profile.id && (
<span>{t('conversationImages.defaultBadge')}</span>
)}
{profile.protocol ===
'openai-images-generations' && (
<span>{t('model.profile.imageBadge')}</span>
)}
</span>
</button>
</div>
))}
</div>
</aside>
{selectedModelProfile &&
(() => {
const profile = selectedModelProfile
const environmentManaged =
profile.credentialSource === 'environment'
const endpointInvalid = modelValidationAttempted &&
!modelConnectionUrlSchema.safeParse(profile.baseUrl).success
const requestHeadersValid =
parseRequestHeadersDraft(
profile.requestHeadersText
).success
const requestBodyValid =
parseRequestBodyDraft(
profile.requestBodyText
).success
return (
<div
aria-labelledby={`model-connection-${profile.id}`}
className="model-connection-detail"
key={profile.id}
>
<div className="settings-section__title">
<div>
<strong id={`model-connection-${profile.id}`}>
{modelProfileDisplayName(profile)}
</strong>
<small>{t('model.profile.detail')}</small>
</div>
<label className="check-field">
<input
checked={defaultModelProfileId === profile.id}
name="default-model-profile"
onChange={() =>
selectDefaultModelProfile(profile)
}
type="radio"
/>
<span>{t('model.profile.defaultConnection')}</span>
</label>
{profile.protocol === 'openai-images-generations' && (
<span className="model-capability-badge">
{t('model.profile.imageGeneration')}
</span>
)}
<button
aria-label={t('model.profile.copyAriaLabel', {
name: modelProfileDisplayName(profile)
})}
className="secondary-button"
disabled={modelProfiles.length >= 20}
onClick={() => copyModelProfile(profile)}
title={t('model.profile.copyDescription')}
type="button"
>
<Copy aria-hidden="true" size={14} />
{t('model.profile.copy')}
</button>
<button
aria-label={t('model.profile.deleteAriaLabel', {
name: modelProfileDisplayName(profile)
})}
className="danger-button danger-button--quiet"
disabled={modelProfiles.length <= 1}
onClick={() => removeModelProfile(profile.id)}
type="button"
>
<Trash2 aria-hidden="true" size={14} />
{t('actions.deleteConnection')}
</button>
</div>
<label className="field">
<span>{t('model.profile.name')}</span>
<input
onChange={(event) =>
updateModelProfile(profile.id, {
name: event.target.value
})
}
value={modelProfileDisplayName(profile)}
/>
</label>
<label className="field">
<span>{t('model.profile.endpoint')}</span>
<input
disabled={environmentManaged}
inputMode="url"
aria-invalid={endpointInvalid || undefined}
aria-describedby={endpointInvalid ? `model-url-error-${profile.id}` : undefined}
onChange={(event) =>
updateModelProfile(profile.id, {
baseUrl: event.target.value
})
}
placeholder="https://api.example.com/v1"
value={profile.baseUrl}
/>
{endpointInvalid && (
<small className="field-error" id={`model-url-error-${profile.id}`} role="alert">
{t('errors.invalidModelUrl', { name: modelProfileDisplayName(profile) })}
</small>
)}
</label>
<label className="field">
<span>{t('model.profile.model')}</span>
<input
disabled={environmentManaged}
onChange={(event) =>
updateModelProfile(profile.id, {
modelName: event.target.value
})
}
placeholder="model-name"
value={profile.modelName}
/>
</label>
<label className="field">
<span>{t('model.profile.protocol')}</span>
<select
aria-label={t(
'model.profile.protocolAriaLabel',
{ name: modelProfileDisplayName(profile) }
)}
onChange={(event) =>
{
const protocol = event.target
.value as ModelProfileDraft['protocol']
updateModelProfile(profile.id, { protocol })
const compatibleFallback =
findPreferredCompatibleModelProfile(
modelProfiles.filter(
(candidate) =>
candidate.id !== profile.id
),
defaultModelProfileId,
(candidate) =>
isAgentRuntimeModelProtocol(
candidate.protocol
)
)
const runtimeFallback: RuntimeModelSource =
compatibleFallback
? {
kind: 'profile',
profileId: compatibleFallback.id
}
: { kind: 'platform' }
if (
!isAgentRuntimeModelProtocol(protocol) &&
opencodeModelSource.kind === 'profile' &&
opencodeModelSource.profileId === profile.id
) {
setOpencodeModelSource(runtimeFallback)
}
if (
!isAgentRuntimeModelProtocol(protocol) &&
continueModelSource.kind === 'profile' &&
continueModelSource.profileId === profile.id
) {
setContinueModelSource(runtimeFallback)
}
if (
protocol !== 'openai-chat-completions' &&
deepseekHarnessModelSource.kind ===
'profile' &&
deepseekHarnessModelSource.profileId ===
profile.id
) {
const harnessFallback =
modelProfiles.find(
(candidate) =>
candidate.id !== profile.id &&
candidate.protocol ===
'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? {
kind: 'profile',
profileId: harnessFallback.id
}
: { kind: 'platform' }
)
}
if (
!isAgentRuntimeModelProtocol(protocol) &&
contextCompression.modelSource.kind ===
'profile' &&
contextCompression.modelSource.profileId ===
profile.id
) {
setContextCompression((current) => ({
...current,
modelSource: { kind: 'current' }
}))
}
}
}
value={profile.protocol}
>
<option value="openai-chat-completions">
{t('model.profile.openAiCompatibleProtocol')}
</option>
<option value="openai-responses">
OpenAI Responses
</option>
<option value="anthropic-messages">
Anthropic Messages
</option>
<option value="openai-images-generations">
{t('model.profile.imageProtocol')}
</option>
</select>
</label>
<label className="field">
<span>{t('model.profile.authentication')}</span>
<select
aria-label={t(
'model.profile.authenticationAriaLabel',
{ name: modelProfileDisplayName(profile) }
)}
onChange={(event) => {
const authentication = event.target
.value as ModelProfileDraft['authentication']
updateModelProfile(profile.id, {
authentication
})
}}
value={profile.authentication}
>
<option value="api-key">API Key</option>
<option value="none">
{t('credentials.noAuthentication')}
</option>
</select>
</label>
{profile.authentication === 'api-key' ? (
<>
<label className="field">
<span>API Key</span>
<input
autoComplete="off"
disabled={
environmentManaged ||
!settings?.secureStorageAvailable
}
onChange={(event) =>
updateModelProfile(profile.id, {
apiKey: event.target.value,
clearApiKey: false
})
}
placeholder={
profile.apiKeyConfigured
? t('credentials.configuredPlaceholder')
: t('credentials.enterApiKey')
}
type="password"
value={profile.apiKey}
/>
</label>
<div className="credential-state">
<LockKeyhole size={15} />
<span>
{profile.copyApiKeyFromProfileId
? t('model.profile.credentialCopyPending')
: t(`credentials.${profile.credentialSource}`)}
</span>
{profile.credentialSource === 'encrypted' && (
<button
onClick={() =>
updateModelProfile(profile.id, {
apiKey: '',
clearApiKey: true
})
}
type="button"
>
{profile.clearApiKey
? t('actions.clearAfterSave')
: t('actions.clearCredential')}
</button>
)}
</div>
</>
) : (
<div className="credential-state">
<LockKeyhole size={15} />
<span>
{profile.apiKeyConfigured
? t('credentials.savedForConnection')
: t(
'credentials.noAuthenticationDescription'
)}
</span>
</div>
)}
{profile.protocol === 'openai-images-generations' && (
<div className="field">
<label className="toggle-row">
<input
checked={profile.allowConversationInvocation ?? false}
onChange={(event) => updateModelProfile(profile.id, {
allowConversationInvocation: event.target.checked
})}
aria-describedby={`conversation-images-help-${profile.id}`}
role="switch"
type="checkbox"
/>
<span>{t('conversationImages.allow')}</span>
</label>
<small id={`conversation-images-help-${profile.id}`}>
{t('conversationImages.description')}
</small>
{profile.allowConversationInvocation && (
<label className="check-field">
<input
checked={defaultImageModelProfileId === profile.id}
name="default-image-model-profile"
onChange={() => setDefaultImageModelProfileId(profile.id)}
type="radio"
/>
<span>{t('conversationImages.setDefault')}</span>
</label>
)}
</div>
)}
{isAgentRuntimeModelProtocol(profile.protocol) && (
<>
<div className="field">
<label className="toggle-row">
<input
checked={profile.supportsImageInput}
onChange={(event) =>
updateModelProfile(profile.id, {
supportsImageInput: event.target.checked
})
}
role="switch"
type="checkbox"
/>
<span>{t('model.profile.supportsImageInput')}</span>
</label>
<small>
{t('model.profile.supportsImageInputDescription')}
</small>
</div>
<div className="field">
<span className="inline-help-label">
<label htmlFor={`model-context-window-${profile.id}`}>{t('model.profile.contextWindow')} (K tokens)</label>
<InlineHelp label={t('model.profile.contextWindow')} id={`model-context-window-help-${profile.id}`}>
{t('model.profile.contextWindowDescription')}
</InlineHelp>
</span>
<input
id={`model-context-window-${profile.id}`}
aria-describedby={`model-context-window-help-${profile.id}`}
aria-label={t('model.profile.contextWindow')}
inputMode="numeric"
max={10_000}
min={32}
onChange={(event) => {
const value = event.target.valueAsNumber
updateModelProfile(profile.id, {
contextWindowTokens: Number.isFinite(value)
? Math.round(value * 1_000)
: undefined
})
}}
placeholder="200"
type="number"
value={
profile.contextWindowTokens === undefined
? ''
: profile.contextWindowTokens / 1_000
}
/>
</div>
{profile.protocol === 'anthropic-messages' && (
<div className="field">
<span className="inline-help-label">
<label htmlFor={`model-maximum-output-${profile.id}`}>{t('model.profile.maximumOutput')} (K tokens)</label>
<InlineHelp label={t('model.profile.maximumOutput')} id={`model-maximum-output-help-${profile.id}`}>
{t('model.profile.maximumOutputDescription')}
</InlineHelp>
</span>
<input
id={`model-maximum-output-${profile.id}`}
aria-describedby={`model-maximum-output-help-${profile.id}`}
aria-label={t('model.profile.maximumOutput')}
inputMode="numeric"
max={10_000}
min={1}
onChange={(event) => {
const value = event.target.valueAsNumber
updateModelProfile(profile.id, {
maximumOutputTokens: Number.isFinite(value)
? Math.round(value * 1_000)
: defaultAnthropicMaximumOutputTokens
})
}}
placeholder="32"
type="number"
value={
(profile.maximumOutputTokens ??
defaultAnthropicMaximumOutputTokens) / 1_000
}
/>
</div>
)}
</>
)}
{profile.protocol ===
'openai-images-generations' && (
<div className="field">
<span className="inline-help-label">
<label htmlFor={`model-image-quality-${profile.id}`}>{t('model.profile.imageQuality')}</label>
<InlineHelp label={t('model.profile.imageQuality')} id={`model-image-quality-help-${profile.id}`}>
{t('model.profile.imageQualityDescription')}
</InlineHelp>
</span>
<select
id={`model-image-quality-${profile.id}`}
aria-describedby={`model-image-quality-help-${profile.id}`}
aria-label={t(
'model.profile.imageQualityAriaLabel',
{ name: modelProfileDisplayName(profile) }
)}
onChange={(event) =>
updateModelProfile(profile.id, {
imageGenerationQuality: event.target
.value as ModelProfileDraft['imageGenerationQuality']
})
}
value={profile.imageGenerationQuality}
>
<option value="auto">
{t('model.profile.quality.auto')}
</option>
<option value="low">
{t('model.profile.quality.low')}
</option>
<option value="medium">
{t('model.profile.quality.medium')}
</option>
<option value="high">
{t('model.profile.quality.high')}
</option>
</select>
</div>
)}
<details className="settings-section" open={!requestHeadersValid || !requestBodyValid}>
<summary>{t('model.profile.advanced')}</summary>
<div className="field">
<span className="inline-help-label">
<label htmlFor={`model-request-headers-${profile.id}`}>{t('model.profile.requestHeaders')}</label>
<InlineHelp label={t('model.profile.requestHeaders')} id={`model-request-headers-help-${profile.id}`}>
{t('model.profile.requestHeadersDescription')}
</InlineHelp>
</span>
<textarea
id={`model-request-headers-${profile.id}`}
aria-describedby={`model-request-headers-help-${profile.id}`}
aria-label={t('model.profile.requestHeaders')}
aria-invalid={
requestHeadersValid ? undefined : true
}
className="model-request-json-input"
maxLength={maximumRequestHeadersDraftCharacters}
onChange={(event) =>
updateModelProfile(profile.id, {
requestHeadersText: event.target.value
})
}
rows={6}
spellCheck={false}
value={profile.requestHeadersText}
/>
{!requestHeadersValid && (
<small className="field-error" role="alert">
{t('model.profile.requestHeadersInvalid')}
</small>
)}
</div>
<div className="field">
<span className="inline-help-label">
<label htmlFor={`model-request-body-${profile.id}`}>{t('model.profile.requestBody')}</label>
<InlineHelp label={t('model.profile.requestBody')} id={`model-request-body-help-${profile.id}`}>
{t('model.profile.requestBodyDescription')}
</InlineHelp>
</span>
<textarea
id={`model-request-body-${profile.id}`}
aria-describedby={`model-request-body-help-${profile.id}`}
aria-label={t('model.profile.requestBody')}
aria-invalid={
requestBodyValid ? undefined : true
}
className="model-request-json-input"
maxLength={maximumRequestBodyDraftCharacters}
onChange={(event) =>
updateModelProfile(profile.id, {
requestBodyText: event.target.value
})
}
rows={6}
spellCheck={false}
value={profile.requestBodyText}
/>
{!requestBodyValid && (
<small className="field-error" role="alert">
{t('model.profile.requestBodyInvalid')}
</small>
)}
</div>
<p className="settings-warning">
{t('model.profile.requestCustomizationWarning')}
</p>
</details>
<small className="model-connection-detail__compatibility">
{t('model.profile.compatibilitySummary', {
directCapability:
profile.protocol ===
'openai-images-generations'
? t('model.profile.imageGeneration')
: t('model.profile.textChat'),
continueCompatibility: isAgentRuntimeModelProtocol(profile.protocol)
? t('model.profile.compatible')
: t('model.profile.incompatible'),
openCodeCompatibility: isAgentRuntimeModelProtocol(profile.protocol)
? t('model.profile.compatible')
: t('model.profile.incompatibleImageProtocol'),
deepseekHarnessCompatibility:
isDeepseekHarnessCompatible(profile)
? t('model.profile.compatible')
: t(
'model.profile.incompatibleHarnessProtocol'
)
})}
</small>
</div>
)
})()}
</div>
{settings && !settings.secureStorageAvailable && (
<p className="settings-warning">
{t('model.profile.secureStorageWarning')}
</p>
)}
</div>
)}
{modelType === 'embedding' && (
<EmbeddingSettingsSection
connections={[
...(embeddingSnapshot?.connections ?? [])
.filter((connection) => connection.kind === 'builtin')
.map((connection) => {
const installed = embeddingModels?.installed.some(
(candidate) => candidate.id === connection.model
)
const operation = embeddingModels?.operations.find(
(candidate) =>
candidate.modelId === connection.model
)
const progress =
operation?.totalBytes &&
Math.round(
(operation.completedBytes /
operation.totalBytes) *
100
)
return {
...connection,
installed,
statusText: operation
? t('model.embedding.progress', {
percent: progress ?? 0
})
: installed
? t('model.embedding.installed')
: t('model.embedding.notInstalled')
}
}),
...embeddingConnections.map((connection) => ({
...connection,
model: connection.modelName,
endpoint: connection.baseUrl
}))
]}
currentConnectionId={activeEmbeddingConnectionId}
diagnostic={embeddingDiagnostic}
diagnosticRunning={embeddingDiagnosticRunning}
enabled={knowledgeEmbeddingEnabled}
models={embeddingModels}
pendingOperation={embeddingModelPendingOperation}
busy={saving || embeddingModelActionRunning}
secureStorageAvailable={
settings?.secureStorageAvailable ?? false
}
selectedConnectionId={editingEmbeddingConnectionId}
onAddConnection={() => {
const id = crypto.randomUUID()
setEmbeddingConnections((connections) => [
...connections,
{
id,
name: t('model.embedding.newConnectionName'),
kind: 'openai-compatible',
baseUrl:
'https://provider.example/v1/embeddings',
modelName: 'text-embedding-3-small',
authentication: 'api-key',
apiKeyConfigured: false,
credentialSource: 'none',
apiKey: '',
clearApiKey: false
}
])
setEditingEmbeddingConnectionId(id)
}}
onCancelBuiltin={(modelId) => {
void cancelEmbeddingModelOperation(modelId)
}}
onDeleteConnection={(connectionId) => {
setEmbeddingConnections((connections) =>
connections.filter(
(connection) => connection.id !== connectionId
)
)
if (activeEmbeddingConnectionId === connectionId) {
setActiveEmbeddingConnectionId(
builtinEmbeddingConnectionId
)
}
setEditingEmbeddingConnectionId(
builtinEmbeddingConnectionId
)
}}
onDownloadBuiltin={(modelId) => {
if (!embeddingModels) {
setError(t('errors.readEmbeddingStatus'))
return
}
const model = embeddingModels?.catalog.find(
(candidate) => candidate.id === modelId
)
void runEmbeddingModelOperation(
modelId,
'download',
(api) =>
api.installModel(
modelId,
embeddingModels.selectedDownloadSource
),
i18n.t('embedding.notifications.installed', {
ns: 'settingsSections',
name: model?.displayName ?? modelId
})
)
}}
onEnabledChange={setKnowledgeEmbeddingEnabled}
onImportBuiltin={(modelId) => {
const name =
embeddingModels?.catalog.find(
(candidate) => candidate.id === modelId
)?.displayName ?? modelId
void runEmbeddingModelOperation(
modelId,
'import',
(api) => api.importModelArchive(modelId),
i18n.t('embedding.notifications.importedZip', {
ns: 'settingsSections',
name
})
)
}}
onRemoveBuiltin={(modelId) => {
const name =
embeddingModels?.catalog.find(
(candidate) => candidate.id === modelId
)?.displayName ?? modelId
void runEmbeddingModelOperation(
modelId,
undefined,
(api) => api.removeModel(modelId),
i18n.t('embedding.notifications.removed', {
ns: 'settingsSections',
name
})
)
}}
onOpenModelDownloadSourceSettings={openModelDownloadSource}
onSelectConnection={setEditingEmbeddingConnectionId}
onSetCurrent={setActiveEmbeddingConnectionId}
onTestConnection={(connectionId) => {
void runEmbeddingDiagnostic(connectionId)
}}
onUpdateConnection={(connectionId, changes) => {
setEmbeddingConnections((connections) =>
connections.map((connection) =>
connection.id === connectionId
? {
...connection,
...(changes.name === undefined
? {}
: { name: changes.name }),
...(changes.endpoint === undefined
? {}
: { baseUrl: changes.endpoint }),
...(changes.model === undefined
? {}
: { modelName: changes.model }),
...(changes.authentication === undefined
? {}
: {
authentication:
changes.authentication
}),
...(changes.apiKey === undefined
? {}
: { apiKey: changes.apiKey }),
...(changes.clearApiKey === undefined
? {}
: {
clearApiKey: changes.clearApiKey
})
}
: connection
)
)
}}
/>
)}
{modelType === 'rerank' && (
<div className="settings-section">
<div className="settings-section__title">
<KeyRound size={17} />
<div>
<strong>{t('model.rerank.title')}</strong>
<small>{t('model.rerank.description')}</small>
</div>
</div>
<div className="runtime-note model-service-form">
<label className="toggle-row">
<input
checked={knowledgeRerankEnabled}
onChange={(event) =>
setKnowledgeRerankEnabled(event.target.checked)
}
role="switch"
type="checkbox"
/>
<span>{t('model.rerank.enabled')}</span>
</label>
<div className="field">
<span className="inline-help-label">
<label htmlFor="model-rerank-endpoint">{t('model.rerank.endpoint')}</label>
<InlineHelp label={t('model.rerank.endpoint')} id="model-rerank-endpoint-help">{t('model.rerank.endpointDescription')}</InlineHelp>
</span>
<input
id="model-rerank-endpoint"
aria-describedby="model-rerank-endpoint-help"
aria-label={t('model.rerank.endpoint')}
disabled={!knowledgeRerankEnabled}
inputMode="url"
onChange={(event) =>
setKnowledgeRerankEndpoint(event.target.value)
}
placeholder="https://api.cohere.com/v1/rerank"
value={knowledgeRerankEndpoint}
/>
</div>
<label className="field">
<span>{t('model.rerank.modelName')}</span>
<input
aria-label={t('model.rerank.modelName')}
disabled={!knowledgeRerankEnabled}
onChange={(event) =>
setKnowledgeRerankModel(event.target.value)
}
value={knowledgeRerankModel}
/>
</label>
<label className="field">
<span>{t('model.rerank.optionalApiKey')}</span>
<input
aria-label={t('model.rerank.optionalApiKey')}
autoComplete="off"
disabled={
!knowledgeRerankEnabled ||
settings?.knowledgeRerankCredentialSource ===
'environment' ||
!settings?.secureStorageAvailable
}
onChange={(event) => {
setKnowledgeRerankApiKey(event.target.value)
setClearKnowledgeRerankApiKey(false)
}}
placeholder={
settings?.knowledgeRerankApiKeyConfigured
? t('credentials.configuredPlaceholder')
: t('model.rerank.optionalApiKeyPlaceholder')
}
type="password"
value={knowledgeRerankApiKey}
/>
</label>
<div className="credential-state" aria-live="polite">
<LockKeyhole size={15} />
<span>
{settings
? t(
`credentials.${settings.knowledgeRerankCredentialSource ?? 'none'}`
)
: t('credentials.none')}
</span>
{settings?.knowledgeRerankCredentialSource ===
'encrypted' && (
<button
onClick={() => {
setKnowledgeRerankApiKey('')
setClearKnowledgeRerankApiKey(true)
}}
type="button"
>
{clearKnowledgeRerankApiKey
? t('actions.clearAfterSave')
: t('actions.clearCredential')}
</button>
)}
</div>
<small>{t('model.rerank.privacyDescription')}</small>
</div>
</div>
)}
{modelType === 'speech' && (
<SpeechModelSettingsSection
downloadSource={modelDownloadSource}
onNotify={onNotify}
onOpenModelDownloadSourceSettings={openModelDownloadSource}
onSelectedModelIdChange={(modelId, changed) => {
setSpeechModelDraftId(modelId)
setSpeechModelSelectionDirty(changed)
}}
onSelectionInvalidated={(modelId) => {
setSpeechModelDraftId(modelId)
setPersistedSpeechModelId(modelId)
setSpeechModelSelectionDirty(false)
}}
persistedSelectedModelId={persistedSpeechModelId}
selectedModelId={speechModelDraftId}
/>
)}
</>
)}
{activeTab === 'context-control' && (
<>
<div className="settings-section">
<div className="field">
<div className="inline-help-label">
<label className="toggle-row">
<input
aria-describedby="context-compression-help"
checked={contextCompression.enabled}
onChange={(event) =>
setContextCompression((current) => ({
...current,
enabled: event.target.checked
}))
}
role="switch"
type="checkbox"
/>
<span>{t('contextControl.enabled')}</span>
</label>
<InlineHelp label={t('contextControl.enabled')} id="context-compression-help">{t('contextControl.enabledDescription')}</InlineHelp>
</div>
<small>{t('contextControl.usageNotice')}</small>
</div>
</div>
<div className="settings-section">
<label className="field">
<span>{t('contextControl.triggerTokens')}</span>
<input
aria-label={t('contextControl.triggerTokens')}
disabled={!contextCompression.enabled}
inputMode="numeric"
max={maximumContextCompressionTriggerThousands}
min={minimumContextCompressionTriggerThousands}
onBlur={commitContextCompressionTokenInput}
onChange={(event) =>
setContextCompressionTokenInput((current) => ({
...current,
trigger: event.target.value
}))
}
required
step={1}
type="number"
value={contextCompressionTokenInput.trigger}
/>
<small>
{t('contextControl.triggerTokensDescription', {
tokens:
normalizedContextCompression.triggerTokens
.toLocaleString(i18n.language)
})}
</small>
</label>
<label className="field">
<span>{t('contextControl.recentRawTokens')}</span>
<input
aria-label={t('contextControl.recentRawTokens')}
disabled={!contextCompression.enabled}
inputMode="numeric"
max={maximumContextCompressionRecentRawThousandsFor(
normalizedContextCompression.triggerTokens
)}
min={minimumContextCompressionRecentRawThousands}
onBlur={commitContextCompressionTokenInput}
onChange={(event) =>
setContextCompressionTokenInput((current) => ({
...current,
recentRaw: event.target.value
}))
}
required
step={1}
type="number"
value={contextCompressionTokenInput.recentRaw}
/>
<small>
{t('contextControl.recentRawTokensDescription', {
tokens:
normalizedContextCompression.recentRawTokens
.toLocaleString(i18n.language)
})}
</small>
</label>
<div className="field">
<span className="inline-help-label">
<label htmlFor="context-summary-model">{t('contextControl.summaryModel')}</label>
<InlineHelp label={t('contextControl.summaryModel')} id="context-summary-model-help">{t('contextControl.summaryModelDescription')}</InlineHelp>
</span>
<select
id="context-summary-model"
aria-describedby="context-summary-model-help"
aria-label={t('contextControl.summaryModel')}
disabled={!contextCompression.enabled}
onChange={(event) =>
setContextCompression((current) => ({
...current,
modelSource:
event.target.value === 'current'
? { kind: 'current' }
: {
kind: 'profile',
profileId: event.target.value
}
}))
}
value={
contextCompression.modelSource.kind === 'current'
? 'current'
: contextCompression.modelSource.profileId
}
>
<option value="current">
{t('contextControl.currentModel')}
</option>
{modelProfiles
.filter((profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
.map((profile) => (
<option key={profile.id} value={profile.id}>
{modelProfileDisplayName(profile)} ·{' '}
{profile.modelName}
</option>
))}
</select>
</div>
<p className="settings-panel__description">
{t('contextControl.fixedTarget')}
</p>
</div>
<div className="runtime-note">
<p>{t('contextControl.modelLimits')}</p>
<button
className="secondary-button"
onClick={() => {
setModelType('llm')
requestTabChange('model')
}}
type="button"
>
{t('contextControl.manageModelLimits')}
</button>
</div>
<details className="settings-section">
<summary>{t('contextControl.advanced')}</summary>
<label className="field">
<span>{t('contextControl.summaryPrompt')}</span>
<textarea
disabled={!contextCompression.enabled}
onChange={(event) =>
setContextCompression((current) => ({
...current,
summaryPrompt: event.target.value
}))
}
rows={7}
value={contextCompression.summaryPrompt}
/>
<small>
{t('contextControl.summaryPromptDescription')}
</small>
</label>
<button
className="secondary-button"
disabled={
!contextCompression.enabled ||
contextCompression.summaryPrompt ===
defaultContextCompressionSettings.summaryPrompt
}
onClick={() =>
setContextCompression((current) => ({
...current,
summaryPrompt:
defaultContextCompressionSettings.summaryPrompt
}))
}
type="button"
>
{t('contextControl.restoreDefaultPrompt')}
</button>
</details>
</>
)}
{activeTab === 'document-parsing' && (
<DocumentParsingSettingsSection
onBusyChange={setTesting}
onDirtyChange={setDocumentParsingDirty}
onNotify={onNotify}
onOpenModelDownloadSourceSettings={openModelDownloadSource}
/>
)}
{remoteProjectsEnabled && activeTab === 'ssh-hosts' && (
<SshHostsSettingsSection
onDirtyChange={setSshHostsDirty}
onNotify={onNotify}
onProjectsDeleted={onProjectsDeleted}
/>
)}
{activeTab === 'security' && (
<>
<div className="settings-section">
<label className="field">
<span>{t('security.toolPolicy.label')}</span>
<select
aria-label={t('security.toolPolicy.label')}
value={toolApproval}
onChange={(event) =>
setToolApproval(
event.target.value as RuntimeSettingsInput['toolApproval']
)
}
>
<option value="always">
{t('security.toolPolicy.always')}
</option>
<option value="policy">
{t('security.toolPolicy.deny')}
</option>
</select>
<small>
{t('security.toolPolicy.description')}
</small>
</label>
</div>
<div className="settings-section settings-section--danger">
<div>
<strong>{t('security.localData.title')}</strong>
<p>
{t('security.localData.description')}
</p>
</div>
{confirmingClear ? (
<div className="danger-actions">
<button
className="secondary-button"
disabled={clearingLocalData}
onClick={() => setConfirmingClear(false)}
type="button"
>
{t('actions.cancel')}
</button>
<button
className="danger-button"
disabled={clearingLocalData}
onClick={() => {
setClearingLocalData(true)
setError(undefined)
void onClearLocalData()
.then(() => {
setConfirmingClear(false)
})
.catch((reason: unknown) => {
setError(
settingsErrorMessage(
reason,
t('errors.clearLocalData')
)
)
})
.finally(() => setClearingLocalData(false))
}}
type="button"
>
{clearingLocalData
? t('actions.clearing')
: t('actions.clearLocalData')}
</button>
</div>
) : (
<button
className="danger-button"
onClick={() => setConfirmingClear(true)}
type="button"
>
{t('actions.clearLocalData')}
</button>
)}
</div>
</>
)}
{activeTab === 'channels' && (
<ChannelSettingsSection
initialChannel={initialChannel}
onNotify={onNotify}
onUpdateProject={onUpdateProject}
projectList={projects}
/>
)}
{activeTab === 'roles' && (
<>
<div className="settings-section subagent-routing-settings">
<div className="settings-section__title">
<span className="inline-help-label">
<strong>{t('roles.smartRouting.title')}</strong>
<InlineHelp label={t('roles.smartRouting.title')} id="subagent-smart-routing-help">
{t('roles.smartRouting.description')}
<p>{t('roles.smartRouting.help')}</p>
</InlineHelp>
</span>
</div>
<label className="toggle-row">
<input
aria-describedby="subagent-smart-routing-help"
checked={subagentSmartRoutingEnabled}
onChange={(event) =>
setSubagentSmartRoutingEnabled(event.target.checked)
}
role="switch"
type="checkbox"
/>
<span>{t('roles.smartRouting.enabled')}</span>
</label>
</div>
<RolePromptSettingsSection
defaultModelProfileId={savedRoleDefaultModelProfileId}
modelProfiles={savedRoleModelProfiles}
onChanged={onExpertsChanged}
/>
</>
)}
{activeTab === 'capabilities' && (
<CapabilitiesAndToolsSettingsSection
onOpenImageModelSettings={() => {
if (requestTabChange('model')) {
setModelType('llm')
const imageProfile = modelProfiles.find(
(profile) => profile.protocol === 'openai-images-generations'
)
if (imageProfile) {
setSelectedModelProfileId(imageProfile.id)
}
}
}}
magicNotesEnabled={magicNotesEnabled}
onNotify={onNotify}
/>
)}
{activeTab === 'about' && (
<UpdateSettingsSection onNotify={onNotify} />
)}
</div>
</div>
</section>
</div>
)
return presentation === 'modal' ? createPortal(panel, document.body) : panel
}
/** Memoized: App re-renders on chat updates; this view re-renders only when its props change. */
export const SettingsPanel = memo(SettingsPanelView)