dsh-context-compression-improved 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +84 -81
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,170 @@
1
+ .settingsSection {
2
+ display: flex;
3
+ max-width: 720px;
4
+ flex-direction: column;
5
+ gap: 12px;
6
+ color: var(--dsw-alias-label-primary);
7
+ }
8
+
9
+ .settingsTitle {
10
+ margin: 0;
11
+ color: var(--dsw-alias-label-primary);
12
+ font-size: 16px;
13
+ font-weight: 500;
14
+ line-height: 24px;
15
+ }
16
+
17
+ .settingsDescription {
18
+ margin: 0;
19
+ color: var(--dsw-alias-label-tertiary);
20
+ font-size: 14px;
21
+ line-height: 22px;
22
+ }
23
+
24
+ .root {
25
+ width: 100%;
26
+ }
27
+
28
+ .profileGrid {
29
+ display: grid;
30
+ grid-template-columns: repeat(2, minmax(0, 1fr));
31
+ gap: 10px;
32
+ }
33
+
34
+ .profileCard {
35
+ min-height: 112px;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 8px;
39
+ padding: 14px;
40
+ border: 1px solid var(--dsw-alias-border-l2);
41
+ border-radius: 10px;
42
+ background: transparent;
43
+ color: var(--dsw-alias-label-primary);
44
+ cursor: pointer;
45
+ text-align: left;
46
+ }
47
+
48
+ .profileCard:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
49
+ .profileCard[aria-pressed="true"] { border-color: var(--dsw-alias-label-primary); }
50
+ .profileCard:active:not(:disabled) { transform: scale(.99); }
51
+ .profileCard:disabled { cursor: default; opacity: .6; }
52
+ .profileCard:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
53
+ .profileCardTop { display: flex; align-items: center; gap: 8px; }
54
+ .profileCardTitle { font-size: 14px; font-weight: 500; line-height: 20px; }
55
+ .profileCurrent { padding: 1px 6px; border-radius: 999px; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); font-size: 10px; line-height: 14px; }
56
+ .profileCardDetail { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
57
+
58
+ .button {
59
+ width: 100%;
60
+ min-height: 34px;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ padding: 6px 8px;
65
+ border: 1px solid var(--dsw-alias-border-l2);
66
+ border-radius: 10px;
67
+ background: transparent;
68
+ color: var(--dsw-alias-label-primary);
69
+ cursor: pointer;
70
+ text-align: left;
71
+ }
72
+
73
+ .button:hover { background: var(--dsw-alias-interactive-bg-hover); }
74
+ .button:disabled { cursor: default; opacity: .6; }
75
+
76
+ .copy { flex: 1; min-width: 0; }
77
+ .label { font-size: 11px; line-height: 14px; color: var(--dsw-alias-label-tertiary); }
78
+ .value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 17px; }
79
+ .chevron { flex: none; color: var(--dsw-alias-label-secondary); }
80
+
81
+ .menuCopy { display: flex; min-width: 220px; flex-direction: column; gap: 2px; }
82
+ .menuTitle { font-size: 13px; line-height: 17px; }
83
+ .menuDetail { max-width: 290px; white-space: normal; font-size: 11px; line-height: 15px; color: var(--dsw-alias-label-tertiary); }
84
+
85
+ .error {
86
+ padding: 4px 8px 0;
87
+ color: var(--dsw-alias-state-error-primary);
88
+ font-size: 11px;
89
+ line-height: 15px;
90
+ }
91
+
92
+ .unavailable {
93
+ padding: 4px 8px 0;
94
+ color: var(--dsw-alias-label-tertiary);
95
+ font-size: 11px;
96
+ line-height: 15px;
97
+ }
98
+
99
+ .settingsHint {
100
+ padding: 4px 8px 0;
101
+ color: var(--dsw-alias-label-secondary);
102
+ font-size: 11px;
103
+ line-height: 15px;
104
+ }
105
+
106
+ .pricing {
107
+ padding: 4px 8px 0;
108
+ color: var(--dsw-alias-label-tertiary);
109
+ font-size: 11px;
110
+ line-height: 15px;
111
+ }
112
+
113
+ .custom {
114
+ margin-top: 16px;
115
+ padding: 16px 0 0;
116
+ border-top: 1px solid var(--dsw-alias-border-l2);
117
+ }
118
+
119
+ .customTitle { margin: 0 0 6px; font-size: 13px; line-height: 17px; }
120
+ .customNote { margin: 4px 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 15px; }
121
+
122
+ .stage {
123
+ margin: 12px 0 0;
124
+ padding: 12px 0 0;
125
+ border: 0;
126
+ border-top: 1px solid var(--dsw-alias-border-l2);
127
+ }
128
+
129
+ .stageToggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; line-height: 16px; }
130
+ .fieldGrid { display: grid; grid-template-columns: 1fr; gap: 8px; }
131
+ .field { display: flex; min-width: 0; flex-direction: column; gap: 4px; margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 15px; }
132
+ .field input, .field select {
133
+ width: 100%;
134
+ min-width: 0;
135
+ min-height: 30px;
136
+ box-sizing: border-box;
137
+ padding: 4px 6px;
138
+ border: 1px solid var(--dsw-alias-border-l2);
139
+ border-radius: 7px;
140
+ background: var(--dsw-alias-bg-base);
141
+ color: var(--dsw-alias-label-primary);
142
+ }
143
+ .field input:focus-visible, .field select:focus-visible, .actions button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
144
+ .actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
145
+ .actions button { min-height: 30px; padding: 4px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: transparent; color: var(--dsw-alias-label-primary); }
146
+ .actions button:active:not(:disabled) { transform: scale(.98); }
147
+ .actions button:disabled { opacity: .6; }
148
+
149
+ @media (max-width: 560px) {
150
+ .profileGrid { grid-template-columns: 1fr; }
151
+ }
152
+
153
+ .autoCompact {
154
+ margin-top: 24px;
155
+ padding: 16px;
156
+ border: 1px solid rgba(128, 128, 128, 0.35);
157
+ border-radius: 8px;
158
+ }
159
+
160
+ .autoCompactTitle {
161
+ margin: 0 0 8px;
162
+ font-size: 15px;
163
+ font-weight: 600;
164
+ }
165
+
166
+ .autoCompactRisk {
167
+ margin: 8px 0 0;
168
+ font-size: 12px;
169
+ opacity: 0.9;
170
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * CompressionProfileSelector — thin re-export hub.
3
+ *
4
+ * Component implementations now live in dedicated modules to keep the
5
+ * god-module under control. This file preserves the public API surface so
6
+ * existing consumers see no change.
7
+ *
8
+ * @module dsh-context-compression-improved/client/CompressionProfileSelector
9
+ */
10
+
11
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client'
12
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
13
+ import {
14
+ COMPRESSION_PROFILES,
15
+ isCustomCompressionPolicy,
16
+ type CompressionProfile,
17
+ type CustomCompressionPolicy,
18
+ type ContextCompressionSettings,
19
+ type PresetOptionsSettings,
20
+ } from '../profiles.ts'
21
+
22
+ export { COMPRESSION_PROFILES, isCustomCompressionPolicy }
23
+ export type { CompressionProfile, CustomCompressionPolicy, ContextCompressionSettings, PresetOptionsSettings }
24
+
25
+ export interface CompressionSelectorInjected {
26
+ hooks: { compression: SettingsScope<ContextCompressionSettings> }
27
+ select: (profile: CompressionProfile) => Promise<void>
28
+ saveCustom: (custom: CustomCompressionPolicy) => Promise<void>
29
+ resetCustom: () => Promise<void>
30
+ saveAutoCompact: (thresholdPercent: number) => Promise<void>
31
+ saveCodeSkeleton: (enabled: boolean) => Promise<void>
32
+ /** Patch of `presetOptions` members; an explicit `undefined` clears that field. */
33
+ savePresetOptions: (options: import('./preset-options.ts').PresetOptionsPatch) => Promise<void>
34
+ }
35
+
36
+ export type CompressionProfileSelectorProps =
37
+ PropsRuntime<'settings.section'>
38
+ & PropsLocale<'context-compression'>
39
+ & InjectFace<CompressionSelectorInjected>
40
+
41
+ // Re-export sub-components from dedicated modules.
42
+ export {
43
+ ContextCompressionSettingsSection,
44
+ SettingsCompressionProfileControls,
45
+ } from './settings-section.tsx'
46
+
47
+ export {
48
+ CompressionProfileControls,
49
+ AutoCompactThresholdControls,
50
+ CodeSkeletonControls,
51
+ } from './CompressionProfileControls.tsx'
52
+
53
+ export {
54
+ EstimatorControls,
55
+ EstimatorInactiveNotice,
56
+ ESTIMATOR_CATALOG_ROUTES,
57
+ } from './EstimatorControls.tsx'
58
+
59
+ export {
60
+ CustomPolicyEditor,
61
+ StageFields,
62
+ editableCustom,
63
+ } from './CustomPolicyEditor.tsx'
64
+
65
+ // Imported for the wrapper component below; re-exported above.
66
+ import { CompressionProfileControls } from './CompressionProfileControls.tsx'
67
+
68
+ /** Full-page Settings surface backed by the same durable selector state. */
69
+ export function CompressionProfileSelector({
70
+ ...props
71
+ }: CompressionProfileSelectorProps) {
72
+ return <CompressionProfileControls {...props} showCustomEditor={false} />
73
+ }
74
+
75
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
76
+ interface LocaleNamespaceMap {
77
+ 'context-compression': import('./locales.ts').ContextCompressionLocaleKey
78
+ }
79
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * CustomPolicyEditor: full custom compression policy editor with StageFields.
3
+ *
4
+ * Extracted from CompressionProfileSelector.tsx to reduce god-module size.
5
+ *
6
+ * @module dsh-context-compression-improved/client/CustomPolicyEditor
7
+ */
8
+
9
+ import type { ContextCompressionLocaleKey } from './locales.ts'
10
+ import css from './CompressionProfileSelector.module.css'
11
+ import {
12
+ isCustomCompressionPolicy,
13
+ type CustomCompressionBudget,
14
+ type CustomCompressionPolicy,
15
+ type CustomCompressionPolicyV3,
16
+ type CustomHistoryPolicy,
17
+ } from '../profiles.ts'
18
+
19
+ interface CustomPolicyEditorProps {
20
+ value: CustomCompressionPolicyV3
21
+ disabled: boolean
22
+ setValue: (value: CustomCompressionPolicyV3) => void
23
+ save: () => Promise<void>
24
+ reset: () => Promise<void>
25
+ settle: (operation: () => Promise<void>) => void
26
+ t: (key: ContextCompressionLocaleKey) => string
27
+ }
28
+
29
+ export function CustomPolicyEditor({ value, disabled, setValue, save, reset, settle, t }: CustomPolicyEditorProps) {
30
+ const valid = isCustomCompressionPolicy(value)
31
+ const unitStep = value.unit === 'tokens' ? 1 : 0.01
32
+ const unitMax = value.unit === 'tokens' ? undefined : 100
33
+ const unitBounds = unitMax === undefined ? {} : { max: unitMax }
34
+ const setBudget = (
35
+ stage: 'fresh' | 'aggregate',
36
+ patch: Partial<CustomCompressionBudget>,
37
+ ): void => {
38
+ setValue({ ...value, [stage]: { ...value[stage], ...patch } })
39
+ }
40
+ const setHistory = (patch: Partial<CustomHistoryPolicy>): void => {
41
+ setValue({ ...value, history: { ...value.history, ...patch } })
42
+ }
43
+ return (
44
+ <section className={css.custom} aria-labelledby="context-compression-custom-title">
45
+ <h3 id="context-compression-custom-title" className={css.customTitle}>{t('custom.title')}</h3>
46
+ <p className={css.customNote}>{t('custom.sessionScope')}</p>
47
+ <p className={css.customNote}>{t('custom.measurement')}</p>
48
+ <label className={css.field}>
49
+ <span>{t('custom.unit')}</span>
50
+ <select
51
+ value={value.unit}
52
+ disabled={disabled}
53
+ onChange={(event) => {
54
+ setValue({
55
+ ...value,
56
+ unit: event.currentTarget.value as CustomCompressionPolicy['unit'],
57
+ })
58
+ }}
59
+ >
60
+ <option value="tokens">{t('custom.unit.tokens')}</option>
61
+ <option value="context-percent">{t('custom.unit.contextPercent')}</option>
62
+ </select>
63
+ </label>
64
+ <StageFields
65
+ t={t}
66
+ title={t('custom.fresh.enabled')}
67
+ enabled={value.fresh.enabled}
68
+ disabled={disabled}
69
+ onEnabled={(enabled) => { setBudget('fresh', { enabled }) }}
70
+ fields={[
71
+ { label: t('custom.fresh.trigger'), value: value.fresh.trigger, set: (trigger) => { setBudget('fresh', { trigger }) }, ...unitBounds },
72
+ { label: t('custom.fresh.target'), value: value.fresh.target, set: (target) => { setBudget('fresh', { target }) }, ...unitBounds },
73
+ ]}
74
+ step={unitStep}
75
+ />
76
+ <StageFields
77
+ t={t}
78
+ title={t('custom.aggregate.enabled')}
79
+ enabled={value.aggregate.enabled}
80
+ disabled={disabled}
81
+ onEnabled={(enabled) => { setBudget('aggregate', { enabled }) }}
82
+ fields={[
83
+ { label: t('custom.aggregate.trigger'), value: value.aggregate.trigger, set: (trigger) => { setBudget('aggregate', { trigger }) }, ...unitBounds },
84
+ { label: t('custom.aggregate.target'), value: value.aggregate.target, set: (target) => { setBudget('aggregate', { target }) }, ...unitBounds },
85
+ ]}
86
+ step={unitStep}
87
+ />
88
+ <StageFields
89
+ t={t}
90
+ title={t('custom.history.enabled')}
91
+ enabled={value.history.enabled}
92
+ disabled={disabled}
93
+ onEnabled={(enabled) => { setHistory({ enabled }) }}
94
+ fields={[
95
+ { label: t('custom.history.trigger'), value: value.history.trigger, set: (trigger) => { setHistory({ trigger }) }, ...unitBounds },
96
+ { label: t('custom.history.keepRecentToolCalls'), value: value.history.keepRecentToolCalls, set: (keepRecentToolCalls) => { setHistory({ keepRecentToolCalls }) }, integer: true, allowZero: true },
97
+ { label: t('custom.history.keepRecentTokens'), value: value.history.keepRecentTokens, set: (keepRecentTokens) => { setHistory({ keepRecentTokens }) }, allowZero: true, ...unitBounds },
98
+ { label: t('custom.history.minReclaim'), value: value.history.minReclaim, set: (minReclaim) => { setHistory({ minReclaim }) }, ...unitBounds },
99
+ ]}
100
+ step={unitStep}
101
+ fieldsEnabled={value.history.enabled || value.tailTrim.enabled}
102
+ />
103
+ <label className={css.field}>
104
+ <span>{t('custom.prefixPolicy')}</span>
105
+ <select
106
+ value={value.prefixPolicy}
107
+ disabled={disabled || !value.history.enabled}
108
+ onChange={(event) => {
109
+ setValue({
110
+ ...value,
111
+ prefixPolicy: event.currentTarget.value as CustomCompressionPolicy['prefixPolicy'],
112
+ })
113
+ }}
114
+ >
115
+ <option value="preserve">{t('custom.prefixPolicy.preserve')}</option>
116
+ <option value="pressure-break">{t('custom.prefixPolicy.pressureBreak')}</option>
117
+ </select>
118
+ </label>
119
+ <div className={css.customNote}>{t('custom.experimental')}</div>
120
+ <StageFields
121
+ t={t}
122
+ title={t('custom.tailTrim.enabled')}
123
+ enabled={value.tailTrim.enabled}
124
+ disabled={disabled}
125
+ onEnabled={(enabled) => {
126
+ setValue({ ...value, tailTrim: { ...value.tailTrim, enabled } })
127
+ }}
128
+ fields={[{
129
+ label: t('custom.tailTrim.trigger'),
130
+ value: value.tailTrim.trigger,
131
+ set: (trigger) => {
132
+ setValue({ ...value, tailTrim: { ...value.tailTrim, trigger } })
133
+ },
134
+ ...unitBounds,
135
+ }]}
136
+ step={unitStep}
137
+ />
138
+ <p className={css.customNote}>{t('custom.tailTrim.warning')}</p>
139
+ {valid ? null : <div className={css.error} role="alert">{t('custom.invalid')}</div>}
140
+ <div className={css.actions}>
141
+ <button type="button" disabled={disabled || !valid} onClick={() => { settle(save) }}>{t('custom.save')}</button>
142
+ <button type="button" disabled={disabled} onClick={() => { settle(reset) }}>{t('custom.reset')}</button>
143
+ </div>
144
+ </section>
145
+ )
146
+ }
147
+
148
+ interface StageNumberField {
149
+ label: string
150
+ value: number
151
+ set: (value: number) => void
152
+ integer?: boolean
153
+ allowZero?: boolean
154
+ max?: number
155
+ }
156
+
157
+ export function StageFields({ title, enabled, disabled, onEnabled, fields, step, fieldsEnabled = enabled, t }: {
158
+ title: string
159
+ enabled: boolean
160
+ disabled: boolean
161
+ onEnabled: (enabled: boolean) => void
162
+ fields: readonly StageNumberField[]
163
+ step: number
164
+ fieldsEnabled?: boolean
165
+ t: (key: ContextCompressionLocaleKey) => string
166
+ }) {
167
+ return (
168
+ <fieldset className={css.stage} disabled={disabled}>
169
+ <legend>{title}</legend>
170
+ <label className={css.field}>
171
+ <span>{t('custom.enabled')}</span>
172
+ <select aria-label={title} value={enabled ? 'on' : 'off'}
173
+ onChange={(event) => { onEnabled(event.currentTarget.value === 'on') }}>
174
+ <option value="on">{t('custom.enabled.on')}</option>
175
+ <option value="off">{t('custom.enabled.off')}</option>
176
+ </select>
177
+ </label>
178
+ <div className={css.fieldGrid}>
179
+ {fields.map(field => (
180
+ <label className={css.field} key={field.label}>
181
+ <span>{field.label}</span>
182
+ <input
183
+ type="number"
184
+ value={field.value}
185
+ min={field.allowZero === true ? 0 : field.integer === true ? 1 : step}
186
+ max={field.max}
187
+ step={field.integer === true ? 1 : step}
188
+ disabled={!fieldsEnabled || disabled}
189
+ onChange={(event) => { field.set(Number(event.currentTarget.value)) }}
190
+ />
191
+ </label>
192
+ ))}
193
+ </div>
194
+ </fieldset>
195
+ )
196
+ }
197
+
198
+ /** Convert a legacy custom policy to V3 format. */
199
+ export function editableCustom(value: CustomCompressionPolicy): CustomCompressionPolicyV3 {
200
+ if (value.version === 3) return structuredClone(value)
201
+ return {
202
+ version: 3,
203
+ unit: value.unit,
204
+ fresh: structuredClone(value.fresh),
205
+ aggregate: structuredClone(value.aggregate),
206
+ history: {
207
+ enabled: value.history.enabled,
208
+ trigger: value.history.trigger,
209
+ keepRecentToolCalls: 10,
210
+ keepRecentTokens: value.history.keepRecent,
211
+ minReclaim: value.history.minReclaim,
212
+ },
213
+ prefixPolicy: value.prefixPolicy,
214
+ tailTrim: value.version === 1 ? { enabled: false, trigger: 700_000 } : structuredClone(value.tailTrim),
215
+ }
216
+ }