From 939c3ab1b11f930ea6ba58b31388e17a56603a02 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sat, 15 Aug 2026 21:11:02 +0800 Subject: [PATCH] fix: allow editing context compression limits --- src/renderer/src/SettingsPanel.test.tsx | 81 +++++++++++ src/renderer/src/SettingsPanel.tsx | 183 ++++++++++++++++++------ 2 files changed, 221 insertions(+), 43 deletions(-) diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index 43c8bb6..684ccb8 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -588,6 +588,87 @@ describe('SettingsPanel runtime files', () => { ) }) + it('allows editing context compression budgets through empty and single-digit states', async () => { + render( + {})} + 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( + {})} + 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( 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( defaultContextCompressionSettings ) + const [ + contextCompressionTokenInput, + setContextCompressionTokenInput + ] = useState(() => + contextCompressionTokenDrafts( + defaultContextCompressionSettings + ) + ) const modelProfileDisplayName = ( profile: Pick ): 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} /> {t('contextControl.triggerTokensDescription', { tokens: - contextCompression.triggerTokens.toLocaleString( - i18n.language - ) + normalizedContextCompression.triggerTokens + .toLocaleString(i18n.language) })} @@ -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} /> {t('contextControl.recentRawTokensDescription', { tokens: - contextCompression.recentRawTokens.toLocaleString( - i18n.language - ) + normalizedContextCompression.recentRawTokens + .toLocaleString(i18n.language) })}