fix: harden scoped tools and settings persistence
This commit is contained in:
@@ -740,6 +740,8 @@ describe('App', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Chat' })
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('Desktop workspace')).toBeInTheDocument()
|
||||
expect(screen.getByText('GOODBUDDY WORKSPACE')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: 'What would you like to accomplish today?'
|
||||
@@ -760,6 +762,13 @@ describe('App', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('renders localized workspace branding in Chinese', async () => {
|
||||
render(<App />)
|
||||
|
||||
expect(await screen.findByText('桌面工作区')).toBeInTheDocument()
|
||||
expect(screen.getByText('GOODBUDDY 工作台')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps Settings open when the interface language changes', async () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
@@ -3278,7 +3287,7 @@ describe('App', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes a legacy Auto conversation to the explicit default Runtime', async () => {
|
||||
it('preserves a legacy Auto conversation without silently persisting a replacement', async () => {
|
||||
vi.mocked(api.conversations.list).mockResolvedValueOnce([
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000020',
|
||||
@@ -3306,17 +3315,14 @@ describe('App', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: '00000000-0000-4000-8000-000000000020',
|
||||
runtimeSelection: {
|
||||
provider: 'model',
|
||||
profileId: modelProfileId
|
||||
}
|
||||
runtimeSelection: { provider: 'auto' }
|
||||
})
|
||||
])
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('rebinds a loaded conversation when its model profile was removed', async () => {
|
||||
it('keeps a removed model selection visible until the user replaces it', async () => {
|
||||
const removedProfileId =
|
||||
'00000000-0000-4000-8000-000000000099'
|
||||
vi.mocked(api.conversations.list).mockResolvedValueOnce([
|
||||
@@ -3341,9 +3347,6 @@ describe('App', () => {
|
||||
])
|
||||
render(<App />)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /默认模型.*sonnet-5/u })
|
||||
).toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(api.conversations.replace).toHaveBeenLastCalledWith(
|
||||
expect.arrayContaining([
|
||||
@@ -3351,7 +3354,7 @@ describe('App', () => {
|
||||
id: '00000000-0000-4000-8000-000000000022',
|
||||
runtimeSelection: {
|
||||
provider: 'model',
|
||||
profileId: modelProfileId
|
||||
profileId: removedProfileId
|
||||
}
|
||||
})
|
||||
])
|
||||
|
||||
+31
-106
@@ -65,9 +65,12 @@ import { maximumPastedImageBytes } from '../../shared/contracts'
|
||||
import {
|
||||
agentRuntimeSelectionKey,
|
||||
agentRuntimeSelectionSchema,
|
||||
repairAgentRuntimeSelection,
|
||||
type AgentRuntimeSelection
|
||||
} from '../../shared/runtime-selection-contracts'
|
||||
import {
|
||||
getDefaultRuntimeSelection,
|
||||
getRuntimeSelectionForProvider
|
||||
} from './runtime-selection'
|
||||
import type {
|
||||
AssistantProject,
|
||||
AssistantArtifact,
|
||||
@@ -802,45 +805,6 @@ function mergeArtifacts(
|
||||
)
|
||||
}
|
||||
|
||||
function getDefaultRuntimeSelection(
|
||||
settings: RuntimeSettings
|
||||
): AgentRuntimeSelection {
|
||||
if (settings.provider === 'model') {
|
||||
return {
|
||||
provider: 'model',
|
||||
profileId: settings.defaultModelProfileId
|
||||
}
|
||||
}
|
||||
if (settings.provider === 'opencode') {
|
||||
return {
|
||||
provider: 'opencode',
|
||||
...(settings.opencodeModelSource.kind === 'profile'
|
||||
? { profileId: settings.opencodeModelSource.profileId }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
if (settings.provider === 'continue') {
|
||||
return {
|
||||
provider: 'continue',
|
||||
...(settings.continueModelSource.kind === 'profile'
|
||||
? { profileId: settings.continueModelSource.profileId }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
if (settings.opencodeBaseUrl || settings.opencodeEmbedded) {
|
||||
return {
|
||||
provider: 'opencode',
|
||||
...(settings.opencodeModelSource.kind === 'profile'
|
||||
? { profileId: settings.opencodeModelSource.profileId }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
return {
|
||||
provider: 'model',
|
||||
profileId: settings.defaultModelProfileId
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectDefaultRuntimeSelection(
|
||||
project: AssistantProject | undefined,
|
||||
settings: RuntimeSettings
|
||||
@@ -848,7 +812,7 @@ function getProjectDefaultRuntimeSelection(
|
||||
const selection = project?.runtimeSelection
|
||||
return !selection || selection.provider === 'auto'
|
||||
? getDefaultRuntimeSelection(settings)
|
||||
: repairAgentRuntimeSelection(selection, settings)
|
||||
: selection
|
||||
}
|
||||
|
||||
function getRuntimeSelectionLabel(
|
||||
@@ -859,6 +823,7 @@ function getRuntimeSelectionLabel(
|
||||
directModel: string
|
||||
automatic: string
|
||||
automaticSelection: string
|
||||
modelUnavailable: string
|
||||
}
|
||||
): string {
|
||||
if (!selection || !settings) {
|
||||
@@ -870,36 +835,36 @@ function getRuntimeSelectionLabel(
|
||||
(candidate) => candidate.id === selection.profileId
|
||||
)
|
||||
: undefined
|
||||
const requestedProfileMissing =
|
||||
'profileId' in selection &&
|
||||
Boolean(selection.profileId) &&
|
||||
profile === undefined
|
||||
if (selection.provider === 'model') {
|
||||
return profile
|
||||
? `${profile.name} · ${profile.modelName}`
|
||||
: status?.label ?? labels.directModel
|
||||
: requestedProfileMissing
|
||||
? labels.modelUnavailable
|
||||
: status?.label ?? labels.directModel
|
||||
}
|
||||
if (selection.provider === 'opencode') {
|
||||
return profile ? `OpenCode · ${profile.name}` : 'OpenCode'
|
||||
return profile
|
||||
? `OpenCode · ${profile.name}`
|
||||
: requestedProfileMissing
|
||||
? `OpenCode · ${labels.modelUnavailable}`
|
||||
: 'OpenCode'
|
||||
}
|
||||
if (selection.provider === 'continue') {
|
||||
return profile ? `Continue · ${profile.name}` : 'Continue'
|
||||
return profile
|
||||
? `Continue · ${profile.name}`
|
||||
: requestedProfileMissing
|
||||
? `Continue · ${labels.modelUnavailable}`
|
||||
: 'Continue'
|
||||
}
|
||||
return status
|
||||
? `${labels.automatic} · ${status.label}`
|
||||
: labels.automaticSelection
|
||||
}
|
||||
|
||||
function getConfiguredAgentRuntimeSelection(
|
||||
settings: RuntimeSettings,
|
||||
provider: 'opencode' | 'continue'
|
||||
): AgentRuntimeSelection {
|
||||
const source =
|
||||
provider === 'opencode'
|
||||
? settings.opencodeModelSource
|
||||
: settings.continueModelSource
|
||||
return {
|
||||
provider,
|
||||
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function getConfiguredAgentRuntimeSource(
|
||||
settings: RuntimeSettings,
|
||||
provider: 'opencode' | 'continue',
|
||||
@@ -910,7 +875,7 @@ function getConfiguredAgentRuntimeSource(
|
||||
useOwnConfiguration: (runtime: string) => string
|
||||
}
|
||||
): { label: string; detail: string } {
|
||||
const selection = getConfiguredAgentRuntimeSelection(settings, provider)
|
||||
const selection = getRuntimeSelectionForProvider(provider, settings)
|
||||
const profile =
|
||||
'profileId' in selection
|
||||
? settings.modelProfiles.find(
|
||||
@@ -1766,7 +1731,8 @@ function App(): React.JSX.Element {
|
||||
() => ({
|
||||
directModel: t('runtime.directModel'),
|
||||
automatic: t('runtime.automatic'),
|
||||
automaticSelection: t('runtime.automaticSelection')
|
||||
automaticSelection: t('runtime.automaticSelection'),
|
||||
modelUnavailable: t('runtime.modelUnavailable')
|
||||
}),
|
||||
[t]
|
||||
)
|
||||
@@ -1787,10 +1753,10 @@ function App(): React.JSX.Element {
|
||||
runtimeLabels
|
||||
)
|
||||
const openCodeMenuSelection = runtimeSettings
|
||||
? getConfiguredAgentRuntimeSelection(runtimeSettings, 'opencode')
|
||||
? getRuntimeSelectionForProvider('opencode', runtimeSettings)
|
||||
: undefined
|
||||
const continueMenuSelection = runtimeSettings
|
||||
? getConfiguredAgentRuntimeSelection(runtimeSettings, 'continue')
|
||||
? getRuntimeSelectionForProvider('continue', runtimeSettings)
|
||||
: undefined
|
||||
const openCodeMenuSource = runtimeSettings
|
||||
? getConfiguredAgentRuntimeSource(
|
||||
@@ -1866,48 +1832,6 @@ function App(): React.JSX.Element {
|
||||
}
|
||||
}, [activeId, conversations])
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtimeSettings || !conversationStoreReady) {
|
||||
return
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
setConversations((current) => {
|
||||
let changed = false
|
||||
const next = current.map((conversation) => {
|
||||
const project = projects.find(
|
||||
(candidate) => candidate.id === conversation.projectId
|
||||
)
|
||||
const defaultSelection = getProjectDefaultRuntimeSelection(
|
||||
project,
|
||||
runtimeSettings
|
||||
)
|
||||
const selection =
|
||||
!conversation.runtimeSelection ||
|
||||
conversation.runtimeSelection.provider === 'auto'
|
||||
? defaultSelection
|
||||
: repairAgentRuntimeSelection(
|
||||
conversation.runtimeSelection,
|
||||
runtimeSettings
|
||||
)
|
||||
if (
|
||||
conversation.runtimeSelection &&
|
||||
agentRuntimeSelectionKey(conversation.runtimeSelection) ===
|
||||
agentRuntimeSelectionKey(selection)
|
||||
) {
|
||||
return conversation
|
||||
}
|
||||
changed = true
|
||||
return {
|
||||
...conversation,
|
||||
runtimeSelection: selection
|
||||
}
|
||||
})
|
||||
return changed ? next : current
|
||||
})
|
||||
}, 0)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [conversationStoreReady, projects, runtimeSettings])
|
||||
|
||||
useEffect(() => {
|
||||
const selection = activeRuntimeSelectionRef.current
|
||||
if (!selection || !runtimeSettings) {
|
||||
@@ -4762,7 +4686,7 @@ function App(): React.JSX.Element {
|
||||
</div>
|
||||
<div className="brand__copy">
|
||||
<strong>GoodBuddy</strong>
|
||||
<span>Desktop workspace</span>
|
||||
<span>{t('brand.desktopWorkspace')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5299,7 +5223,7 @@ function App(): React.JSX.Element {
|
||||
<div className="welcome__badge">
|
||||
<Sparkles size={18} />
|
||||
</div>
|
||||
<p className="eyebrow">GOODBUDDY WORKSPACE</p>
|
||||
<p className="eyebrow">{t('chat.welcome.eyebrow')}</p>
|
||||
<h1>{t('chat.welcome.title')}</h1>
|
||||
<p className="welcome__description">
|
||||
{t('chat.welcome.description')}
|
||||
@@ -6849,6 +6773,7 @@ function App(): React.JSX.Element {
|
||||
<SettingsPanel
|
||||
appearanceTheme={appearanceTheme}
|
||||
heartbeats={assistantHeartbeats}
|
||||
magicNotesEnabled={magicNotesEnabled}
|
||||
onAppearanceThemeChange={setAppearanceTheme}
|
||||
onClearLocalData={clearLocalData}
|
||||
onClose={() => setView('chat')}
|
||||
|
||||
@@ -35,7 +35,10 @@ import type { WeixinBindingSnapshot } from '../../shared/weixin-channel-contract
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import { PageTabs, SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
|
||||
type ChannelDraft = {
|
||||
enabled: boolean
|
||||
@@ -455,6 +458,8 @@ function ChannelEditor({
|
||||
<small>
|
||||
{settings.source === 'environment'
|
||||
? t('channels.credential.environmentSource')
|
||||
: settings.source === 'unreadable'
|
||||
? t('channels.credential.secretUnreadable')
|
||||
: settings.secretConfigured
|
||||
? t('channels.credential.secretSaved')
|
||||
: t('channels.credential.secretMissing')}
|
||||
@@ -1327,7 +1332,7 @@ export function ChannelSettingsSection({
|
||||
aria-label={t('channels.sectionAriaLabel')}
|
||||
className="settings-section channel-settings"
|
||||
>
|
||||
{snapshot.warning && <p className="settings-warning">{snapshot.warning}</p>}
|
||||
<SettingsWarningList warnings={snapshot.warnings} />
|
||||
|
||||
<div className="channel-settings__tabs">
|
||||
<PageTabs
|
||||
|
||||
@@ -199,6 +199,36 @@ describe('DocumentParsingSettingsSection', () => {
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
it.each([
|
||||
['zh-CN', '正在加载…'],
|
||||
['en-US', 'Loading…']
|
||||
] as const)('localizes the loading state in %s', async (locale, label) => {
|
||||
await changeUiLocale(locale)
|
||||
getSnapshot.mockImplementationOnce(
|
||||
() => new Promise(() => undefined)
|
||||
)
|
||||
|
||||
render(<DocumentParsingSettingsSection />)
|
||||
|
||||
expect(screen.getByText(label)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('localizes recovered document parsing settings warnings', async () => {
|
||||
await changeUiLocale('en-US')
|
||||
getSnapshot.mockResolvedValueOnce({
|
||||
...snapshot,
|
||||
warnings: [{ code: 'document-parsing-settings-recovered' }]
|
||||
})
|
||||
|
||||
render(<DocumentParsingSettingsSection />)
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/The document parsing settings file was corrupt/u
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows actual capability status and saves workflow settings', async () => {
|
||||
const onNotify = vi.fn()
|
||||
render(
|
||||
|
||||
@@ -29,7 +29,10 @@ import type {
|
||||
DocumentParsingTestPurpose
|
||||
} from '../../shared/document-parsing-contracts'
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
|
||||
type DocumentParsingSettingsSectionProps = {
|
||||
onNotify?: (notification: AppNotificationInput) => void
|
||||
@@ -402,7 +405,9 @@ export function DocumentParsingSettingsSection({
|
||||
error={error ?? unavailableError}
|
||||
/>
|
||||
{!error && !unavailableError && (
|
||||
<p className="settings-empty">Loading…</p>
|
||||
<p className="settings-empty">
|
||||
{t('documentParsing.loading')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -453,6 +458,7 @@ export function DocumentParsingSettingsSection({
|
||||
category="document-parsing"
|
||||
error={error}
|
||||
/>
|
||||
<SettingsWarningList warnings={snapshot.warnings} />
|
||||
{settingsDirty && (
|
||||
<p
|
||||
className="settings-notice"
|
||||
|
||||
@@ -31,7 +31,10 @@ import type {
|
||||
WebSearchTestResult
|
||||
} from '../../shared/capability-contracts'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
import { PageTabs } from './WorkspacePrimitives'
|
||||
|
||||
const configurableMcpTargets: RuntimeTarget[] = ['model']
|
||||
@@ -85,7 +88,11 @@ function editorFromServer(server: McpServerSummary): McpEditor {
|
||||
}
|
||||
}
|
||||
|
||||
export function McpSettingsSection(): React.JSX.Element {
|
||||
export function McpSettingsSection({
|
||||
magicNotesEnabled = false
|
||||
}: {
|
||||
magicNotesEnabled?: boolean
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation('integrations')
|
||||
const tRef = useRef(t)
|
||||
useEffect(() => {
|
||||
@@ -106,7 +113,6 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
disabled: t('mcp.diagnosticStatuses.disabled')
|
||||
}
|
||||
const [snapshot, setSnapshot] = useState<CapabilitySnapshot>()
|
||||
const [magicNotesEnabled, setMagicNotesEnabled] = useState(false)
|
||||
const [editor, setEditor] = useState<McpEditor>()
|
||||
const [busy, setBusy] = useState<string>()
|
||||
const [error, setError] = useState<string>()
|
||||
@@ -158,20 +164,6 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = window.goodbuddy.updates?.getSettings
|
||||
if (!getSettings) {
|
||||
return
|
||||
}
|
||||
void getSettings()
|
||||
.then((settings) => {
|
||||
setMagicNotesEnabled(settings.magicNotesEnabled)
|
||||
})
|
||||
.catch(() => {
|
||||
setMagicNotesEnabled(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorOpen) {
|
||||
return
|
||||
@@ -417,6 +409,7 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
error={!editor ? error : undefined}
|
||||
headingId="mcp-settings-heading"
|
||||
/>
|
||||
<SettingsWarningList warnings={snapshot?.warnings} />
|
||||
<PageTabs
|
||||
ariaLabel={t('mcp.tabs.ariaLabel')}
|
||||
idPrefix="mcp-settings"
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { MagicNoteCommentFormat } from '../../shared/magic-notes-contracts'
|
||||
import { SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
|
||||
type PlatformFeaturesSettingsSectionProps = {
|
||||
onMagicNotesEnabledChange: (enabled: boolean) => void
|
||||
@@ -116,6 +119,7 @@ export function PlatformFeaturesSettingsSection({
|
||||
error={error}
|
||||
headingId="platform-features-heading"
|
||||
/>
|
||||
<SettingsWarningList warnings={settings?.warnings} />
|
||||
<section
|
||||
aria-label={t('platformFeatures.label')}
|
||||
className="settings-section"
|
||||
|
||||
@@ -18,8 +18,11 @@ import {
|
||||
normalizeInteractiveWorkMode
|
||||
} from '../../shared/assistant-contracts'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import {
|
||||
getDefaultRuntimeSelection,
|
||||
getRuntimeSelectionForProvider
|
||||
} from './runtime-selection'
|
||||
|
||||
type ProjectSwitcherProps = {
|
||||
projects: AssistantProject[]
|
||||
@@ -36,43 +39,6 @@ type ProjectSwitcherProps = {
|
||||
) => Promise<AssistantProject>
|
||||
}
|
||||
|
||||
function runtimeSelectionForProvider(
|
||||
provider: 'model' | 'opencode' | 'continue',
|
||||
settings: RuntimeSettings
|
||||
): AgentRuntimeSelection {
|
||||
if (provider === 'model') {
|
||||
return {
|
||||
provider,
|
||||
profileId: settings.defaultModelProfileId
|
||||
}
|
||||
}
|
||||
const source =
|
||||
provider === 'opencode'
|
||||
? settings.opencodeModelSource
|
||||
: settings.continueModelSource
|
||||
return {
|
||||
provider,
|
||||
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRuntimeSelection(
|
||||
settings: RuntimeSettings
|
||||
): AgentRuntimeSelection {
|
||||
if (settings.provider === 'model') {
|
||||
return runtimeSelectionForProvider('model', settings)
|
||||
}
|
||||
if (settings.provider === 'opencode') {
|
||||
return runtimeSelectionForProvider('opencode', settings)
|
||||
}
|
||||
if (settings.provider === 'continue') {
|
||||
return runtimeSelectionForProvider('continue', settings)
|
||||
}
|
||||
return settings.opencodeBaseUrl || settings.opencodeEmbedded
|
||||
? runtimeSelectionForProvider('opencode', settings)
|
||||
: runtimeSelectionForProvider('model', settings)
|
||||
}
|
||||
|
||||
export function ProjectSwitcher({
|
||||
projects,
|
||||
activeProjectId,
|
||||
@@ -157,7 +123,7 @@ export function ProjectSwitcher({
|
||||
? draft
|
||||
: {
|
||||
...draft,
|
||||
runtimeSelection: defaultRuntimeSelection(runtimeSettings)
|
||||
runtimeSelection: getDefaultRuntimeSelection(runtimeSettings)
|
||||
}
|
||||
if (dialogMode === 'settings' && activeProject) {
|
||||
await onUpdate(activeProject.id, input)
|
||||
@@ -276,7 +242,7 @@ export function ProjectSwitcher({
|
||||
rootPath: '',
|
||||
defaultWorkMode: 'ask',
|
||||
runtimeSelection: runtimeSettings
|
||||
? defaultRuntimeSelection(runtimeSettings)
|
||||
? getDefaultRuntimeSelection(runtimeSettings)
|
||||
: undefined
|
||||
})
|
||||
restoreFocusTarget.current = 'create'
|
||||
@@ -308,7 +274,7 @@ export function ProjectSwitcher({
|
||||
runtimeSelection:
|
||||
activeProject.runtimeSelection ??
|
||||
(runtimeSettings
|
||||
? defaultRuntimeSelection(runtimeSettings)
|
||||
? getDefaultRuntimeSelection(runtimeSettings)
|
||||
: undefined)
|
||||
})
|
||||
restoreFocusTarget.current = 'settings'
|
||||
@@ -441,7 +407,7 @@ export function ProjectSwitcher({
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
runtimeSelection: runtimeSelectionForProvider(
|
||||
runtimeSelection: getRuntimeSelectionForProvider(
|
||||
event.target.value as
|
||||
| 'model'
|
||||
| 'opencode'
|
||||
@@ -454,7 +420,7 @@ export function ProjectSwitcher({
|
||||
draft.runtimeSelection?.provider === 'auto'
|
||||
? 'model'
|
||||
: (draft.runtimeSelection?.provider ??
|
||||
defaultRuntimeSelection(runtimeSettings)
|
||||
getDefaultRuntimeSelection(runtimeSettings)
|
||||
.provider)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
waitFor,
|
||||
within
|
||||
} from '@testing-library/react'
|
||||
import { useState } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AssistantExpert } from '../../shared/assistant-contracts'
|
||||
import type {
|
||||
@@ -569,6 +570,31 @@ describe('SettingsPanel runtime files', () => {
|
||||
screen.getByRole('radio', { name: /Use system language/u })
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('tab', { name: 'Model connections' })
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Edit model connection Default model'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Name')).toHaveValue('Default model')
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Save settings' })
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(updateRuntime).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
modelProfiles: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: modelProfileId,
|
||||
name: '默认模型'
|
||||
})
|
||||
])
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('tab', { name: 'Agent Runtime' })
|
||||
)
|
||||
@@ -590,6 +616,69 @@ describe('SettingsPanel runtime files', () => {
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not translate user-defined model connection names', async () => {
|
||||
const userProfileId = '00000000-0000-4000-8000-000000000099'
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
...runtimeSettings,
|
||||
modelProfiles: [
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
name: 'My renamed model'
|
||||
},
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
id: userProfileId,
|
||||
name: '默认模型'
|
||||
}
|
||||
]
|
||||
})
|
||||
await changeUiLocale('en-US')
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('tab', { name: 'Model connections' })
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Edit model connection My renamed model'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'Edit model connection 默认模型'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('localizes structured Runtime recovery warnings', async () => {
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
...runtimeSettings,
|
||||
warnings: [{ code: 'runtime-settings-recovered' }]
|
||||
})
|
||||
await changeUiLocale('en-US')
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByText(/The Runtime settings file was corrupt/u)
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggles the Magic Notes platform entry setting', async () => {
|
||||
const onMagicNotesEnabledChange = vi.fn()
|
||||
render(
|
||||
@@ -617,7 +706,6 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
)
|
||||
expect(onMagicNotesEnabledChange).toHaveBeenCalledWith(true)
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '保存后自动' })
|
||||
)
|
||||
@@ -638,6 +726,59 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('refreshes built-in Notes MCP after enabling Magic Notes', async () => {
|
||||
function Harness(): React.JSX.Element {
|
||||
const [magicNotesEnabled, setMagicNotesEnabled] = useState(false)
|
||||
return (
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
magicNotesEnabled={magicNotesEnabled}
|
||||
onMagicNotesEnabledChange={setMagicNotesEnabled}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
<Harness />
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
const noteServerToggle = await screen.findByRole('button', {
|
||||
name: '展开服务器 笔记'
|
||||
})
|
||||
expect(noteServerToggle.closest('article')).toHaveClass(
|
||||
'mcp-server-card--disabled'
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
|
||||
fireEvent.click(
|
||||
await screen.findByRole('switch', {
|
||||
name: '显示魔法笔记入口'
|
||||
})
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(updateApplicationSettings).toHaveBeenCalledWith({
|
||||
magicNotesEnabled: true
|
||||
})
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen
|
||||
.getByRole('button', { name: '展开服务器 笔记' })
|
||||
.closest('article')
|
||||
).not.toHaveClass('mcp-server-card--disabled')
|
||||
)
|
||||
expect(
|
||||
screen.getByText('内置 MCP Server · 按模式读写 · 按对话授权')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps page navigation beside an independently scrollable panel', () => {
|
||||
render(
|
||||
<SettingsPanel
|
||||
@@ -759,6 +900,77 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(screen.queryByText('设置已保存')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits configured model values while environment values are effective', async () => {
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
...runtimeSettings,
|
||||
modelBaseUrl: 'https://environment.example/v1',
|
||||
modelName: 'environment-model',
|
||||
apiKeyConfigured: true,
|
||||
credentialSource: 'environment',
|
||||
modelProfiles: [
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
baseUrl: 'https://environment.example/v1',
|
||||
modelName: 'environment-model',
|
||||
apiKeyConfigured: true,
|
||||
credentialSource: 'environment'
|
||||
}
|
||||
],
|
||||
configured: {
|
||||
modelProfiles: [
|
||||
{
|
||||
...runtimeSettings.modelProfiles[0]!,
|
||||
baseUrl: 'https://stored.example/v1',
|
||||
modelName: 'stored-model',
|
||||
apiKeyConfigured: true,
|
||||
credentialSource: 'environment'
|
||||
}
|
||||
],
|
||||
opencodeBaseUrl: '',
|
||||
opencodeBinaryPath: '',
|
||||
opencodeConfigPath: '',
|
||||
continueBinaryPath: '',
|
||||
continueConfigPath: '',
|
||||
workspacePath: 'C:\\Workspace',
|
||||
opencodeModelSource: runtimeSettings.opencodeModelSource,
|
||||
continueModelSource: runtimeSettings.continueModelSource
|
||||
}
|
||||
})
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
await screen.findByDisplayValue('C:\\Workspace')
|
||||
fireEvent.click(screen.getByRole('tab', { name: '模型连接' }))
|
||||
expect(
|
||||
await screen.findByDisplayValue('https://environment.example/v1')
|
||||
).toBeDisabled()
|
||||
expect(screen.getByDisplayValue('environment-model')).toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateRuntime).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modelBaseUrl: 'https://stored.example/v1',
|
||||
modelName: 'stored-model',
|
||||
modelProfiles: [
|
||||
expect.objectContaining({
|
||||
baseUrl: 'https://stored.example/v1',
|
||||
modelName: 'stored-model',
|
||||
apiKey: { action: 'keep' }
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('applies a speech model draft only when Settings is saved', async () => {
|
||||
render(
|
||||
<SettingsPanel
|
||||
|
||||
+259
-138
@@ -9,7 +9,7 @@ import {
|
||||
Trash2,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
AssistantExpert,
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
} from '../../shared/contracts'
|
||||
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
|
||||
import {
|
||||
defaultModelProfileId as builtInDefaultModelProfileId,
|
||||
defaultRuntimeSettings,
|
||||
isAgentRuntimeModelProtocol
|
||||
} from '../../shared/contracts'
|
||||
@@ -40,7 +41,10 @@ import { SpeechModelSettingsSection } from './SpeechModelSettingsSection'
|
||||
import { EmbeddingSettingsSection } from './EmbeddingSettingsSection'
|
||||
import { DocumentParsingSettingsSection } from './DocumentParsingSettingsSection'
|
||||
import { PageHeader, SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
import {
|
||||
settingsCategoryList,
|
||||
type SettingsCategoryId
|
||||
@@ -81,6 +85,7 @@ type SettingsPanelProps = {
|
||||
onRunHeartbeat: (heartbeatId: string) => Promise<void>
|
||||
appearanceTheme?: AppearanceTheme
|
||||
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
|
||||
magicNotesEnabled?: boolean
|
||||
onMagicNotesEnabledChange?: (enabled: boolean) => void
|
||||
}
|
||||
|
||||
@@ -120,6 +125,118 @@ function toModelProfileDrafts(
|
||||
}))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
runtimeSandboxMode: (
|
||||
value: RuntimeSettings['runtimeSandboxMode']
|
||||
) => void
|
||||
knowledgeEmbeddingEnabled: (value: boolean) => void
|
||||
knowledgeEmbeddingBaseUrl: (value: string) => void
|
||||
knowledgeEmbeddingModel: (value: string) => void
|
||||
knowledgeEmbeddingApiKey: (value: string) => void
|
||||
clearKnowledgeEmbeddingApiKey: (value: boolean) => 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
|
||||
},
|
||||
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.opencodeBaseUrl(configured.opencodeBaseUrl)
|
||||
setters.opencodeBinaryPath(configured.opencodeBinaryPath)
|
||||
setters.opencodeConfigPath(configured.opencodeConfigPath)
|
||||
setters.continueBinaryPath(configured.continueBinaryPath)
|
||||
setters.continueConfigPath(configured.continueConfigPath)
|
||||
setters.continueMode(value.continueMode)
|
||||
setters.runtimeSandboxMode(value.runtimeSandboxMode)
|
||||
setters.knowledgeEmbeddingEnabled(value.knowledgeEmbeddingEnabled)
|
||||
setters.knowledgeEmbeddingBaseUrl(value.knowledgeEmbeddingBaseUrl)
|
||||
setters.knowledgeEmbeddingModel(value.knowledgeEmbeddingModel)
|
||||
setters.knowledgeEmbeddingApiKey('')
|
||||
setters.clearKnowledgeEmbeddingApiKey(false)
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
type RuntimeConfigCardProps = {
|
||||
runtime: AgentRuntimeType
|
||||
runtimeLabel: string
|
||||
@@ -246,6 +363,7 @@ export function SettingsPanel({
|
||||
onExpertsChanged = () => {},
|
||||
appearanceTheme = 'system',
|
||||
onAppearanceThemeChange = () => {},
|
||||
magicNotesEnabled = false,
|
||||
onMagicNotesEnabledChange = () => {}
|
||||
}: SettingsPanelProps): React.JSX.Element | null {
|
||||
const { i18n, t } = useTranslation('settings')
|
||||
@@ -322,6 +440,13 @@ export function SettingsPanel({
|
||||
subagentSmartRoutingEnabled,
|
||||
setSubagentSmartRoutingEnabled
|
||||
] = useState(false)
|
||||
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 [embeddingConfiguration, setEmbeddingConfiguration] =
|
||||
@@ -350,6 +475,47 @@ export function SettingsPanel({
|
||||
const [agentRuntimeType, setAgentRuntimeType] =
|
||||
useState<AgentRuntimeType>('opencode')
|
||||
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,
|
||||
opencodeBaseUrl: setOpencodeBaseUrl,
|
||||
opencodeBinaryPath: setOpencodeBinaryPath,
|
||||
opencodeConfigPath: setOpencodeConfigPath,
|
||||
continueBinaryPath: setContinueBinaryPath,
|
||||
continueConfigPath: setContinueConfigPath,
|
||||
continueMode: setContinueMode,
|
||||
runtimeSandboxMode: setRuntimeSandboxMode,
|
||||
knowledgeEmbeddingEnabled: setKnowledgeEmbeddingEnabled,
|
||||
knowledgeEmbeddingBaseUrl: setKnowledgeEmbeddingBaseUrl,
|
||||
knowledgeEmbeddingModel: setKnowledgeEmbeddingModel,
|
||||
knowledgeEmbeddingApiKey: setKnowledgeEmbeddingApiKey,
|
||||
clearKnowledgeEmbeddingApiKey: setClearKnowledgeEmbeddingApiKey,
|
||||
knowledgeRerankEnabled: setKnowledgeRerankEnabled,
|
||||
knowledgeRerankEndpoint: setKnowledgeRerankEndpoint,
|
||||
knowledgeRerankModel: setKnowledgeRerankModel,
|
||||
knowledgeRerankApiKey: setKnowledgeRerankApiKey,
|
||||
clearKnowledgeRerankApiKey: setClearKnowledgeRerankApiKey,
|
||||
workspacePath: setWorkspacePath,
|
||||
toolApproval: setToolApproval,
|
||||
subagentSmartRoutingEnabled: setSubagentSmartRoutingEnabled
|
||||
},
|
||||
preserveSelectedProfile
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
const configurationTab =
|
||||
activeTab === 'model' ||
|
||||
activeTab === 'runtime' ||
|
||||
@@ -409,52 +575,7 @@ export function SettingsPanel({
|
||||
setPersistedSpeechModelId(undefined)
|
||||
setSpeechModelSelectionDirty(false)
|
||||
setAgentRuntimeType('opencode')
|
||||
setSettings(value)
|
||||
setProvider(value.provider)
|
||||
setModelProfiles(toModelProfileDrafts(value))
|
||||
setSelectedModelProfileId(
|
||||
value.modelProfiles.some(
|
||||
(profile) => profile.id === value.defaultModelProfileId
|
||||
)
|
||||
? value.defaultModelProfileId
|
||||
: value.modelProfiles[0]?.id ?? ''
|
||||
)
|
||||
setDefaultModelProfileId(value.defaultModelProfileId)
|
||||
setOpencodeModelSource(value.opencodeModelSource)
|
||||
setContinueModelSource(value.continueModelSource)
|
||||
setOpencodeBaseUrl(value.opencodeBaseUrl)
|
||||
setOpencodeBinaryPath(value.opencodeBinaryPath)
|
||||
setOpencodeConfigPath(value.opencodeConfigPath)
|
||||
setContinueBinaryPath(value.continueBinaryPath)
|
||||
setContinueConfigPath(value.continueConfigPath)
|
||||
setContinueMode(value.continueMode)
|
||||
setRuntimeSandboxMode(value.runtimeSandboxMode)
|
||||
setKnowledgeEmbeddingEnabled(value.knowledgeEmbeddingEnabled)
|
||||
setKnowledgeEmbeddingBaseUrl(value.knowledgeEmbeddingBaseUrl)
|
||||
setKnowledgeEmbeddingModel(value.knowledgeEmbeddingModel)
|
||||
setKnowledgeEmbeddingApiKey('')
|
||||
setClearKnowledgeEmbeddingApiKey(false)
|
||||
setKnowledgeRerankEnabled(
|
||||
value.knowledgeRerankEnabled ??
|
||||
defaultRuntimeSettings.knowledgeRerankEnabled
|
||||
)
|
||||
setKnowledgeRerankEndpoint(
|
||||
value.knowledgeRerankEndpoint ??
|
||||
defaultRuntimeSettings.knowledgeRerankEndpoint
|
||||
)
|
||||
setKnowledgeRerankModel(
|
||||
value.knowledgeRerankModel ??
|
||||
defaultRuntimeSettings.knowledgeRerankModel
|
||||
)
|
||||
setKnowledgeRerankApiKey('')
|
||||
setClearKnowledgeRerankApiKey(false)
|
||||
setWorkspacePath(value.workspacePath)
|
||||
setToolApproval(
|
||||
value.toolApproval === 'policy' ? 'policy' : 'always'
|
||||
)
|
||||
setSubagentSmartRoutingEnabled(
|
||||
value.subagentSmartRoutingEnabled
|
||||
)
|
||||
hydrateSettings(value)
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
setError(
|
||||
@@ -475,7 +596,7 @@ export function SettingsPanel({
|
||||
)
|
||||
)
|
||||
})
|
||||
}, [i18n, open])
|
||||
}, [hydrateSettings, i18n, open])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && settingsBodyRef.current) {
|
||||
@@ -550,32 +671,52 @@ export function SettingsPanel({
|
||||
if (!defaultProfile) {
|
||||
throw new Error(t('errors.requireModelConnection'))
|
||||
}
|
||||
const profileInputs = modelProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
baseUrl: profile.baseUrl,
|
||||
modelName: profile.modelName,
|
||||
protocol: profile.protocol,
|
||||
authentication: profile.authentication,
|
||||
supportsImageInput: profile.supportsImageInput,
|
||||
imageGenerationQuality: profile.imageGenerationQuality,
|
||||
apiKey: profile.clearApiKey
|
||||
? ({ action: 'clear' } as const)
|
||||
: profile.apiKey.trim()
|
||||
? ({
|
||||
action: 'replace',
|
||||
value: profile.apiKey.trim()
|
||||
} as const)
|
||||
: ({ action: 'keep' } as const)
|
||||
}))
|
||||
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
|
||||
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,
|
||||
imageGenerationQuality: profile.imageGenerationQuality,
|
||||
apiKey: profile.clearApiKey
|
||||
? ({ action: 'clear' } as const)
|
||||
: 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 value = await window.goodbuddy.settings.updateRuntime({
|
||||
provider,
|
||||
modelBaseUrl: defaultProfile.baseUrl,
|
||||
modelName: defaultProfile.modelName,
|
||||
modelProtocol: defaultProfile.protocol,
|
||||
modelAuthentication: defaultProfile.authentication,
|
||||
modelBaseUrl: defaultProfileInput.baseUrl,
|
||||
modelName: defaultProfileInput.modelName,
|
||||
modelProtocol: defaultProfileInput.protocol,
|
||||
modelAuthentication: defaultProfileInput.authentication,
|
||||
imageGenerationQuality:
|
||||
defaultProfile.imageGenerationQuality,
|
||||
defaultProfileInput.imageGenerationQuality,
|
||||
opencodeBaseUrl,
|
||||
opencodeEmbedded: !opencodeBaseUrl,
|
||||
opencodeBinaryPath,
|
||||
@@ -607,9 +748,7 @@ export function SettingsPanel({
|
||||
}
|
||||
: { action: 'keep' },
|
||||
workspacePath,
|
||||
apiKey: profileInputs.find(
|
||||
(profile) => profile.id === defaultProfile.id
|
||||
)!.apiKey,
|
||||
apiKey: defaultProfileInput.apiKey,
|
||||
modelProfiles: profileInputs,
|
||||
defaultModelProfileId: defaultProfile.id,
|
||||
opencodeModelSource,
|
||||
@@ -628,48 +767,7 @@ export function SettingsPanel({
|
||||
)
|
||||
selectedSpeechModelId = speechSnapshot.selectedModelId
|
||||
}
|
||||
setSettings(value)
|
||||
setModelProfiles(toModelProfileDrafts(value))
|
||||
setSelectedModelProfileId((selectedId) =>
|
||||
value.modelProfiles.some((profile) => profile.id === selectedId)
|
||||
? selectedId
|
||||
: value.defaultModelProfileId
|
||||
)
|
||||
setDefaultModelProfileId(value.defaultModelProfileId)
|
||||
setOpencodeModelSource(value.opencodeModelSource)
|
||||
setContinueModelSource(value.continueModelSource)
|
||||
setOpencodeBaseUrl(value.opencodeBaseUrl)
|
||||
setOpencodeBinaryPath(value.opencodeBinaryPath)
|
||||
setOpencodeConfigPath(value.opencodeConfigPath)
|
||||
setContinueBinaryPath(value.continueBinaryPath)
|
||||
setContinueConfigPath(value.continueConfigPath)
|
||||
setContinueMode(value.continueMode)
|
||||
setRuntimeSandboxMode(value.runtimeSandboxMode)
|
||||
setKnowledgeEmbeddingEnabled(value.knowledgeEmbeddingEnabled)
|
||||
setKnowledgeEmbeddingBaseUrl(value.knowledgeEmbeddingBaseUrl)
|
||||
setKnowledgeEmbeddingModel(value.knowledgeEmbeddingModel)
|
||||
setKnowledgeEmbeddingApiKey('')
|
||||
setClearKnowledgeEmbeddingApiKey(false)
|
||||
setKnowledgeRerankEnabled(
|
||||
value.knowledgeRerankEnabled ??
|
||||
defaultRuntimeSettings.knowledgeRerankEnabled
|
||||
)
|
||||
setKnowledgeRerankEndpoint(
|
||||
value.knowledgeRerankEndpoint ??
|
||||
defaultRuntimeSettings.knowledgeRerankEndpoint
|
||||
)
|
||||
setKnowledgeRerankModel(
|
||||
value.knowledgeRerankModel ??
|
||||
defaultRuntimeSettings.knowledgeRerankModel
|
||||
)
|
||||
setKnowledgeRerankApiKey('')
|
||||
setClearKnowledgeRerankApiKey(false)
|
||||
setToolApproval(
|
||||
value.toolApproval === 'policy' ? 'policy' : 'always'
|
||||
)
|
||||
setSubagentSmartRoutingEnabled(
|
||||
value.subagentSmartRoutingEnabled
|
||||
)
|
||||
hydrateSettings(value, true)
|
||||
if (speechModelSelectionDirty) {
|
||||
setSpeechModelDraftId(selectedSpeechModelId)
|
||||
setPersistedSpeechModelId(selectedSpeechModelId)
|
||||
@@ -987,7 +1085,10 @@ export function SettingsPanel({
|
||||
.filter((profile) =>
|
||||
isAgentRuntimeModelProtocol(profile.protocol)
|
||||
)
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
.map(({ id, name }) => ({
|
||||
id,
|
||||
name: modelProfileDisplayName({ id, name })
|
||||
}))
|
||||
const savedRoleDefaultModelProfileId =
|
||||
savedRoleModelProfiles.some(
|
||||
(profile) => profile.id === settings?.defaultModelProfileId
|
||||
@@ -1246,9 +1347,7 @@ export function SettingsPanel({
|
||||
)}
|
||||
{activeTab === 'runtime' && (
|
||||
<>
|
||||
{settings?.warning && (
|
||||
<p className="settings-warning">{settings.warning}</p>
|
||||
)}
|
||||
<SettingsWarningList warnings={settings?.warnings} />
|
||||
<div className="settings-section">
|
||||
<div className="settings-section__title">
|
||||
<FolderOpen size={17} />
|
||||
@@ -1327,12 +1426,16 @@ export function SettingsPanel({
|
||||
})
|
||||
: activeRuntimeModelProfile
|
||||
? t('runtime.followGoodBuddy', {
|
||||
name: activeRuntimeModelProfile.name,
|
||||
name: modelProfileDisplayName(
|
||||
activeRuntimeModelProfile
|
||||
),
|
||||
model: activeRuntimeModelProfile.modelName
|
||||
})
|
||||
: defaultTextModelProfile
|
||||
? t('runtime.followGoodBuddy', {
|
||||
name: defaultTextModelProfile.name,
|
||||
name: modelProfileDisplayName(
|
||||
defaultTextModelProfile
|
||||
),
|
||||
model: defaultTextModelProfile.modelName
|
||||
})
|
||||
: t('runtime.noCompatibleModel')}
|
||||
@@ -1415,7 +1518,7 @@ export function SettingsPanel({
|
||||
key={profile.id}
|
||||
value={profile.id}
|
||||
>
|
||||
{profile.name}
|
||||
{modelProfileDisplayName(profile)}
|
||||
{isOpenCodeCompatible(profile)
|
||||
? ''
|
||||
: t('runtime.incompatibleSuffix')}
|
||||
@@ -1440,7 +1543,12 @@ export function SettingsPanel({
|
||||
path={opencodeConfigPath}
|
||||
runtime="opencode"
|
||||
runtimeLabel="OpenCode"
|
||||
savedPath={settings?.opencodeConfigPath}
|
||||
savedPath={
|
||||
settings
|
||||
? configuredRuntimeSettings(settings)
|
||||
.opencodeConfigPath
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{opencodeModelSource.kind === 'platform' &&
|
||||
@@ -1548,12 +1656,16 @@ export function SettingsPanel({
|
||||
})
|
||||
: activeRuntimeModelProfile
|
||||
? t('runtime.followGoodBuddy', {
|
||||
name: activeRuntimeModelProfile.name,
|
||||
name: modelProfileDisplayName(
|
||||
activeRuntimeModelProfile
|
||||
),
|
||||
model: activeRuntimeModelProfile.modelName
|
||||
})
|
||||
: defaultTextModelProfile
|
||||
? t('runtime.followGoodBuddy', {
|
||||
name: defaultTextModelProfile.name,
|
||||
name: modelProfileDisplayName(
|
||||
defaultTextModelProfile
|
||||
),
|
||||
model: defaultTextModelProfile.modelName
|
||||
})
|
||||
: t('runtime.noCompatibleModel')}
|
||||
@@ -1634,7 +1746,7 @@ export function SettingsPanel({
|
||||
key={profile.id}
|
||||
value={profile.id}
|
||||
>
|
||||
{profile.name}
|
||||
{modelProfileDisplayName(profile)}
|
||||
{isContinueCompatible(profile)
|
||||
? ''
|
||||
: t('runtime.incompatibleSuffix')}
|
||||
@@ -1658,7 +1770,12 @@ export function SettingsPanel({
|
||||
path={continueConfigPath}
|
||||
runtime="continue"
|
||||
runtimeLabel="Continue"
|
||||
savedPath={settings?.continueConfigPath}
|
||||
savedPath={
|
||||
settings
|
||||
? configuredRuntimeSettings(settings)
|
||||
.continueConfigPath
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<label className="field">
|
||||
@@ -1798,7 +1915,7 @@ export function SettingsPanel({
|
||||
: undefined
|
||||
}
|
||||
aria-label={t('model.profile.editAriaLabel', {
|
||||
name: profile.name
|
||||
name: modelProfileDisplayName(profile)
|
||||
})}
|
||||
onClick={() =>
|
||||
setSelectedModelProfileId(profile.id)
|
||||
@@ -1806,7 +1923,7 @@ export function SettingsPanel({
|
||||
type="button"
|
||||
>
|
||||
<span className="model-connection-list__name">
|
||||
<strong>{profile.name}</strong>
|
||||
<strong>{modelProfileDisplayName(profile)}</strong>
|
||||
<small>{profile.modelName}</small>
|
||||
</span>
|
||||
<span className="model-connection-list__badges">
|
||||
@@ -1836,7 +1953,7 @@ export function SettingsPanel({
|
||||
<div className="settings-section__title">
|
||||
<div>
|
||||
<strong id={`model-connection-${profile.id}`}>
|
||||
{profile.name}
|
||||
{modelProfileDisplayName(profile)}
|
||||
</strong>
|
||||
<small>{t('model.profile.detail')}</small>
|
||||
</div>
|
||||
@@ -1858,7 +1975,7 @@ export function SettingsPanel({
|
||||
)}
|
||||
<button
|
||||
aria-label={t('model.profile.deleteAriaLabel', {
|
||||
name: profile.name
|
||||
name: modelProfileDisplayName(profile)
|
||||
})}
|
||||
className="danger-button danger-button--quiet"
|
||||
disabled={modelProfiles.length <= 1}
|
||||
@@ -1877,7 +1994,7 @@ export function SettingsPanel({
|
||||
name: event.target.value
|
||||
})
|
||||
}
|
||||
value={profile.name}
|
||||
value={modelProfileDisplayName(profile)}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
@@ -1912,7 +2029,7 @@ export function SettingsPanel({
|
||||
<select
|
||||
aria-label={t(
|
||||
'model.profile.protocolAriaLabel',
|
||||
{ name: profile.name }
|
||||
{ name: modelProfileDisplayName(profile) }
|
||||
)}
|
||||
onChange={(event) =>
|
||||
{
|
||||
@@ -1979,7 +2096,7 @@ export function SettingsPanel({
|
||||
<select
|
||||
aria-label={t(
|
||||
'model.profile.authenticationAriaLabel',
|
||||
{ name: profile.name }
|
||||
{ name: modelProfileDisplayName(profile) }
|
||||
)}
|
||||
onChange={(event) => {
|
||||
const authentication = event.target
|
||||
@@ -2027,7 +2144,7 @@ export function SettingsPanel({
|
||||
<select
|
||||
aria-label={t(
|
||||
'model.profile.imageQualityAriaLabel',
|
||||
{ name: profile.name }
|
||||
{ name: modelProfileDisplayName(profile) }
|
||||
)}
|
||||
onChange={(event) =>
|
||||
updateModelProfile(profile.id, {
|
||||
@@ -2530,7 +2647,11 @@ export function SettingsPanel({
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'skills' && <SkillsSettingsSection />}
|
||||
{activeTab === 'mcp' && <McpSettingsSection />}
|
||||
{activeTab === 'mcp' && (
|
||||
<McpSettingsSection
|
||||
magicNotesEnabled={magicNotesEnabled}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'about' && <UpdateSettingsSection />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
settingsWarningKey,
|
||||
type SettingsWarning
|
||||
} from '../../shared/settings-warning-contracts'
|
||||
import {
|
||||
settingsCategories,
|
||||
type SettingsCategoryId
|
||||
} from './settings-categories'
|
||||
import { translateSettingsWarning } from './settings-warnings'
|
||||
|
||||
export function SettingsWarningList({
|
||||
warnings
|
||||
}: {
|
||||
warnings?: readonly SettingsWarning[]
|
||||
}): React.JSX.Element | null {
|
||||
const { t } = useTranslation('warnings')
|
||||
if (!warnings?.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{warnings.map((warning) => (
|
||||
<p
|
||||
className="settings-warning"
|
||||
key={settingsWarningKey(warning)}
|
||||
role="alert"
|
||||
>
|
||||
{translateSettingsWarning(warning, t)}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsCategoryHeader({
|
||||
actions,
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
VersionCheckResult
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { AppInfo } from '../../shared/contracts'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
SettingsCategoryHeader,
|
||||
SettingsWarningList
|
||||
} from './SettingsPrimitives'
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
@@ -137,6 +140,7 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
error={error}
|
||||
headingId="update-settings-heading"
|
||||
/>
|
||||
<SettingsWarningList warnings={settings?.warnings} />
|
||||
<section
|
||||
aria-label={t('updates.label')}
|
||||
className="settings-section update-settings"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { magicNotes as englishMagicNotes } from './locales/en-US/magicNotes'
|
||||
import { settings as englishSettings } from './locales/en-US/settings'
|
||||
import { settingsSections as englishSettingsSections } from './locales/en-US/settingsSections'
|
||||
import { workspace as englishWorkspace } from './locales/en-US/workspace'
|
||||
import { warnings as englishWarnings } from './locales/en-US/warnings'
|
||||
import { activity as chineseActivity } from './locales/zh-CN/activity'
|
||||
import { app as chineseApp } from './locales/zh-CN/app'
|
||||
import { heartbeat as chineseHeartbeat } from './locales/zh-CN/heartbeat'
|
||||
@@ -18,6 +19,7 @@ import { magicNotes as chineseMagicNotes } from './locales/zh-CN/magicNotes'
|
||||
import { settings as chineseSettings } from './locales/zh-CN/settings'
|
||||
import { settingsSections as chineseSettingsSections } from './locales/zh-CN/settingsSections'
|
||||
import { workspace as chineseWorkspace } from './locales/zh-CN/workspace'
|
||||
import { warnings as chineseWarnings } from './locales/zh-CN/warnings'
|
||||
|
||||
export const supportedUiLocales = ['zh-CN', 'en-US'] as const
|
||||
export type UiLocale = (typeof supportedUiLocales)[number]
|
||||
@@ -32,7 +34,8 @@ export const i18nResources = {
|
||||
magicNotes: chineseMagicNotes,
|
||||
settings: chineseSettings,
|
||||
settingsSections: chineseSettingsSections,
|
||||
workspace: chineseWorkspace
|
||||
workspace: chineseWorkspace,
|
||||
warnings: chineseWarnings
|
||||
},
|
||||
'en-US': {
|
||||
activity: englishActivity,
|
||||
@@ -43,7 +46,8 @@ export const i18nResources = {
|
||||
magicNotes: englishMagicNotes,
|
||||
settings: englishSettings,
|
||||
settingsSections: englishSettingsSections,
|
||||
workspace: englishWorkspace
|
||||
workspace: englishWorkspace,
|
||||
warnings: englishWarnings
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { TranslationShape } from '../../resource-types'
|
||||
import type { app as chineseApp } from '../zh-CN/app'
|
||||
|
||||
export const app = {
|
||||
brand: {
|
||||
desktopWorkspace: 'Desktop workspace'
|
||||
},
|
||||
notifications: {
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
@@ -125,6 +128,7 @@ export const app = {
|
||||
user: 'You',
|
||||
assistantResult: 'Assistant result {{index}}',
|
||||
welcome: {
|
||||
eyebrow: 'GOODBUDDY WORKSPACE',
|
||||
title: 'What would you like to accomplish today?',
|
||||
description:
|
||||
'Ask a question, organize information, or connect OpenCode for file search and development tools.'
|
||||
|
||||
@@ -70,6 +70,7 @@ export const integrations = {
|
||||
environmentSource: 'Provided by environment variables',
|
||||
secretSaved: 'Secret saved with encryption',
|
||||
secretMissing: 'Secret not configured',
|
||||
secretUnreadable: 'Secret saved, but currently unreadable',
|
||||
readOnly:
|
||||
'This channel is managed by environment variables. Change the launch environment and restart the app.',
|
||||
enable: 'Enable the {{channel}} channel',
|
||||
|
||||
@@ -136,6 +136,7 @@ export const settings = {
|
||||
none: 'Not configured',
|
||||
encrypted: 'Encrypted in secure system storage',
|
||||
environment: 'Provided by an environment variable',
|
||||
unreadable: 'Saved, but currently unreadable',
|
||||
configuredPlaceholder: 'Configured; leave blank to keep it',
|
||||
enterApiKey: 'Enter API Key',
|
||||
noAuthentication: 'No authentication',
|
||||
@@ -224,6 +225,7 @@ export const settings = {
|
||||
}
|
||||
},
|
||||
documentParsing: {
|
||||
loading: 'Loading…',
|
||||
status: {
|
||||
title: 'Runtime status',
|
||||
description: 'Capabilities currently available on this device',
|
||||
@@ -409,6 +411,7 @@ export const settings = {
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
seededDefaultName: 'Default model',
|
||||
generatedName: 'Model connection {{count}}',
|
||||
title: 'LLM model connections',
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { TranslationShape } from '../../resource-types'
|
||||
import type { warnings as chineseWarnings } from '../zh-CN/warnings'
|
||||
|
||||
export const warnings = {
|
||||
'application-settings-recovered':
|
||||
'The application settings file was corrupt. The original file was isolated, and safe defaults are now in use.',
|
||||
'document-parsing-settings-recovered':
|
||||
'The document parsing settings file was corrupt. The original file was isolated, and safe defaults are now in use.',
|
||||
'capability-settings-recovered':
|
||||
'The capability settings file was corrupt. The original file was isolated. Web search and computer control remain off until you review and enable them.',
|
||||
'runtime-settings-recovered':
|
||||
'The Runtime settings file was corrupt. The original file was isolated, and defaults are now in use.',
|
||||
'runtime-model-credential-unreadable':
|
||||
'The API Key for model connection “{{subject}}” cannot be read. Re-enter or clear this credential.',
|
||||
'runtime-model-credential-binding-mismatch':
|
||||
'The service address for model connection “{{subject}}” does not match its saved API Key. Re-enter or clear this credential.',
|
||||
'runtime-embedding-credential-unreadable':
|
||||
'The embedding model API Key cannot be read. Re-enter or clear this credential.',
|
||||
'runtime-embedding-credential-binding-mismatch':
|
||||
'The embedding endpoint does not match its saved API Key. Re-enter or clear this credential.',
|
||||
'runtime-rerank-credential-unreadable':
|
||||
'The rerank model API Key cannot be read. Re-enter or clear this credential.',
|
||||
'runtime-rerank-credential-binding-mismatch':
|
||||
'The rerank endpoint does not match its saved API Key. Re-enter or clear this credential.',
|
||||
'channel-settings-recovered':
|
||||
'The channel settings file was corrupt. The original file was isolated, and all channels were restored as disabled.',
|
||||
'channel-weixin-credential-unreadable':
|
||||
'The WeChat connection credential cannot be read, so the channel is temporarily disabled. Connect it again with a QR code.',
|
||||
'channel-weixin-secure-storage-unavailable':
|
||||
'Secure system storage is temporarily unavailable, so the WeChat channel is disabled. Retry after secure storage recovers.',
|
||||
'channel-weixin-legacy-binding-invalid':
|
||||
'The legacy WeChat connection could not be migrated safely. Connect it again with a QR code.',
|
||||
'channel-wecom-environment-invalid':
|
||||
'The WeCom environment configuration is invalid or incomplete, so the channel remains off.',
|
||||
'channel-dingtalk-environment-invalid':
|
||||
'The DingTalk environment configuration is invalid or incomplete, so the channel remains off.',
|
||||
'channel-wecom-credential-unreadable':
|
||||
'The WeCom Secret cannot be read. Re-enter or clear this credential.',
|
||||
'channel-dingtalk-credential-unreadable':
|
||||
'The DingTalk Client Secret cannot be read. Re-enter or clear this credential.',
|
||||
'channel-runtime-selections-repaired':
|
||||
'Repaired {{count}} unavailable backend selections for unattended channels. Review each channel project setting.'
|
||||
} as const satisfies TranslationShape<typeof chineseWarnings>
|
||||
|
||||
export default warnings
|
||||
@@ -1,4 +1,7 @@
|
||||
export const app = {
|
||||
brand: {
|
||||
desktopWorkspace: '桌面工作区'
|
||||
},
|
||||
notifications: {
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
@@ -121,6 +124,7 @@ export const app = {
|
||||
user: '用户',
|
||||
assistantResult: '助手成果 {{index}}',
|
||||
welcome: {
|
||||
eyebrow: 'GOODBUDDY 工作台',
|
||||
title: '今天想一起完成什么?',
|
||||
description:
|
||||
'快速提问、梳理信息,或连接 OpenCode 使用文件搜索和开发工具。'
|
||||
|
||||
@@ -62,6 +62,7 @@ export const integrations = {
|
||||
environmentSource: '由环境变量提供',
|
||||
secretSaved: 'Secret 已加密保存',
|
||||
secretMissing: 'Secret 尚未配置',
|
||||
secretUnreadable: 'Secret 已保存,但当前无法读取',
|
||||
readOnly:
|
||||
'当前通道由环境变量管理。请在启动环境中修改配置后重启应用。',
|
||||
enable: '启用{{channel}}通道',
|
||||
|
||||
@@ -124,6 +124,7 @@ export const settings = {
|
||||
none: '尚未配置',
|
||||
encrypted: '已由系统安全存储加密',
|
||||
environment: '由环境变量提供',
|
||||
unreadable: '已保存,但当前无法读取',
|
||||
configuredPlaceholder: '已配置,留空保持不变',
|
||||
enterApiKey: '输入 API Key',
|
||||
noAuthentication: '无需认证',
|
||||
@@ -205,6 +206,7 @@ export const settings = {
|
||||
}
|
||||
},
|
||||
documentParsing: {
|
||||
loading: '正在加载…',
|
||||
status: {
|
||||
title: '运行状态',
|
||||
description: '显示当前设备实际可用的解析能力',
|
||||
@@ -372,6 +374,7 @@ export const settings = {
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
seededDefaultName: '默认模型',
|
||||
generatedName: '模型连接 {{count}}',
|
||||
title: 'LLM 模型连接',
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export const warnings = {
|
||||
'application-settings-recovered':
|
||||
'应用设置文件已损坏。原文件已隔离,当前使用安全默认设置。',
|
||||
'document-parsing-settings-recovered':
|
||||
'文档解析设置文件已损坏。原文件已隔离,当前使用安全默认设置。',
|
||||
'capability-settings-recovered':
|
||||
'能力设置文件已损坏。原文件已隔离,网页搜索和电脑控制已保持关闭,请检查后手动启用。',
|
||||
'runtime-settings-recovered':
|
||||
'Runtime 设置文件已损坏。原文件已隔离,当前使用默认设置。',
|
||||
'runtime-model-credential-unreadable':
|
||||
'模型连接“{{subject}}”的 API Key 无法读取。请重新输入或清除该凭据。',
|
||||
'runtime-model-credential-binding-mismatch':
|
||||
'模型连接“{{subject}}”的服务地址与已保存 API Key 不匹配。请重新输入或清除该凭据。',
|
||||
'runtime-embedding-credential-unreadable':
|
||||
'向量模型 API Key 无法读取。请重新输入或清除该凭据。',
|
||||
'runtime-embedding-credential-binding-mismatch':
|
||||
'向量接口地址与已保存 API Key 不匹配。请重新输入或清除该凭据。',
|
||||
'runtime-rerank-credential-unreadable':
|
||||
'重排模型 API Key 无法读取。请重新输入或清除该凭据。',
|
||||
'runtime-rerank-credential-binding-mismatch':
|
||||
'重排接口地址与已保存 API Key 不匹配。请重新输入或清除该凭据。',
|
||||
'channel-settings-recovered':
|
||||
'通道设置文件已损坏。原文件已隔离,所有通道已恢复为关闭状态。',
|
||||
'channel-weixin-credential-unreadable':
|
||||
'微信绑定凭据无法读取,通道已临时停用。请重新扫码绑定。',
|
||||
'channel-weixin-secure-storage-unavailable':
|
||||
'系统安全存储暂不可用,微信绑定已临时停用。恢复安全存储后可重试。',
|
||||
'channel-weixin-legacy-binding-invalid':
|
||||
'旧版微信绑定无法安全迁移,请重新扫码绑定。',
|
||||
'channel-wecom-environment-invalid':
|
||||
'企业微信环境变量配置无效或不完整,通道保持关闭。',
|
||||
'channel-dingtalk-environment-invalid':
|
||||
'钉钉环境变量配置无效或不完整,通道保持关闭。',
|
||||
'channel-wecom-credential-unreadable':
|
||||
'企业微信 Secret 无法读取。请重新输入或清除该凭据。',
|
||||
'channel-dingtalk-credential-unreadable':
|
||||
'钉钉 Client Secret 无法读取。请重新输入或清除该凭据。',
|
||||
'channel-runtime-selections-repaired':
|
||||
'已修复 {{count}} 个无人值守通道的不可用后端选择。请检查各通道项目设置。'
|
||||
} as const
|
||||
|
||||
export default warnings
|
||||
Vendored
+2
@@ -8,6 +8,7 @@ import activity from './locales/zh-CN/activity'
|
||||
import magicNotes from './locales/zh-CN/magicNotes'
|
||||
import integrations from './locales/zh-CN/integrations'
|
||||
import workspace from './locales/zh-CN/workspace'
|
||||
import warnings from './locales/zh-CN/warnings'
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
@@ -22,6 +23,7 @@ declare module 'i18next' {
|
||||
magicNotes: typeof magicNotes
|
||||
integrations: typeof integrations
|
||||
workspace: typeof workspace
|
||||
warnings: typeof warnings
|
||||
}
|
||||
returnNull: false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
|
||||
|
||||
export function getRuntimeSelectionForProvider(
|
||||
provider: 'model' | 'opencode' | 'continue',
|
||||
settings: RuntimeSettings
|
||||
): AgentRuntimeSelection {
|
||||
if (provider === 'model') {
|
||||
return {
|
||||
provider,
|
||||
profileId: settings.defaultModelProfileId
|
||||
}
|
||||
}
|
||||
const source =
|
||||
provider === 'opencode'
|
||||
? settings.opencodeModelSource
|
||||
: settings.continueModelSource
|
||||
return {
|
||||
provider,
|
||||
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultRuntimeSelection(
|
||||
settings: RuntimeSettings
|
||||
): AgentRuntimeSelection {
|
||||
if (
|
||||
settings.provider === 'model' ||
|
||||
settings.provider === 'opencode' ||
|
||||
settings.provider === 'continue'
|
||||
) {
|
||||
return getRuntimeSelectionForProvider(settings.provider, settings)
|
||||
}
|
||||
return settings.opencodeBaseUrl || settings.opencodeEmbedded
|
||||
? getRuntimeSelectionForProvider('opencode', settings)
|
||||
: getRuntimeSelectionForProvider('model', settings)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { SettingsWarning } from '../../shared/settings-warning-contracts'
|
||||
|
||||
export function translateSettingsWarning(
|
||||
warning: SettingsWarning,
|
||||
t: TFunction<'warnings'>
|
||||
): string {
|
||||
switch (warning.code) {
|
||||
case 'runtime-model-credential-unreadable':
|
||||
case 'runtime-model-credential-binding-mismatch':
|
||||
return t(warning.code, {
|
||||
subject: warning.subject ?? ''
|
||||
})
|
||||
case 'channel-runtime-selections-repaired':
|
||||
return t(warning.code, {
|
||||
count: warning.count ?? 0
|
||||
})
|
||||
default:
|
||||
return t(warning.code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user