compressed-context 0.1.0 → 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 (57) hide show
  1. package/README.md +59 -51
  2. package/package.json +64 -64
  3. package/src/compression/active-summary-cache.ts +50 -50
  4. package/src/compression/adopt.ts +57 -57
  5. package/src/compression/auto.ts +386 -372
  6. package/src/compression/babel.ts +85 -85
  7. package/src/compression/block.ts +25 -25
  8. package/src/compression/cache-history.ts +75 -75
  9. package/src/compression/compaction.ts +189 -189
  10. package/src/compression/decompress-tool.ts +74 -74
  11. package/src/compression/decompress.ts +292 -292
  12. package/src/compression/deleted-sessions.ts +24 -24
  13. package/src/compression/distill.ts +220 -220
  14. package/src/compression/events.ts +275 -275
  15. package/src/compression/markdown.ts +53 -53
  16. package/src/compression/normalize.ts +114 -114
  17. package/src/compression/render.ts +70 -70
  18. package/src/compression/schema.ts +131 -84
  19. package/src/compression/select.ts +123 -123
  20. package/src/compression/session-message.ts +112 -112
  21. package/src/compression/settings-rpc.ts +123 -111
  22. package/src/compression/settings.ts +175 -175
  23. package/src/compression/shape.ts +75 -0
  24. package/src/compression/state.ts +248 -248
  25. package/src/compression/summarize.ts +534 -595
  26. package/src/compression/summary.ts +73 -73
  27. package/src/compression/tool.ts +184 -142
  28. package/src/compression/transcript-store.ts +258 -258
  29. package/src/compression/transcript.ts +63 -63
  30. package/src/compression/validate.ts +81 -81
  31. package/src/i18n/en.ts +93 -93
  32. package/src/i18n/index.ts +28 -28
  33. package/src/i18n/types.ts +89 -89
  34. package/src/i18n/zh.ts +93 -93
  35. package/src/index.ts +116 -108
  36. package/src/shared/blocks.ts +17 -17
  37. package/src/shared/cache.ts +199 -199
  38. package/src/shared/format.ts +23 -23
  39. package/src/shared/media.ts +184 -184
  40. package/src/shared/parse.ts +25 -25
  41. package/src/shared/parts.ts +72 -72
  42. package/src/shared/rpc.ts +145 -145
  43. package/src/shared/scan.ts +32 -32
  44. package/src/shared/settings.ts +466 -471
  45. package/src/shared/stats.ts +142 -142
  46. package/src/shared/tokens.ts +259 -259
  47. package/src/tui/chart/cache-chart.ts +237 -237
  48. package/src/tui/chart/color.ts +58 -58
  49. package/src/tui/index.tsx +109 -111
  50. package/src/tui/panel/cache.tsx +203 -203
  51. package/src/tui/panel/data.ts +105 -105
  52. package/src/tui/panel/index.tsx +176 -176
  53. package/src/tui/panel/summaries.tsx +203 -192
  54. package/src/tui/settings/dialogs.tsx +215 -215
  55. package/src/tui/settings/options.ts +238 -238
  56. package/src/tui/settings.tsx +418 -418
  57. package/src/tui/types.ts +4 -4
@@ -1,418 +1,418 @@
1
- import type { Context } from "@opencode/plugin/tui/context"
2
- import { createEffect, createMemo, createSignal, onMount } from "solid-js"
3
-
4
- import { formatTokens } from "../shared/format.ts"
5
- import { resolveMessages } from "../i18n/index.ts"
6
- import { contextRpc } from "../shared/rpc.ts"
7
- import {
8
- localeArgument,
9
- MAX_CONTEXT_LIMIT,
10
- MAX_DECOMPRESS_INLINE_MAX,
11
- MAX_NUDGE_GROWTH_TOKENS,
12
- MAX_DECOMPRESS_TEMP_CAP,
13
- MIN_CONTEXT_LIMIT,
14
- MIN_DECOMPRESS_INLINE_MAX,
15
- MIN_DECOMPRESS_TEMP_CAP,
16
- effectiveDecompressInlineMax,
17
- effectiveDecompressTempCap,
18
- effectiveSummaryBlockPercent,
19
- effectiveSummaryLanguage,
20
- effectiveSummaryTotalPercent,
21
- maxSummaryPercent,
22
- serverSettingsPayload,
23
- type ContextLimit,
24
- type LocaleSetting,
25
- type Settings,
26
- type SummaryFormat,
27
- type SummaryLanguage,
28
- } from "../shared/settings.ts"
29
- import { PercentDialog, ThresholdDialog, TokenDialog } from "./settings/dialogs.tsx"
30
- import { buildSettingsOptions, type SettingsAction, type ToggleSetting } from "./settings/options.ts"
31
- import type { UpdateSettings } from "./types.ts"
32
-
33
- /** 可选上限预设;菜单里只出现合法值(最低 50k)。 */
34
- const PRESET_LIMITS = [
35
- 64_000,
36
- 100_000,
37
- 128_000,
38
- 200_000,
39
- 256_000,
40
- 300_000,
41
- 400_000,
42
- 512_000,
43
- 768_000,
44
- 1_000_000,
45
- ] as const
46
-
47
- /**
48
- * 输入:TUI 插件上下文 + 设置。
49
- * 输出:注册 /compressed-context 命令(斜杠 + 命令面板)。
50
- * 预期:打开设置菜单;所有文案来自语言文件,切换语言后命令会重新注册。
51
- */
52
- export function SettingsCommand(props: {
53
- readonly context: Context
54
- readonly settings: Settings
55
- readonly updateSettings: UpdateSettings
56
- }) {
57
- const copy = createMemo(() => resolveMessages(localeArgument(props.settings.locale)))
58
- const [resolvedWindow, setResolvedWindow] = createSignal<number | undefined>(undefined)
59
-
60
- /** 输入:无;输出:当前生效的窗口;预期:数字设置是上限,取 min(设置, 原生窗口)。 */
61
- const currentWindow = (): number | undefined => {
62
- const limit: unknown = props.settings.contextLimit
63
- const native = resolvedWindow()
64
- if (typeof limit === "number" && Number.isFinite(limit)) {
65
- return native === undefined ? limit : Math.min(limit, native)
66
- }
67
- return native
68
- }
69
-
70
- /** 输入:sessionID;输出:无;预期:按会话模型拉取原生窗口,失败保留旧值。 */
71
- const fetchWindow = async (sessionID: string | undefined): Promise<void> => {
72
- try {
73
- const got = await contextRpc(props.context.client).settingsGet(
74
- sessionID === undefined ? {} : { sessionID },
75
- )
76
- if (typeof got.resolvedWindow === "number" && got.resolvedWindow > 0) setResolvedWindow(got.resolvedWindow)
77
- } catch {
78
- // server plugin not loaded
79
- }
80
- }
81
-
82
- /** 输入:无;输出:当前路由的 sessionID;预期:不在会话页时返回 undefined。 */
83
- const routeSessionID = (): string | undefined => {
84
- const route = props.context.ui.router.current()
85
- return route.type === "session" ? route.sessionID : undefined
86
- }
87
-
88
- // 读取当前会话模型的原生窗口,auto 时也能把绝对值换算成百分比。
89
- onMount(() => {
90
- void fetchWindow(routeSessionID())
91
- })
92
-
93
- // 设置变化时推给服务端;服务端插件未加载时静默忽略。
94
- createEffect(() => {
95
- try {
96
- void contextRpc(props.context.client)
97
- .settingsSet(serverSettingsPayload(props.settings))
98
- .catch(() => undefined)
99
- } catch {
100
- // server plugin not loaded
101
- }
102
- })
103
-
104
- const openNudge = (): void => {
105
- props.context.ui.dialog.show(() => (
106
- <TokenDialog
107
- context={props.context}
108
- copy={copy()}
109
- title={copy().settings.nudgeGrowth}
110
- hint={copy().settings.nudgeGrowthHint}
111
- invalid={copy().settings.nudgeGrowthInvalid}
112
- placeholder="50k"
113
- initial={props.settings.nudgeGrowthTokens === 0 ? "0" : formatTokens(props.settings.nudgeGrowthTokens)}
114
- min={0}
115
- max={MAX_NUDGE_GROWTH_TOKENS}
116
- allowZero
117
- onConfirm={(value) => {
118
- void props.updateSettings((draft) => {
119
- draft.nudgeGrowthTokens = value
120
- })
121
- }}
122
- />
123
- ))
124
- }
125
-
126
- const openThreshold = (): void => {
127
- props.context.ui.dialog.show(() => (
128
- <ThresholdDialog
129
- context={props.context}
130
- copy={copy()}
131
- initial={props.settings.triggerPercent.toString()}
132
- onConfirm={(value) => {
133
- void props.updateSettings((draft) => {
134
- draft.triggerPercent = value
135
- })
136
- }}
137
- />
138
- ))
139
- }
140
-
141
- const openLocale = async (): Promise<void> => {
142
- const picked = await props.context.ui.dialog.select<LocaleSetting>({
143
- title: copy().settings.language,
144
- current: props.settings.locale,
145
- options: [
146
- { title: copy().settings.auto, value: "auto" },
147
- { title: copy().languages.zh, value: "zh" },
148
- { title: copy().languages.en, value: "en" },
149
- ],
150
- })
151
- if (picked === undefined) return
152
- await props.updateSettings((draft) => {
153
- draft.locale = picked
154
- })
155
- }
156
-
157
- const openCustom = (): void => {
158
- props.context.ui.dialog.show(() => (
159
- <TokenDialog
160
- context={props.context}
161
- copy={copy()}
162
- title={copy().settings.limit}
163
- hint={copy().settings.limitHint}
164
- invalid={copy().settings.limitInvalid}
165
- placeholder={copy().settings.limitPlaceholder}
166
- initial={props.settings.contextLimit === "auto" ? "" : props.settings.contextLimit.toString()}
167
- min={MIN_CONTEXT_LIMIT}
168
- max={MAX_CONTEXT_LIMIT}
169
- onConfirm={(value) => {
170
- void props.updateSettings((draft) => {
171
- draft.contextLimit = value
172
- })
173
- }}
174
- />
175
- ))
176
- }
177
-
178
- const openSummaryBlock = (): void => {
179
- const window = currentWindow()
180
- props.context.ui.dialog.show(() => (
181
- <PercentDialog
182
- context={props.context}
183
- copy={copy()}
184
- title={copy().settings.summaryBlockPercent}
185
- hint={copy().settings.summaryBlockPercentHint}
186
- invalid={copy().settings.summaryPercentInvalid}
187
- initial={effectiveSummaryBlockPercent(props.settings)}
188
- windowTokens={window}
189
- minPercent={1}
190
- maxPercent={maxSummaryPercent(window)}
191
- onConfirm={(next) => {
192
- void props.updateSettings((draft) => {
193
- draft.summaryBlockPercent = next
194
- })
195
- }}
196
- />
197
- ))
198
- }
199
-
200
- const openSummaryTotal = (): void => {
201
- const window = currentWindow()
202
- props.context.ui.dialog.show(() => (
203
- <PercentDialog
204
- context={props.context}
205
- copy={copy()}
206
- title={copy().settings.summaryTotalPercent}
207
- hint={copy().settings.summaryTotalPercentHint}
208
- invalid={copy().settings.summaryPercentInvalid}
209
- initial={effectiveSummaryTotalPercent(props.settings)}
210
- windowTokens={window}
211
- minPercent={1}
212
- maxPercent={maxSummaryPercent(window)}
213
- onConfirm={(next) => {
214
- void props.updateSettings((draft) => {
215
- draft.summaryTotalPercent = next
216
- })
217
- }}
218
- />
219
- ))
220
- }
221
-
222
- const openSummaryLanguage = async (): Promise<void> => {
223
- const picked = await props.context.ui.dialog.select<SummaryLanguage>({
224
- title: copy().settings.summaryLanguage,
225
- current: effectiveSummaryLanguage(props.settings),
226
- options: [
227
- { title: copy().settings.auto, value: "auto" },
228
- { title: copy().languages.zh, value: "zh" },
229
- { title: copy().languages.en, value: "en" },
230
- ],
231
- })
232
- if (picked === undefined) return
233
- await props.updateSettings((draft) => {
234
- draft.summaryLanguage = picked
235
- })
236
- }
237
-
238
- const openSummaryFormat = async (): Promise<void> => {
239
- const picked = await props.context.ui.dialog.select<SummaryFormat>({
240
- title: copy().settings.summaryFormat,
241
- current: props.settings.summaryFormat,
242
- options: [
243
- { title: copy().settings.summaryFormatJson, value: "json" },
244
- { title: copy().settings.summaryFormatMarkdown, value: "markdown" },
245
- { title: copy().settings.summaryFormatBabel, value: "babel" },
246
- ],
247
- })
248
- if (picked === undefined) return
249
- await props.updateSettings((draft) => {
250
- draft.summaryFormat = picked
251
- })
252
- }
253
-
254
- /** 输入:布尔设置键;输出:无;预期:翻转后重开菜单。 */
255
- const toggleSetting = async (key: ToggleSetting): Promise<void> => {
256
- await props.updateSettings((draft) => {
257
- draft[key] = !draft[key]
258
- })
259
- void openSettings()
260
- }
261
-
262
- const openDecompressInline = (): void => {
263
- props.context.ui.dialog.show(() => (
264
- <TokenDialog
265
- context={props.context}
266
- copy={copy()}
267
- title={copy().settings.decompressInlineMax}
268
- hint={copy().settings.decompressInlineMaxHint}
269
- invalid={copy().settings.decompressInlineMaxInvalid}
270
- placeholder="10k"
271
- initial={formatTokens(effectiveDecompressInlineMax(props.settings))}
272
- min={MIN_DECOMPRESS_INLINE_MAX}
273
- max={MAX_DECOMPRESS_INLINE_MAX}
274
- onConfirm={(next) => {
275
- void props.updateSettings((draft) => {
276
- draft.decompressInlineMax = next
277
- })
278
- }}
279
- />
280
- ))
281
- }
282
-
283
- const openDecompressCap = (): void => {
284
- props.context.ui.dialog.show(() => (
285
- <TokenDialog
286
- context={props.context}
287
- copy={copy()}
288
- title={copy().settings.decompressTempCap}
289
- hint={copy().settings.decompressTempCapHint}
290
- invalid={copy().settings.decompressTempCapInvalid}
291
- placeholder="50"
292
- initial={effectiveDecompressTempCap(props.settings).toString()}
293
- min={MIN_DECOMPRESS_TEMP_CAP}
294
- max={MAX_DECOMPRESS_TEMP_CAP}
295
- onConfirm={(next) => {
296
- void props.updateSettings((draft) => {
297
- draft.decompressTempCap = next
298
- })
299
- }}
300
- />
301
- ))
302
- }
303
-
304
- const openLimit = async (): Promise<boolean> => {
305
- const picked = await props.context.ui.dialog.select<ContextLimit | "custom">({
306
- title: copy().settings.limit,
307
- current: props.settings.contextLimit,
308
- options: [
309
- { title: copy().settings.limitAuto, value: "auto" },
310
- ...PRESET_LIMITS.map((value) => ({ title: formatTokens(value), value })),
311
- { title: copy().settings.limitCustom, value: "custom" as const },
312
- ],
313
- })
314
- if (picked === undefined) return false
315
- if (picked === "custom") {
316
- openCustom()
317
- return true
318
- }
319
- await props.updateSettings((draft) => {
320
- draft.contextLimit = picked
321
- })
322
- return false
323
- }
324
-
325
- const openSettings = async (): Promise<void> => {
326
- // 每次打开菜单都按当前会话模型刷新窗口,避免显示成别的模型。
327
- await fetchWindow(routeSessionID())
328
- const window = currentWindow()
329
- const picked = await props.context.ui.dialog.select<SettingsAction>({
330
- title: copy().settings.title,
331
- options: buildSettingsOptions({ settings: props.settings, copy: copy(), window }),
332
- })
333
- if (picked === "show") {
334
- await props.updateSettings((draft) => {
335
- draft.enabled = !draft.enabled
336
- })
337
- void openSettings()
338
- return
339
- }
340
- if (picked === "limit") {
341
- const custom = await openLimit()
342
- // 自定义对话框已经打开时不要再弹主菜单,否则会把它顶掉。
343
- if (!custom) void openSettings()
344
- return
345
- }
346
- if (picked === "threshold") {
347
- openThreshold()
348
- return
349
- }
350
- if (picked === "nudge") {
351
- openNudge()
352
- return
353
- }
354
- if (picked === "blockPercent") {
355
- openSummaryBlock()
356
- return
357
- }
358
- if (picked === "totalPercent") {
359
- openSummaryTotal()
360
- return
361
- }
362
- if (picked === "summaryLanguage") {
363
- await openSummaryLanguage()
364
- void openSettings()
365
- return
366
- }
367
- if (picked === "summaryFormat") {
368
- await openSummaryFormat()
369
- void openSettings()
370
- return
371
- }
372
- if (
373
- picked === "autoCompress" ||
374
- picked === "enableCompressTool" ||
375
- picked === "enableDecompressTool" ||
376
- picked === "showTimeline" ||
377
- picked === "showBlockTokens" ||
378
- picked === "showCacheChart" ||
379
- picked === "showCacheLog" ||
380
- picked === "showSummaries" ||
381
- picked === "decompressShowTokens"
382
- ) {
383
- await toggleSetting(picked)
384
- return
385
- }
386
- if (picked === "decompressInlineMax") {
387
- openDecompressInline()
388
- return
389
- }
390
- if (picked === "decompressTempCap") {
391
- openDecompressCap()
392
- return
393
- }
394
- if (picked === "language") {
395
- await openLocale()
396
- void openSettings()
397
- }
398
- }
399
-
400
- props.context.keymap.layer(() => ({
401
- mode: "global",
402
- commands: [
403
- {
404
- id: "compressed-context.settings",
405
- title: copy().command.title,
406
- description: copy().command.description,
407
- group: copy().command.group,
408
- slash: { name: "compressed-context" },
409
- palette: true,
410
- run() {
411
- void openSettings()
412
- },
413
- },
414
- ],
415
- }))
416
-
417
- return null
418
- }
1
+ import type { Context } from "@opencode/plugin/tui/context"
2
+ import { createEffect, createMemo, createSignal, onMount } from "solid-js"
3
+
4
+ import { formatTokens } from "../shared/format.ts"
5
+ import { resolveMessages } from "../i18n/index.ts"
6
+ import { contextRpc } from "../shared/rpc.ts"
7
+ import {
8
+ localeArgument,
9
+ MAX_CONTEXT_LIMIT,
10
+ MAX_DECOMPRESS_INLINE_MAX,
11
+ MAX_NUDGE_GROWTH_TOKENS,
12
+ MAX_DECOMPRESS_TEMP_CAP,
13
+ MIN_CONTEXT_LIMIT,
14
+ MIN_DECOMPRESS_INLINE_MAX,
15
+ MIN_DECOMPRESS_TEMP_CAP,
16
+ effectiveDecompressInlineMax,
17
+ effectiveDecompressTempCap,
18
+ effectiveSummaryBlockPercent,
19
+ effectiveSummaryLanguage,
20
+ effectiveSummaryTotalPercent,
21
+ maxSummaryPercent,
22
+ serverSettingsPayload,
23
+ type ContextLimit,
24
+ type LocaleSetting,
25
+ type Settings,
26
+ type SummaryFormat,
27
+ type SummaryLanguage,
28
+ } from "../shared/settings.ts"
29
+ import { PercentDialog, ThresholdDialog, TokenDialog } from "./settings/dialogs.tsx"
30
+ import { buildSettingsOptions, type SettingsAction, type ToggleSetting } from "./settings/options.ts"
31
+ import type { UpdateSettings } from "./types.ts"
32
+
33
+ /** 可选上限预设;菜单里只出现合法值(最低 50k)。 */
34
+ const PRESET_LIMITS = [
35
+ 64_000,
36
+ 100_000,
37
+ 128_000,
38
+ 200_000,
39
+ 256_000,
40
+ 300_000,
41
+ 400_000,
42
+ 512_000,
43
+ 768_000,
44
+ 1_000_000,
45
+ ] as const
46
+
47
+ /**
48
+ * 输入:TUI 插件上下文 + 设置。
49
+ * 输出:注册 /compressed-context 命令(斜杠 + 命令面板)。
50
+ * 预期:打开设置菜单;所有文案来自语言文件,切换语言后命令会重新注册。
51
+ */
52
+ export function SettingsCommand(props: {
53
+ readonly context: Context
54
+ readonly settings: Settings
55
+ readonly updateSettings: UpdateSettings
56
+ }) {
57
+ const copy = createMemo(() => resolveMessages(localeArgument(props.settings.locale)))
58
+ const [resolvedWindow, setResolvedWindow] = createSignal<number | undefined>(undefined)
59
+
60
+ /** 输入:无;输出:当前生效的窗口;预期:数字设置是上限,取 min(设置, 原生窗口)。 */
61
+ const currentWindow = (): number | undefined => {
62
+ const limit: unknown = props.settings.contextLimit
63
+ const native = resolvedWindow()
64
+ if (typeof limit === "number" && Number.isFinite(limit)) {
65
+ return native === undefined ? limit : Math.min(limit, native)
66
+ }
67
+ return native
68
+ }
69
+
70
+ /** 输入:sessionID;输出:无;预期:按会话模型拉取原生窗口,失败保留旧值。 */
71
+ const fetchWindow = async (sessionID: string | undefined): Promise<void> => {
72
+ try {
73
+ const got = await contextRpc(props.context.client).settingsGet(
74
+ sessionID === undefined ? {} : { sessionID },
75
+ )
76
+ if (typeof got.resolvedWindow === "number" && got.resolvedWindow > 0) setResolvedWindow(got.resolvedWindow)
77
+ } catch {
78
+ // server plugin not loaded
79
+ }
80
+ }
81
+
82
+ /** 输入:无;输出:当前路由的 sessionID;预期:不在会话页时返回 undefined。 */
83
+ const routeSessionID = (): string | undefined => {
84
+ const route = props.context.ui.router.current()
85
+ return route.type === "session" ? route.sessionID : undefined
86
+ }
87
+
88
+ // 读取当前会话模型的原生窗口,auto 时也能把绝对值换算成百分比。
89
+ onMount(() => {
90
+ void fetchWindow(routeSessionID())
91
+ })
92
+
93
+ // 设置变化时推给服务端;服务端插件未加载时静默忽略。
94
+ createEffect(() => {
95
+ try {
96
+ void contextRpc(props.context.client)
97
+ .settingsSet(serverSettingsPayload(props.settings))
98
+ .catch(() => undefined)
99
+ } catch {
100
+ // server plugin not loaded
101
+ }
102
+ })
103
+
104
+ const openNudge = (): void => {
105
+ props.context.ui.dialog.show(() => (
106
+ <TokenDialog
107
+ context={props.context}
108
+ copy={copy()}
109
+ title={copy().settings.nudgeGrowth}
110
+ hint={copy().settings.nudgeGrowthHint}
111
+ invalid={copy().settings.nudgeGrowthInvalid}
112
+ placeholder="50k"
113
+ initial={props.settings.nudgeGrowthTokens === 0 ? "0" : formatTokens(props.settings.nudgeGrowthTokens)}
114
+ min={0}
115
+ max={MAX_NUDGE_GROWTH_TOKENS}
116
+ allowZero
117
+ onConfirm={(value) => {
118
+ void props.updateSettings((draft) => {
119
+ draft.nudgeGrowthTokens = value
120
+ })
121
+ }}
122
+ />
123
+ ))
124
+ }
125
+
126
+ const openThreshold = (): void => {
127
+ props.context.ui.dialog.show(() => (
128
+ <ThresholdDialog
129
+ context={props.context}
130
+ copy={copy()}
131
+ initial={props.settings.triggerPercent.toString()}
132
+ onConfirm={(value) => {
133
+ void props.updateSettings((draft) => {
134
+ draft.triggerPercent = value
135
+ })
136
+ }}
137
+ />
138
+ ))
139
+ }
140
+
141
+ const openLocale = async (): Promise<void> => {
142
+ const picked = await props.context.ui.dialog.select<LocaleSetting>({
143
+ title: copy().settings.language,
144
+ current: props.settings.locale,
145
+ options: [
146
+ { title: copy().settings.auto, value: "auto" },
147
+ { title: copy().languages.zh, value: "zh" },
148
+ { title: copy().languages.en, value: "en" },
149
+ ],
150
+ })
151
+ if (picked === undefined) return
152
+ await props.updateSettings((draft) => {
153
+ draft.locale = picked
154
+ })
155
+ }
156
+
157
+ const openCustom = (): void => {
158
+ props.context.ui.dialog.show(() => (
159
+ <TokenDialog
160
+ context={props.context}
161
+ copy={copy()}
162
+ title={copy().settings.limit}
163
+ hint={copy().settings.limitHint}
164
+ invalid={copy().settings.limitInvalid}
165
+ placeholder={copy().settings.limitPlaceholder}
166
+ initial={props.settings.contextLimit === "auto" ? "" : props.settings.contextLimit.toString()}
167
+ min={MIN_CONTEXT_LIMIT}
168
+ max={MAX_CONTEXT_LIMIT}
169
+ onConfirm={(value) => {
170
+ void props.updateSettings((draft) => {
171
+ draft.contextLimit = value
172
+ })
173
+ }}
174
+ />
175
+ ))
176
+ }
177
+
178
+ const openSummaryBlock = (): void => {
179
+ const window = currentWindow()
180
+ props.context.ui.dialog.show(() => (
181
+ <PercentDialog
182
+ context={props.context}
183
+ copy={copy()}
184
+ title={copy().settings.summaryBlockPercent}
185
+ hint={copy().settings.summaryBlockPercentHint}
186
+ invalid={copy().settings.summaryPercentInvalid}
187
+ initial={effectiveSummaryBlockPercent(props.settings)}
188
+ windowTokens={window}
189
+ minPercent={1}
190
+ maxPercent={maxSummaryPercent(window)}
191
+ onConfirm={(next) => {
192
+ void props.updateSettings((draft) => {
193
+ draft.summaryBlockPercent = next
194
+ })
195
+ }}
196
+ />
197
+ ))
198
+ }
199
+
200
+ const openSummaryTotal = (): void => {
201
+ const window = currentWindow()
202
+ props.context.ui.dialog.show(() => (
203
+ <PercentDialog
204
+ context={props.context}
205
+ copy={copy()}
206
+ title={copy().settings.summaryTotalPercent}
207
+ hint={copy().settings.summaryTotalPercentHint}
208
+ invalid={copy().settings.summaryPercentInvalid}
209
+ initial={effectiveSummaryTotalPercent(props.settings)}
210
+ windowTokens={window}
211
+ minPercent={1}
212
+ maxPercent={maxSummaryPercent(window)}
213
+ onConfirm={(next) => {
214
+ void props.updateSettings((draft) => {
215
+ draft.summaryTotalPercent = next
216
+ })
217
+ }}
218
+ />
219
+ ))
220
+ }
221
+
222
+ const openSummaryLanguage = async (): Promise<void> => {
223
+ const picked = await props.context.ui.dialog.select<SummaryLanguage>({
224
+ title: copy().settings.summaryLanguage,
225
+ current: effectiveSummaryLanguage(props.settings),
226
+ options: [
227
+ { title: copy().settings.auto, value: "auto" },
228
+ { title: copy().languages.zh, value: "zh" },
229
+ { title: copy().languages.en, value: "en" },
230
+ ],
231
+ })
232
+ if (picked === undefined) return
233
+ await props.updateSettings((draft) => {
234
+ draft.summaryLanguage = picked
235
+ })
236
+ }
237
+
238
+ const openSummaryFormat = async (): Promise<void> => {
239
+ const picked = await props.context.ui.dialog.select<SummaryFormat>({
240
+ title: copy().settings.summaryFormat,
241
+ current: props.settings.summaryFormat,
242
+ options: [
243
+ { title: copy().settings.summaryFormatJson, value: "json" },
244
+ { title: copy().settings.summaryFormatMarkdown, value: "markdown" },
245
+ { title: copy().settings.summaryFormatBabel, value: "babel" },
246
+ ],
247
+ })
248
+ if (picked === undefined) return
249
+ await props.updateSettings((draft) => {
250
+ draft.summaryFormat = picked
251
+ })
252
+ }
253
+
254
+ /** 输入:布尔设置键;输出:无;预期:翻转后重开菜单。 */
255
+ const toggleSetting = async (key: ToggleSetting): Promise<void> => {
256
+ await props.updateSettings((draft) => {
257
+ draft[key] = !draft[key]
258
+ })
259
+ void openSettings()
260
+ }
261
+
262
+ const openDecompressInline = (): void => {
263
+ props.context.ui.dialog.show(() => (
264
+ <TokenDialog
265
+ context={props.context}
266
+ copy={copy()}
267
+ title={copy().settings.decompressInlineMax}
268
+ hint={copy().settings.decompressInlineMaxHint}
269
+ invalid={copy().settings.decompressInlineMaxInvalid}
270
+ placeholder="10k"
271
+ initial={formatTokens(effectiveDecompressInlineMax(props.settings))}
272
+ min={MIN_DECOMPRESS_INLINE_MAX}
273
+ max={MAX_DECOMPRESS_INLINE_MAX}
274
+ onConfirm={(next) => {
275
+ void props.updateSettings((draft) => {
276
+ draft.decompressInlineMax = next
277
+ })
278
+ }}
279
+ />
280
+ ))
281
+ }
282
+
283
+ const openDecompressCap = (): void => {
284
+ props.context.ui.dialog.show(() => (
285
+ <TokenDialog
286
+ context={props.context}
287
+ copy={copy()}
288
+ title={copy().settings.decompressTempCap}
289
+ hint={copy().settings.decompressTempCapHint}
290
+ invalid={copy().settings.decompressTempCapInvalid}
291
+ placeholder="50"
292
+ initial={effectiveDecompressTempCap(props.settings).toString()}
293
+ min={MIN_DECOMPRESS_TEMP_CAP}
294
+ max={MAX_DECOMPRESS_TEMP_CAP}
295
+ onConfirm={(next) => {
296
+ void props.updateSettings((draft) => {
297
+ draft.decompressTempCap = next
298
+ })
299
+ }}
300
+ />
301
+ ))
302
+ }
303
+
304
+ const openLimit = async (): Promise<boolean> => {
305
+ const picked = await props.context.ui.dialog.select<ContextLimit | "custom">({
306
+ title: copy().settings.limit,
307
+ current: props.settings.contextLimit,
308
+ options: [
309
+ { title: copy().settings.limitAuto, value: "auto" },
310
+ ...PRESET_LIMITS.map((value) => ({ title: formatTokens(value), value })),
311
+ { title: copy().settings.limitCustom, value: "custom" as const },
312
+ ],
313
+ })
314
+ if (picked === undefined) return false
315
+ if (picked === "custom") {
316
+ openCustom()
317
+ return true
318
+ }
319
+ await props.updateSettings((draft) => {
320
+ draft.contextLimit = picked
321
+ })
322
+ return false
323
+ }
324
+
325
+ const openSettings = async (): Promise<void> => {
326
+ // 每次打开菜单都按当前会话模型刷新窗口,避免显示成别的模型。
327
+ await fetchWindow(routeSessionID())
328
+ const window = currentWindow()
329
+ const picked = await props.context.ui.dialog.select<SettingsAction>({
330
+ title: copy().settings.title,
331
+ options: buildSettingsOptions({ settings: props.settings, copy: copy(), window }),
332
+ })
333
+ if (picked === "show") {
334
+ await props.updateSettings((draft) => {
335
+ draft.enabled = !draft.enabled
336
+ })
337
+ void openSettings()
338
+ return
339
+ }
340
+ if (picked === "limit") {
341
+ const custom = await openLimit()
342
+ // 自定义对话框已经打开时不要再弹主菜单,否则会把它顶掉。
343
+ if (!custom) void openSettings()
344
+ return
345
+ }
346
+ if (picked === "threshold") {
347
+ openThreshold()
348
+ return
349
+ }
350
+ if (picked === "nudge") {
351
+ openNudge()
352
+ return
353
+ }
354
+ if (picked === "blockPercent") {
355
+ openSummaryBlock()
356
+ return
357
+ }
358
+ if (picked === "totalPercent") {
359
+ openSummaryTotal()
360
+ return
361
+ }
362
+ if (picked === "summaryLanguage") {
363
+ await openSummaryLanguage()
364
+ void openSettings()
365
+ return
366
+ }
367
+ if (picked === "summaryFormat") {
368
+ await openSummaryFormat()
369
+ void openSettings()
370
+ return
371
+ }
372
+ if (
373
+ picked === "autoCompress" ||
374
+ picked === "enableCompressTool" ||
375
+ picked === "enableDecompressTool" ||
376
+ picked === "showTimeline" ||
377
+ picked === "showBlockTokens" ||
378
+ picked === "showCacheChart" ||
379
+ picked === "showCacheLog" ||
380
+ picked === "showSummaries" ||
381
+ picked === "decompressShowTokens"
382
+ ) {
383
+ await toggleSetting(picked)
384
+ return
385
+ }
386
+ if (picked === "decompressInlineMax") {
387
+ openDecompressInline()
388
+ return
389
+ }
390
+ if (picked === "decompressTempCap") {
391
+ openDecompressCap()
392
+ return
393
+ }
394
+ if (picked === "language") {
395
+ await openLocale()
396
+ void openSettings()
397
+ }
398
+ }
399
+
400
+ props.context.keymap.layer(() => ({
401
+ mode: "global",
402
+ commands: [
403
+ {
404
+ id: "compressed-context.settings",
405
+ title: copy().command.title,
406
+ description: copy().command.description,
407
+ group: copy().command.group,
408
+ slash: { name: "compressed-context" },
409
+ palette: true,
410
+ run() {
411
+ void openSettings()
412
+ },
413
+ },
414
+ ],
415
+ }))
416
+
417
+ return null
418
+ }