Files
goodbuddy/src/renderer/src/SettingsPanel.tsx
T
mesalogo a4b64a1641 feat: preview Agent HTML in conversations and unblock long replies
Agent replies that produced HTML could only be read as source, so users had
to copy the markup into an external file to see the result. Completed Agent
and Subagent output now renders complete HTML documents, single-root HTML
replies, and `html` / `htm` code blocks as an isolated static preview in
place. Each preview keeps a persistent notice that scripts and network are
disabled and offers the same icon actions as Mermaid diagrams: a source
toggle and a full-screen preview. The new "render HTML in conversations"
switch under Platform Features / General is on by default, applies to the
open conversation immediately, and falls back to HTML source when disabled.
Previews cannot run scripts, reach the network, submit forms, open windows,
or load remote media; author stylesheets and inline styles are preserved so
the intended layout survives, and remote resource loading is refused by the
preview CSP instead.

Direct-model replies no longer fail once they grow large. The byte caps on
streaming responses, single stream blocks, non-streaming replies, model tool
arguments, tool request context, aggregate tool results, and accumulated
answer text are removed, so users stop seeing "模型接口流式响应超过安全限制"
on long answers. Response length is now governed by the request timeout,
cancellation, and context compression. Image generation responses keep their
decodable-image validation.

A message sent after a failed or interrupted run stayed in the pending queue
instead of running. The conversation is now released whenever a run reaches
any terminal state, not only when the renderer reports readiness again, and a
dispatched queue item whose run never starts releases its reservation and
returns to pending. Later messages dispatch without extra user action.

Release note: Agent 回复中的 HTML 现在可直接在会话内预览,支持查看源码和全屏预览,默认开启并可在“平台功能 / 通用设置”关闭;直连模型的长回复不再因大小限制中断;上一次运行失败或被中断后,新发送的消息会立即执行而不再排队等待。
2026-09-05 09:10:44 +08:00

4101 lines
146 KiB
TypeScript

import {
FolderOpen,
KeyRound,
LockKeyhole,
Plus,
Save,
SunMoon,
TerminalSquare,
Trash2,
X
} from 'lucide-react'
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next'
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,
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 { 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
}
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
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
brandingPreferences?: BrandingPreferences
brandingFallbackLogo?: string
onBrandingPreferencesChange?: (
preferences: BrandingPreferences
) => boolean
magicNotesEnabled?: boolean
remoteProjectsEnabled?: boolean
onConversationHtmlRenderingEnabledChange?: (
enabled: boolean
) => void
onMagicNotesEnabledChange?: (enabled: boolean) => void
onRemoteProjectsEnabledChange?: (enabled: boolean) => void
onMagicNotesShowIncompleteTodoCountChange?: (
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,
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
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.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>
)
}
export function SettingsPanel({
open,
presentation = 'modal',
initialCategory,
initialChannel,
onClose,
onProjectsDeleted = () => {},
onSaved,
onNotify = () => {},
onUpdateProject,
projects,
onClearLocalData,
onExpertsChanged = () => {},
appearanceTheme = 'system',
onAppearanceThemeChange = () => {},
brandingPreferences = defaultBrandingPreferences,
brandingFallbackLogo,
onBrandingPreferencesChange = () => false,
magicNotesEnabled = false,
remoteProjectsEnabled = false,
onConversationHtmlRenderingEnabledChange = () => {},
onMagicNotesEnabledChange = () => {},
onRemoteProjectsEnabledChange = () => {},
onMagicNotesShowIncompleteTodoCountChange = () => {},
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 [selectedModelProfileId, setSelectedModelProfileId] =
useState('')
const [defaultModelProfileId, setDefaultModelProfileId] = useState('')
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 [embeddingModels, 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 [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 }
| { kind: 'external'; proceed: () => void }
>()
const runtimeCustomizationRef =
useRef<RuntimeCustomizationSectionHandle>(null)
const keepEditingRef = 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 => {
hydrateRuntimeSettings(
value,
{
settings: setSettings,
provider: setProvider,
modelProfiles: setModelProfiles,
selectedModelProfileId: setSelectedModelProfileId,
defaultModelProfileId: setDefaultModelProfileId,
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,
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,
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
): boolean => {
if (category === activeTab) {
return true
}
if (navigationWouldLoseDraft) {
leaveTriggerRef.current =
trigger ??
(document.activeElement instanceof HTMLElement
? document.activeElement
: undefined)
setPendingLeave({ kind: 'navigate', category })
return false
}
setPendingLeave(undefined)
setError(undefined)
setActiveTab(category)
return 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 (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)
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])
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
])
)
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) {
throw new Error(
t('errors.invalidModelRequestHeaders', {
name: modelProfileDisplayName(profile)
})
)
}
const requestBody = parseRequestBodyDraft(
profile.requestBodyText
)
if (!requestBody.success) {
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,
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)
: ({ action: 'keep' } as const)
}
})
const defaultProfileInput =
profileInputs.find(
(profile) => profile.id === defaultProfile.id
) ?? profileInputs[0]!
const normalizedDeepseekHarnessModelSource =
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === defaultProfile.id &&
defaultProfile.credentialSource === 'environment'
? { kind: 'platform' as const }
: deepseekHarnessModelSource
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,
modelProfiles: profileInputs,
defaultModelProfileId: defaultProfile.id,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource:
normalizedDeepseekHarnessModelSource,
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
}
: { 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 => {
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,
maximumOutputTokens: defaultAnthropicMaximumOutputTokens,
imageGenerationQuality:
defaultRuntimeSettings.imageGenerationQuality,
requestHeadersText: '{}',
requestBodyText: '{}',
apiKeyConfigured: false,
credentialSource: 'none',
apiKey: '',
clearApiKey: false
}
])
if (!defaultModelProfileId) {
setDefaultModelProfileId(id)
}
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 => {
const previousDefaultProfileId = defaultModelProfileId
const compatibleProfile = findPreferredCompatibleModelProfile(
modelProfiles,
profile.id,
(candidate) =>
isAgentRuntimeModelProtocol(candidate.protocol)
)
const nextRuntimeSource: RuntimeModelSource = compatibleProfile
? { kind: 'profile', profileId: compatibleProfile.id }
: { kind: 'platform' }
setDefaultModelProfileId(profile.id)
if (
opencodeModelSource.kind === 'profile' &&
opencodeModelSource.profileId === previousDefaultProfileId
) {
setOpencodeModelSource(nextRuntimeSource)
}
if (
continueModelSource.kind === 'profile' &&
continueModelSource.profileId === previousDefaultProfileId
) {
setContinueModelSource(nextRuntimeSource)
}
if (
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === previousDefaultProfileId &&
profile.protocol === 'openai-chat-completions'
) {
setDeepseekHarnessModelSource({
kind: 'profile',
profileId: profile.id
})
}
}
const parseModelSource = (value: string): RuntimeModelSource =>
value === 'platform'
? { kind: 'platform' }
: { 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 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))
)
: 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
return (
<div
className={
presentation === 'page'
? 'settings-page'
: 'settings-backdrop'
}
role="presentation"
>
<section
aria-labelledby="settings-title"
aria-modal={presentation === 'modal' ? 'true' : undefined}
className="settings-panel"
role={presentation === 'modal' ? 'dialog' : 'region'}
>
<div className="settings-panel__header">
<div>
<PageHeader
actions={
<button
aria-label={t('center.close')}
className="icon-button"
onClick={(event) => close(event.currentTarget)}
type="button"
>
<X aria-hidden="true" size={19} />
</button>
}
description={t('center.description')}
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) => (
<button
aria-controls={`settings-panel-${category.id}`}
aria-label={t(
`categories.${category.translationKey}.label`
)}
aria-selected={activeTab === category.id}
id={`settings-tab-${category.id}`}
key={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>
<small>
{t(
`categories.${category.translationKey}.navigationDescription`
)}
</small>
</button>
))}
</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}
/>
)}
{activeTab === 'appearance' && (
<>
<div className="settings-section appearance-settings">
<div className="settings-section__title">
<SunMoon size={17} />
<div>
<strong>{t('appearance.theme.title')}</strong>
<small>{t('appearance.theme.description')}</small>
</div>
</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 appearance-settings">
<div className="settings-section__title">
<div>
<strong>{t('appearance.language.title')}</strong>
<small>{t('appearance.language.description')}</small>
</div>
</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
}
onMagicNotesEnabledChange={onMagicNotesEnabledChange}
onMagicNotesShowIncompleteTodoCountChange={
onMagicNotesShowIncompleteTodoCountChange
}
onRemoteProjectsEnabledChange={
onRemoteProjectsEnabledChange
}
onNotify={onNotify}
onShortcutSettingsChanged={onShortcutSettingsChanged}
/>
)}
{activeTab === 'runtime' && (
<>
<SettingsWarningList warnings={settings?.warnings} />
<div className="settings-section">
<div className="settings-section__title">
<FolderOpen size={17} />
<div>
<strong>{t('runtime.workspace.title')}</strong>
<small>{t('runtime.workspace.description')}</small>
</div>
</div>
<label className="field">
<span>{t('runtime.workspace.directoryLabel')}</span>
<div className="workspace-picker">
<input
aria-label={t('runtime.workspace.directoryLabel')}
onChange={(event) => setWorkspacePath(event.target.value)}
value={workspacePath}
/>
<button
className="secondary-button"
onClick={() => {
void window.goodbuddy.settings
.selectWorkspace()
.then((selected) => {
if (selected) {
setWorkspacePath(selected)
}
})
.catch((reason: unknown) => {
setError(
settingsErrorMessage(
reason,
t('errors.selectWorkspace')
)
)
})
}}
type="button"
>
{t('actions.select')}
</button>
</div>
</label>
</div>
<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>
{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 === 'profile'}
disabled={!defaultTextModelProfile}
name="opencode-model-source"
onChange={() => {
if (defaultTextModelProfile) {
setOpencodeModelSource({
kind: 'profile',
profileId: defaultTextModelProfile.id
})
setOpencodeBaseUrl('')
}
}}
type="radio"
/>
<span>
<strong>{t('runtime.followRecommended')}</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 === 'profile' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`OpenCode ${t(
'runtime.goodBuddyConnection'
)}`}
value={opencodeModelSource.profileId}
onChange={(event) => {
setOpencodeModelSource(
parseModelSource(event.target.value)
)
setOpencodeBaseUrl('')
}}
>
{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 === 'profile'}
disabled={!defaultTextModelProfile}
name="continue-model-source"
onChange={() => {
if (defaultTextModelProfile) {
setContinueModelSource({
kind: 'profile',
profileId: defaultTextModelProfile.id
})
}
}}
type="radio"
/>
<span>
<strong>{t('runtime.followRecommended')}</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 === 'profile' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`Continue ${t(
'runtime.goodBuddyConnection'
)}`}
value={continueModelSource.profileId}
onChange={(event) =>
setContinueModelSource(
parseModelSource(event.target.value)
)
}
>
{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'
? t('runtime.deepseekHarness.managedSource')
: 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'
})}
/>
<label className="field">
<span>{t('runtime.deepseekHarness.connection')}</span>
<select
aria-label={`DeepSeek Harness ${t(
'runtime.deepseekHarness.connection'
)}`}
disabled={!defaultDeepseekHarnessModelProfile}
onChange={(event) =>
setDeepseekHarnessModelSource(
parseModelSource(event.target.value)
)
}
value={
deepseekHarnessModelSource.kind === 'profile'
? deepseekHarnessModelSource.profileId
: ''
}
>
<option disabled value="">
{t(
'runtime.deepseekHarness.connectionPlaceholder'
)}
</option>
{modelProfiles.map((profile) => (
<option
disabled={!isDeepseekHarnessCompatible(profile)}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isDeepseekHarnessCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
<small>
{t(
'runtime.deepseekHarness.connectionDescription'
)}
</small>
</label>
<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' && (
<>
<div className="model-type-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}
/>
</div>
{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"
onClick={addModelProfile}
type="button"
>
<Plus size={14} />
{t('actions.addCustom')}
</button>
</div>
<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' && (
<span>{t('model.profile.imageBadge')}</span>
)}
</span>
</button>
</div>
))}
</div>
</aside>
{selectedModelProfile &&
(() => {
const profile = selectedModelProfile
const environmentManaged =
profile.credentialSource === 'environment'
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.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"
onChange={(event) =>
updateModelProfile(profile.id, {
baseUrl: event.target.value
})
}
placeholder="https://api.example.com/v1"
value={profile.baseUrl}
/>
</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>
{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>
)}
{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>
<label className="field">
<span>{t('model.profile.contextWindow')}</span>
<input
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
}
/>
<small>
{t('model.profile.contextWindowDescription')}
</small>
</label>
{profile.protocol === 'anthropic-messages' && (
<label className="field">
<span>{t('model.profile.maximumOutput')}</span>
<input
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
}
/>
<small>
{t('model.profile.maximumOutputDescription')}
</small>
</label>
)}
</>
)}
{profile.protocol ===
'openai-images-generations' && (
<label className="field">
<span>{t('model.profile.imageQuality')}</span>
<select
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>
<small>
{t('model.profile.imageQualityDescription')}
</small>
</label>
)}
<label className="field">
<span>{t('model.profile.requestHeaders')}</span>
<textarea
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}
/>
<small
id={`model-request-headers-help-${profile.id}`}
>
{t('model.profile.requestHeadersDescription')}
</small>
{!requestHeadersValid && (
<small className="field-error" role="alert">
{t('model.profile.requestHeadersInvalid')}
</small>
)}
</label>
<label className="field">
<span>{t('model.profile.requestBody')}</span>
<textarea
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}
/>
<small
id={`model-request-body-help-${profile.id}`}
>
{t('model.profile.requestBodyDescription')}
</small>
{!requestBodyValid && (
<small className="field-error" role="alert">
{t('model.profile.requestBodyInvalid')}
</small>
)}
</label>
<p className="settings-warning">
{t('model.profile.requestCustomizationWarning')}
</p>
<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: isContinueCompatible(profile)
? t('model.profile.compatible')
: t('model.profile.incompatible'),
openCodeCompatibility: isOpenCodeCompatible(profile)
? 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={() =>
requestTabChange('platform-features')
}
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>
<label className="field">
<span>{t('model.rerank.endpoint')}</span>
<input
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}
/>
<small>{t('model.rerank.endpointDescription')}</small>
</label>
<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
onNotify={onNotify}
onOpenModelDownloadSourceSettings={() =>
requestTabChange('platform-features')
}
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">
<label className="toggle-row">
<input
checked={contextCompression.enabled}
onChange={(event) =>
setContextCompression((current) => ({
...current,
enabled: event.target.checked
}))
}
role="switch"
type="checkbox"
/>
<span>{t('contextControl.enabled')}</span>
</label>
<small>{t('contextControl.enabledDescription')}</small>
<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>
<label className="field">
<span>{t('contextControl.summaryModel')}</span>
<select
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>
<small>{t('contextControl.summaryModelDescription')}</small>
</label>
<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
onDirtyChange={setDocumentParsingDirty}
onNotify={onNotify}
onOpenModelDownloadSourceSettings={() =>
requestTabChange('platform-features')
}
/>
)}
{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">
<div>
<strong>{t('roles.smartRouting.title')}</strong>
<small>
{t('roles.smartRouting.description')}
</small>
</div>
</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>
<small id="subagent-smart-routing-help">
{t('roles.smartRouting.help')}
</small>
</div>
<RolePromptSettingsSection
defaultModelProfileId={savedRoleDefaultModelProfileId}
modelProfiles={savedRoleModelProfiles}
onChanged={onExpertsChanged}
/>
</>
)}
{activeTab === 'capabilities' && (
<CapabilitiesAndToolsSettingsSection
magicNotesEnabled={magicNotesEnabled}
onNotify={onNotify}
/>
)}
{activeTab === 'about' && (
<UpdateSettingsSection onNotify={onNotify} />
)}
</div>
</div>
</section>
</div>
)
}