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.
- package/README.md +59 -51
- package/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +386 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +131 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +123 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/shape.ts +75 -0
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +534 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +184 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +116 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +466 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +109 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +203 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
package/src/tui/settings.tsx
CHANGED
|
@@ -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
|
+
}
|