fix: allow editing context compression limits
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user