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 () => {
render(
<SettingsPanel
+140 -43
View File
@@ -73,6 +73,86 @@ type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
}
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 = {
open: boolean
@@ -534,6 +614,14 @@ export function SettingsPanel({
useState<ContextCompressionSettings>(
defaultContextCompressionSettings
)
const [
contextCompressionTokenInput,
setContextCompressionTokenInput
] = useState<ContextCompressionTokenDrafts>(() =>
contextCompressionTokenDrafts(
defaultContextCompressionSettings
)
)
const modelProfileDisplayName = (
profile: Pick<ModelProfileDraft, 'id' | 'name'>
): string =>
@@ -608,6 +696,12 @@ export function SettingsPanel({
},
preserveSelectedProfile
)
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
value.contextCompression ??
defaultContextCompressionSettings
)
)
},
[]
)
@@ -735,6 +829,22 @@ export function SettingsPanel({
return null
}
const normalizedContextCompression =
normalizeContextCompressionTokenDrafts(
contextCompression,
contextCompressionTokenInput
)
const commitContextCompressionTokenInput =
(): ContextCompressionSettings => {
setContextCompression(normalizedContextCompression)
setContextCompressionTokenInput(
contextCompressionTokenDrafts(
normalizedContextCompression
)
)
return normalizedContextCompression
}
const close = (): void => {
setModelProfiles((profiles) =>
profiles.map((profile) => ({
@@ -760,6 +870,8 @@ export function SettingsPanel({
setSaving(true)
setError(undefined)
try {
const contextCompressionInput =
commitContextCompressionTokenInput()
const defaultProfile =
modelProfiles.find(
(profile) => profile.id === defaultModelProfileId
@@ -857,7 +969,7 @@ export function SettingsPanel({
continueModelSource,
deepseekHarnessModelSource:
normalizedDeepseekHarnessModelSource,
contextCompression,
contextCompression: contextCompressionInput,
toolApproval,
subagentSmartRoutingEnabled
})
@@ -2824,34 +2936,25 @@ export function SettingsPanel({
aria-label={t('contextControl.triggerTokens')}
disabled={!contextCompression.enabled}
inputMode="numeric"
max={1_000}
min={8}
onChange={(event) => {
const value = event.target.valueAsNumber
if (Number.isFinite(value)) {
setContextCompression((current) => ({
...current,
triggerTokens: Math.round(value * 1_000),
recentRawTokens: Math.min(
current.recentRawTokens,
Math.max(
4_000,
Math.round(value * 1_000) - 1_000
)
)
}))
}
}}
max={maximumContextCompressionTriggerThousands}
min={minimumContextCompressionTriggerThousands}
onBlur={commitContextCompressionTokenInput}
onChange={(event) =>
setContextCompressionTokenInput((current) => ({
...current,
trigger: event.target.value
}))
}
required
step={1}
type="number"
value={contextCompression.triggerTokens / 1_000}
value={contextCompressionTokenInput.trigger}
/>
<small>
{t('contextControl.triggerTokensDescription', {
tokens:
contextCompression.triggerTokens.toLocaleString(
i18n.language
)
normalizedContextCompression.triggerTokens
.toLocaleString(i18n.language)
})}
</small>
</label>
@@ -2861,33 +2964,27 @@ export function SettingsPanel({
aria-label={t('contextControl.recentRawTokens')}
disabled={!contextCompression.enabled}
inputMode="numeric"
max={Math.min(
256,
contextCompression.triggerTokens / 1_000 - 1
max={maximumContextCompressionRecentRawThousandsFor(
normalizedContextCompression.triggerTokens
)}
min={4}
onChange={(event) => {
const value = event.target.valueAsNumber
if (Number.isFinite(value)) {
setContextCompression((current) => ({
...current,
recentRawTokens: Math.min(
Math.round(value * 1_000),
current.triggerTokens - 1_000
)
}))
}
}}
min={minimumContextCompressionRecentRawThousands}
onBlur={commitContextCompressionTokenInput}
onChange={(event) =>
setContextCompressionTokenInput((current) => ({
...current,
recentRaw: event.target.value
}))
}
required
step={1}
type="number"
value={contextCompression.recentRawTokens / 1_000}
value={contextCompressionTokenInput.recentRaw}
/>
<small>
{t('contextControl.recentRawTokensDescription', {
tokens:
contextCompression.recentRawTokens.toLocaleString(
i18n.language
)
normalizedContextCompression.recentRawTokens
.toLocaleString(i18n.language)
})}
</small>
</label>