fix: allow editing context compression limits

This commit is contained in:
mesalogo
2026-08-15 21:11:02 +08:00
parent 1cde0f3385
commit 939c3ab1b1
2 changed files with 221 additions and 43 deletions
+81
View File
@@ -588,6 +588,87 @@ describe('SettingsPanel runtime files', () => {
) )
}) })
it('allows editing context compression budgets through empty and single-digit states', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
initialCategory="context-control"
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('switch', {
name: '自动压缩较早的对话'
})
)
const trigger = screen.getByLabelText('压缩触发阈值')
const recent = screen.getByLabelText('最近原文预算')
fireEvent.change(trigger, { target: { value: '' } })
expect(trigger).toHaveValue(null)
fireEvent.blur(trigger)
expect(trigger).toHaveValue(200)
fireEvent.change(trigger, { target: { value: '8' } })
expect(trigger).toHaveValue(8)
expect(recent).toHaveValue(32)
expect(recent).toHaveAttribute('max', '7')
fireEvent.change(recent, { target: { value: '4' } })
expect(recent).toHaveValue(4)
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenLastCalledWith(
expect.objectContaining({
contextCompression: expect.objectContaining({
triggerTokens: 8_000,
recentRawTokens: 4_000
})
})
)
)
})
it('normalizes context compression limits only after editing finishes', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
initialCategory="context-control"
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('switch', {
name: '自动压缩较早的对话'
})
)
const trigger = screen.getByLabelText('压缩触发阈值')
const recent = screen.getByLabelText('最近原文预算')
fireEvent.change(trigger, { target: { value: '2' } })
expect(trigger).toHaveValue(2)
expect(recent).toHaveAttribute('max', '7')
fireEvent.blur(trigger)
expect(trigger).toHaveValue(8)
expect(recent).toHaveValue(7)
fireEvent.change(trigger, { target: { value: '1200' } })
fireEvent.blur(trigger)
expect(trigger).toHaveValue(1000)
fireEvent.change(recent, { target: { value: '999' } })
fireEvent.blur(recent)
expect(recent).toHaveValue(256)
})
it('stores an optional context window on direct text models', async () => { it('stores an optional context window on direct text models', async () => {
render( render(
<SettingsPanel <SettingsPanel
+134 -37
View File
@@ -73,6 +73,86 @@ type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
} }
const settingsTabs = settingsCategoryList.map(({ id }) => id) const settingsTabs = settingsCategoryList.map(({ id }) => id)
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
}
}
type SettingsPanelProps = { type SettingsPanelProps = {
open: boolean open: boolean
@@ -534,6 +614,14 @@ export function SettingsPanel({
useState<ContextCompressionSettings>( useState<ContextCompressionSettings>(
defaultContextCompressionSettings defaultContextCompressionSettings
) )
const [
contextCompressionTokenInput,
setContextCompressionTokenInput
] = useState<ContextCompressionTokenDrafts>(() =>
contextCompressionTokenDrafts(
defaultContextCompressionSettings
)
)
const modelProfileDisplayName = ( const modelProfileDisplayName = (
profile: Pick<ModelProfileDraft, 'id' | 'name'> profile: Pick<ModelProfileDraft, 'id' | 'name'>
): string => ): string =>
@@ -608,6 +696,12 @@ export function SettingsPanel({
}, },
preserveSelectedProfile preserveSelectedProfile
) )
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
value.contextCompression ??
defaultContextCompressionSettings
)
)
}, },
[] []
) )
@@ -735,6 +829,22 @@ export function SettingsPanel({
return null return null
} }
const normalizedContextCompression =
normalizeContextCompressionTokenDrafts(
contextCompression,
contextCompressionTokenInput
)
const commitContextCompressionTokenInput =
(): ContextCompressionSettings => {
setContextCompression(normalizedContextCompression)
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
normalizedContextCompression
)
)
return normalizedContextCompression
}
const close = (): void => { const close = (): void => {
setModelProfiles((profiles) => setModelProfiles((profiles) =>
profiles.map((profile) => ({ profiles.map((profile) => ({
@@ -760,6 +870,8 @@ export function SettingsPanel({
setSaving(true) setSaving(true)
setError(undefined) setError(undefined)
try { try {
const contextCompressionInput =
commitContextCompressionTokenInput()
const defaultProfile = const defaultProfile =
modelProfiles.find( modelProfiles.find(
(profile) => profile.id === defaultModelProfileId (profile) => profile.id === defaultModelProfileId
@@ -857,7 +969,7 @@ export function SettingsPanel({
continueModelSource, continueModelSource,
deepseekHarnessModelSource: deepseekHarnessModelSource:
normalizedDeepseekHarnessModelSource, normalizedDeepseekHarnessModelSource,
contextCompression, contextCompression: contextCompressionInput,
toolApproval, toolApproval,
subagentSmartRoutingEnabled subagentSmartRoutingEnabled
}) })
@@ -2824,34 +2936,25 @@ export function SettingsPanel({
aria-label={t('contextControl.triggerTokens')} aria-label={t('contextControl.triggerTokens')}
disabled={!contextCompression.enabled} disabled={!contextCompression.enabled}
inputMode="numeric" inputMode="numeric"
max={1_000} max={maximumContextCompressionTriggerThousands}
min={8} min={minimumContextCompressionTriggerThousands}
onChange={(event) => { onBlur={commitContextCompressionTokenInput}
const value = event.target.valueAsNumber onChange={(event) =>
if (Number.isFinite(value)) { setContextCompressionTokenInput((current) => ({
setContextCompression((current) => ({
...current, ...current,
triggerTokens: Math.round(value * 1_000), trigger: event.target.value
recentRawTokens: Math.min(
current.recentRawTokens,
Math.max(
4_000,
Math.round(value * 1_000) - 1_000
)
)
})) }))
} }
}}
required required
step={1}
type="number" type="number"
value={contextCompression.triggerTokens / 1_000} value={contextCompressionTokenInput.trigger}
/> />
<small> <small>
{t('contextControl.triggerTokensDescription', { {t('contextControl.triggerTokensDescription', {
tokens: tokens:
contextCompression.triggerTokens.toLocaleString( normalizedContextCompression.triggerTokens
i18n.language .toLocaleString(i18n.language)
)
})} })}
</small> </small>
</label> </label>
@@ -2861,33 +2964,27 @@ export function SettingsPanel({
aria-label={t('contextControl.recentRawTokens')} aria-label={t('contextControl.recentRawTokens')}
disabled={!contextCompression.enabled} disabled={!contextCompression.enabled}
inputMode="numeric" inputMode="numeric"
max={Math.min( max={maximumContextCompressionRecentRawThousandsFor(
256, normalizedContextCompression.triggerTokens
contextCompression.triggerTokens / 1_000 - 1
)} )}
min={4} min={minimumContextCompressionRecentRawThousands}
onChange={(event) => { onBlur={commitContextCompressionTokenInput}
const value = event.target.valueAsNumber onChange={(event) =>
if (Number.isFinite(value)) { setContextCompressionTokenInput((current) => ({
setContextCompression((current) => ({
...current, ...current,
recentRawTokens: Math.min( recentRaw: event.target.value
Math.round(value * 1_000),
current.triggerTokens - 1_000
)
})) }))
} }
}}
required required
step={1}
type="number" type="number"
value={contextCompression.recentRawTokens / 1_000} value={contextCompressionTokenInput.recentRaw}
/> />
<small> <small>
{t('contextControl.recentRawTokensDescription', { {t('contextControl.recentRawTokensDescription', {
tokens: tokens:
contextCompression.recentRawTokens.toLocaleString( normalizedContextCompression.recentRawTokens
i18n.language .toLocaleString(i18n.language)
)
})} })}
</small> </small>
</label> </label>