compressed-context 0.1.1 → 0.2.1
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 +5 -5
- package/src/compression/auto.ts +15 -1
- package/src/compression/schema.ts +47 -0
- package/src/compression/settings-rpc.ts +15 -3
- package/src/compression/shape.ts +75 -0
- package/src/compression/summarize.ts +31 -80
- package/src/compression/tool.ts +60 -18
- package/src/i18n/en.ts +3 -3
- package/src/i18n/zh.ts +3 -3
- package/src/index.ts +10 -2
- package/src/shared/settings.ts +0 -5
- package/src/tui/index.tsx +0 -2
- package/src/tui/panel/summaries.tsx +26 -15
package/README.md
CHANGED
|
@@ -1,89 +1,97 @@
|
|
|
1
1
|
# compressed-context
|
|
2
2
|
|
|
3
|
-
OpenCode
|
|
3
|
+
OpenCode 的上下文压缩插件。它会将较早的对话历史折叠成独立的摘要块,让长会话在有限的上下文窗口里继续跑下去。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **手动压缩**:模型可主动调用 `compress` 工具,在任意时刻打检查点
|
|
9
|
-
- **分层摘要**:T1 压缩当前对话流;T2 / T3 合并相邻摘要,层级单调递减
|
|
10
|
-
- **解压**:`decompress` 还原某个块覆盖的原始消息,可只还原下一层或展开到原文
|
|
11
|
-
- **缓存友好**:压缩请求保持前缀不变,尽量减少缓存未命中
|
|
12
|
-
- **摘要格式**:Markdown(默认)或 Babel(首行 `T=标题|G=目标`,正文自由高密度)
|
|
13
|
-
- **主动提醒**:上下文增长超过设定值时,在请求尾部追加一次提醒,让模型自己决定何时压缩
|
|
14
|
-
- **TUI 面板**:查看摘要块、缓存命中率、上下文构成,设置项在插件设置里直接改
|
|
7
|
+
## 架构与工作原理
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
插件接管发给模型的上下文:把较早的对话折叠成摘要块,让长会话继续跑下去:
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
```text
|
|
12
|
+
原始上下文:
|
|
13
|
+
[ 系统提示词 ][ 早期历史对话 ............................. ][ 最近活跃对话 ]
|
|
14
|
+
↓ 触发压缩 (T1)
|
|
15
|
+
一级压缩后:
|
|
16
|
+
[ 系统提示词 ][ T1 摘要块 #1 ][ T1 摘要块 #2 ] ............ [ 最近活跃对话 ]
|
|
17
|
+
↓ 摘要堆积达到阈值 (T2/T3)
|
|
18
|
+
分层合并后:
|
|
19
|
+
[ 系统提示词 ][ T2 合并摘要 (块#1+#2) ][ T1 摘要块 #3 ] .... [ 最近活跃对话 ]
|
|
20
|
+
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
- **递归分层折叠**:
|
|
23
|
+
- **T1 级**:将溢出的原始对话流折叠为自包含检查点(原始消息移入持久化存储,释放窗口)。
|
|
24
|
+
- **T2 / T3 级**:当摘要块自身占用接近预算上限时,自动在背景对相邻摘要进行高阶合并(`T1 + T1 → T2`,`T2 + T2 → T3`)。
|
|
25
|
+
- **无损按需回溯**:模型可在需要时通过 `decompress` 按块还原历史,支持只解压下一层级摘要或直接恢复原始对话流。
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
## 核心特性
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
- **自动压缩**:上下文达到阈值时自动触发(工具调用循环中的子步骤同样生效)。
|
|
30
|
+
- **主动压缩**:向模型暴露 `compress` 工具,允许模型在阶段性任务完成后主动打检查点。
|
|
31
|
+
- **多级合并**:T1 负责压缩原始对话;当摘要本身积攒过多时,会自动向上合并为 T2、T3 高阶摘要。
|
|
32
|
+
- **按需解压**:模型可通过 `decompress` 工具还原指定摘要块(支持逐层展开或直接恢复为原始对话)。
|
|
33
|
+
- **缓存友好**:压缩请求尽量保持前缀不变,避免破坏 Prompt Cache。
|
|
34
|
+
- **双摘要格式**:支持标准 `markdown`(默认)和极致压缩的 `babel` 格式(首行 `T=标题|G=目标`,正文为高密度非自然语言)。
|
|
35
|
+
- **超长提醒**:上下文增长超过设定阈值时,在请求尾部追加轻量提醒,引导模型自主决定压缩时机。
|
|
36
|
+
- **TUI 面板**:直观查看摘要块、缓存命中率与上下文构成,支持在面板内直接修改配置。
|
|
37
|
+
|
|
38
|
+
## 安装与配置
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
插件通过 npm 分发,OpenCode 启动时会自动安装。
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
由于本插件会接管上下文管理,**建议同时关闭 OpenCode 原生的自动压缩**,避免两套策略打架。在全局配置 `~/.config/opencode/opencode.json`(或项目级 `opencode.json`)中加入:
|
|
33
43
|
|
|
34
44
|
```json
|
|
35
45
|
{
|
|
36
|
-
"plugin": ["
|
|
46
|
+
"plugin": ["compressed-context"],
|
|
47
|
+
"compaction": { "auto": false }
|
|
37
48
|
}
|
|
38
49
|
```
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
插件会接管上下文压缩,建议关掉 OpenCode 原生自动压缩,避免两套策略互相干扰:
|
|
51
|
+
**本地开发调试**:如果你拉取了源码自行修改,直接填入本地仓库的绝对路径即可:
|
|
43
52
|
|
|
44
53
|
```json
|
|
45
54
|
{
|
|
46
|
-
"plugin": ["compressed-context"],
|
|
55
|
+
"plugin": ["/absolute/path/to/compressed-context"],
|
|
47
56
|
"compaction": { "auto": false }
|
|
48
57
|
}
|
|
49
58
|
```
|
|
50
59
|
|
|
51
60
|
## 配置项
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
所有配置均可直接在 TUI 的插件设置面板中修改,或通过配置文件写入:
|
|
54
63
|
|
|
55
|
-
|
|
|
56
|
-
|
|
57
|
-
| `contextLimit`
|
|
58
|
-
| `triggerPercent`
|
|
59
|
-
| `autoCompress`
|
|
60
|
-
| `summaryBlockPercent`
|
|
61
|
-
| `summaryTotalPercent`
|
|
62
|
-
| `summaryFormat`
|
|
63
|
-
| `summaryLanguage`
|
|
64
|
-
| `nudgeGrowthTokens`
|
|
65
|
-
| `enableCompressTool`
|
|
66
|
-
| `enableDecompressTool` |
|
|
64
|
+
| 配置项 | 默认值 | 说明 |
|
|
65
|
+
| ---------------------- | ------------------ | -------------------------------------------------------------- |
|
|
66
|
+
| `contextLimit` | `"auto"` | 上下文窗口上限。填具体数字时,实际取该值与模型原生窗口的较小值 |
|
|
67
|
+
| `triggerPercent` | `80` | 上下文占用达到百分之多少时触发自动压缩 |
|
|
68
|
+
| `autoCompress` | `true` | 是否开启自动压缩 |
|
|
69
|
+
| `summaryBlockPercent` | `10` | 单个摘要块的 Token 预算(占窗口的百分比) |
|
|
70
|
+
| `summaryTotalPercent` | `50` | 所有摘要的总 Token 预算占比,超出后触发 T2 / T3 合并 |
|
|
71
|
+
| `summaryFormat` | `"markdown"` | 摘要格式:`markdown` 或 `babel` |
|
|
72
|
+
| `summaryLanguage` | `"auto"` | 摘要语言(`babel` 格式下忽略此项) |
|
|
73
|
+
| `nudgeGrowthTokens` | `50000` | 触发“主动压缩提醒”的 Token 增长阈值,设为 `0` 则关闭 |
|
|
74
|
+
| `enableCompressTool` | `true` | 是否允许模型调用 `compress` 工具 |
|
|
75
|
+
| `enableDecompressTool` | `true` | 是否允许模型调用 `decompress` 工具 |
|
|
67
76
|
|
|
68
|
-
##
|
|
77
|
+
## 本地开发
|
|
69
78
|
|
|
70
79
|
```bash
|
|
71
80
|
pnpm install
|
|
72
|
-
pnpm check
|
|
73
|
-
bun scripts/e2e-nudge.mjs #
|
|
74
|
-
bun scripts/e2e-tool-loop.mjs #
|
|
81
|
+
pnpm check # 运行 typecheck + lint + 单测 + TUI 测试
|
|
82
|
+
bun scripts/e2e-nudge.mjs # E2E 测试:主动提醒(隔离环境 + Mock Provider)
|
|
83
|
+
bun scripts/e2e-tool-loop.mjs # E2E 测试:工具循环内的自动压缩
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
##
|
|
86
|
+
## 效果参考
|
|
78
87
|
|
|
79
|
-
在 Terminal-Bench 的 `schemelike-metacircular-eval`
|
|
80
|
-
压在 93,560 并让该次运行通过验证。**只跑了一次,波动很大,只能当参考**,详见
|
|
81
|
-
[docs/benchmark.md](docs/benchmark.md)。
|
|
88
|
+
在 Terminal-Bench 的 `schemelike-metacircular-eval` 任务上做过初步实测:在 100k 窗口限制下,插件成功将单步上下文峰值压在 93,560 tokens 并顺利通过验证。由于单次测试存在随机波动,数据仅供参考,详细记录见 [docs/benchmark.md](docs/benchmark.md)。
|
|
82
89
|
|
|
83
|
-
##
|
|
90
|
+
## 致谢与灵感
|
|
84
91
|
|
|
85
|
-
[
|
|
92
|
+
- [billion-context](https://github.com/ranxianglei/billion-context)
|
|
93
|
+
- [Large Language Models Do Not Always Need Readable Language](https://arxiv.org/abs/2606.19857) —— Babel 摘要格式(BabelTele)的灵感来源
|
|
86
94
|
|
|
87
|
-
##
|
|
95
|
+
## 协议
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
[MIT](LICENSE) © 1cyberlangke1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compressed-context",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OpenCode 上下文管理插件:按需压缩、自包含摘要",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^10.0.1",
|
|
49
|
-
"@opencode/plugin": "^2.
|
|
50
|
-
"@opencode/theme": "2.
|
|
51
|
-
"@opentui/core": "0.
|
|
52
|
-
"@opentui/solid": "0.
|
|
49
|
+
"@opencode/plugin": "^2.1.2",
|
|
50
|
+
"@opencode/theme": "2.1.2",
|
|
51
|
+
"@opentui/core": "0.6.6",
|
|
52
|
+
"@opentui/solid": "0.6.6",
|
|
53
53
|
"@types/node": "^26.6.5",
|
|
54
54
|
"eslint": "^10.13.0",
|
|
55
55
|
"solid-js": "1.9.15",
|
package/src/compression/auto.ts
CHANGED
|
@@ -357,15 +357,29 @@ export async function maybeNudge(input: NudgeInput): Promise<void> {
|
|
|
357
357
|
if (eligible.length === 0 || sourceChars < MIN_SOURCE_CHARS) return
|
|
358
358
|
|
|
359
359
|
const hasCompressed = state.blocks.length > 0
|
|
360
|
+
const notice = buildNudgeText(growth, hasCompressed, settings.autoCompress)
|
|
360
361
|
input.messages.push({
|
|
361
362
|
role: "user",
|
|
362
|
-
content: [{ type: "text", text:
|
|
363
|
+
content: [{ type: "text", text: notice }],
|
|
363
364
|
})
|
|
364
365
|
await saveState(input.ctx.storage, input.sessionID, {
|
|
365
366
|
...state,
|
|
366
367
|
nudgeBaselineTokens: total,
|
|
367
368
|
nudgeCount: (state.nudgeCount ?? 0) + 1,
|
|
368
369
|
})
|
|
370
|
+
// 让 TUI 的消息流也能看到这次提醒;内容就是发给模型的那一条,不另写文案。
|
|
371
|
+
// steer 让它在当前 step 结束后投递,resume:false 不会额外唤醒新一轮。
|
|
372
|
+
try {
|
|
373
|
+
await input.ctx.session.synthetic({
|
|
374
|
+
sessionID: input.sessionID,
|
|
375
|
+
text: notice,
|
|
376
|
+
description: notice.slice(0, 120),
|
|
377
|
+
delivery: "steer",
|
|
378
|
+
resume: false,
|
|
379
|
+
})
|
|
380
|
+
} catch {
|
|
381
|
+
// best effort
|
|
382
|
+
}
|
|
369
383
|
} catch {
|
|
370
384
|
// 提醒是 best effort,不能影响模型请求。
|
|
371
385
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Schema } from "effect"
|
|
2
2
|
|
|
3
|
+
import type { SummaryFormat } from "../shared/settings.ts"
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* 摘要的规范 JSON 形状:写入存储、渲染、校验都用这一份,所有字段必填。
|
|
5
7
|
*
|
|
@@ -82,3 +84,48 @@ export const SummaryInput = Schema.Struct({
|
|
|
82
84
|
})
|
|
83
85
|
|
|
84
86
|
export type SummaryInput = Schema.Schema.Type<typeof SummaryInput>
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* JSON 模式的模型输入:goal 必填,其余可选。
|
|
90
|
+
*
|
|
91
|
+
* 与 SummaryInput 的差别只有 goal:JSON 模式没有 goal 就没有内容可写。
|
|
92
|
+
*/
|
|
93
|
+
export const JsonSummaryInput = Schema.Struct({
|
|
94
|
+
goal: Schema.String,
|
|
95
|
+
title: Schema.optional(Schema.String),
|
|
96
|
+
constraints: Schema.optional(Schema.Array(Schema.String)),
|
|
97
|
+
decisions: Schema.optional(Schema.Array(Schema.String)),
|
|
98
|
+
progress: Schema.optional(
|
|
99
|
+
Schema.Struct({
|
|
100
|
+
completed: Schema.optional(Schema.Array(Schema.String)),
|
|
101
|
+
active: Schema.optional(Schema.Array(Schema.String)),
|
|
102
|
+
blocked: Schema.optional(Schema.Array(Schema.String)),
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
open_items: Schema.optional(Schema.Array(Schema.String)),
|
|
106
|
+
files: Schema.optional(Schema.Array(Schema.String)),
|
|
107
|
+
facts: Schema.optional(Schema.Array(Schema.String)),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** Markdown 模式的模型输入:正文必填;title 可选,传了就省掉从首行提取。 */
|
|
111
|
+
export const MarkdownSummaryInput = Schema.Struct({
|
|
112
|
+
markdown: Schema.String,
|
|
113
|
+
title: Schema.optional(Schema.String),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/** Babel 模式的模型输入:正文必填;title 可选,目标从首行 `G=` 解析。 */
|
|
117
|
+
export const BabelSummaryInput = Schema.Struct({
|
|
118
|
+
babel: Schema.String,
|
|
119
|
+
title: Schema.optional(Schema.String),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 输入:摘要格式。
|
|
124
|
+
* 输出:该格式的模型输入 schema,只保留这个格式用得上的字段。
|
|
125
|
+
* 预期:切换格式后工具参数真的收窄,模型不会按旧习惯传别的字段。
|
|
126
|
+
*/
|
|
127
|
+
export function summaryInputFor(format: SummaryFormat) {
|
|
128
|
+
if (format === "markdown") return MarkdownSummaryInput
|
|
129
|
+
if (format === "babel") return BabelSummaryInput
|
|
130
|
+
return JsonSummaryInput
|
|
131
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadServerSettings,
|
|
12
12
|
parseServerSettings,
|
|
13
13
|
saveServerSettings,
|
|
14
|
+
type ServerSettings,
|
|
14
15
|
windowKey,
|
|
15
16
|
} from "./settings.ts"
|
|
16
17
|
|
|
@@ -19,13 +20,23 @@ export interface Disposable {
|
|
|
19
20
|
dispose(): Promise<void>
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** 设置 RPC 的可选回调。 */
|
|
24
|
+
export interface SettingsRpcOptions {
|
|
25
|
+
/** 输入:刚保存的设置;输出:无;预期:让 compress 工具的描述与参数跟着摘要格式切换。 */
|
|
26
|
+
readonly onSaved?: (settings: ServerSettings) => Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
/**
|
|
23
|
-
*
|
|
30
|
+
* 输入:服务端插件上下文 + 可选保存回调。
|
|
24
31
|
* 输出:注册 settingsGet / settingsSet / blocksGet / cacheHistoryGet,并返回注销句柄。
|
|
25
32
|
* 预期:TUI 推来的设置存进 ctx.storage;非法值返回 ok:false;读取时带上最近解析到的原生窗口;
|
|
26
|
-
* blocksGet 只返回 active 链,cacheHistoryGet
|
|
33
|
+
* blocksGet 只返回 active 链,cacheHistoryGet 读取前先补写当前上下文;
|
|
34
|
+
* 保存成功后调用 onSaved,让依赖设置的运行时(工具描述)立刻更新。
|
|
27
35
|
*/
|
|
28
|
-
export async function registerSettingsRpc(
|
|
36
|
+
export async function registerSettingsRpc(
|
|
37
|
+
ctx: Plugin.Context,
|
|
38
|
+
options?: SettingsRpcOptions,
|
|
39
|
+
): Promise<Disposable> {
|
|
29
40
|
return ctx.rpc.register(ContextRpc, {
|
|
30
41
|
settingsGet: async (input) => {
|
|
31
42
|
const settings = await loadServerSettings(ctx.storage)
|
|
@@ -73,6 +84,7 @@ export async function registerSettingsRpc(ctx: Plugin.Context): Promise<Disposab
|
|
|
73
84
|
const parsed = parseServerSettings({ ...current, ...(input as Record<string, unknown>) })
|
|
74
85
|
if (!parsed) return { ok: false }
|
|
75
86
|
await saveServerSettings(ctx.storage, parsed)
|
|
87
|
+
if (options?.onSaved !== undefined) await options.onSaved(parsed)
|
|
76
88
|
return { ok: true }
|
|
77
89
|
},
|
|
78
90
|
blocksGet: async (input) => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SummaryFormat } from "../shared/settings.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 本模块只放「摘要长什么样」的文案。
|
|
5
|
+
*
|
|
6
|
+
* 压缩提示词(summarize.ts)和 compress 工具描述(tool.ts)都从这里取,
|
|
7
|
+
* 避免两处各写一份、改一处忘另一处。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const JSON_SHAPE = `{
|
|
11
|
+
"title": "optional short label for this block",
|
|
12
|
+
"goal": "required: the objective and any pivots it went through",
|
|
13
|
+
"constraints": ["optional: hard limits and user preferences"],
|
|
14
|
+
"decisions": ["optional: what was decided — why"],
|
|
15
|
+
"progress": {
|
|
16
|
+
"completed": ["optional: work actually finished"],
|
|
17
|
+
"active": ["optional: work in progress, including what the user still expects"],
|
|
18
|
+
"blocked": ["optional: what is blocked — why"]
|
|
19
|
+
},
|
|
20
|
+
"open_items": ["optional: unanswered questions, TODOs, next actions"],
|
|
21
|
+
"files": ["optional: real file paths only, full/path.ext:line — role in the work"],
|
|
22
|
+
"facts": ["optional: exact errors, signatures, versions, config keys, thresholds, measured values"]
|
|
23
|
+
}`
|
|
24
|
+
|
|
25
|
+
const MARKDOWN_SHAPE = `# Short title
|
|
26
|
+
## Goal
|
|
27
|
+
...
|
|
28
|
+
## Constraints
|
|
29
|
+
- ...
|
|
30
|
+
## Decisions
|
|
31
|
+
- ...
|
|
32
|
+
## Progress
|
|
33
|
+
### Completed
|
|
34
|
+
- ...
|
|
35
|
+
### Active
|
|
36
|
+
- ...
|
|
37
|
+
### Blocked
|
|
38
|
+
- ...
|
|
39
|
+
## Open Items
|
|
40
|
+
- ...
|
|
41
|
+
## Files
|
|
42
|
+
- ...
|
|
43
|
+
## Facts
|
|
44
|
+
- ...`
|
|
45
|
+
|
|
46
|
+
/** 输入:格式 + 是否要 candidate 行;输出:格式说明行。 */
|
|
47
|
+
export function shapeLines(format: SummaryFormat | undefined, withCandidate: boolean): string[] {
|
|
48
|
+
if (format === "babel") {
|
|
49
|
+
return [
|
|
50
|
+
withCandidate
|
|
51
|
+
? "Return ONLY Babel text. First line must be `c=N`, where N is the chosen candidate; second line must be `T=<short title>|G=<one-line goal>`."
|
|
52
|
+
: "Return ONLY Babel text. First line must be `T=<short title>|G=<one-line goal>`.",
|
|
53
|
+
"After the header, use adaptive Babel: only T/G is fixed; invent the shortest anchors/delimiters for this source. No fixed key schema, no Markdown headings, no JSON, no code fences, no commentary.",
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
if (format === "markdown") {
|
|
57
|
+
return [
|
|
58
|
+
withCandidate
|
|
59
|
+
? "Return ONLY markdown. First line must be `candidate: N`, then a self-contained markdown checkpoint."
|
|
60
|
+
: "Return ONLY markdown. Do not return JSON. Do not wrap in code fences.",
|
|
61
|
+
MARKDOWN_SHAPE,
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
return [
|
|
65
|
+
withCandidate
|
|
66
|
+
? "Return ONLY a JSON object with the normal summary shape PLUS a top-level `candidate` number:"
|
|
67
|
+
: "Return ONLY a JSON object with this exact shape (no markdown fences, no commentary):",
|
|
68
|
+
JSON_SHAPE,
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 输入:摘要格式;输出:该格式的写法说明,供压缩提示词和工具描述共用。 */
|
|
73
|
+
export function summaryShape(format: SummaryFormat): string {
|
|
74
|
+
return shapeLines(format, false).join("\n")
|
|
75
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { SummaryFormat, SummaryLanguage } from "../shared/settings.ts"
|
|
|
4
4
|
import { splitBabelCandidate, toBabelSummary } from "./babel.ts"
|
|
5
5
|
import { toSummary } from "./normalize.ts"
|
|
6
6
|
import type { Summary } from "./schema.ts"
|
|
7
|
+
import { shapeLines } from "./shape.ts"
|
|
7
8
|
import type { Tier } from "./state.ts"
|
|
8
9
|
import { MAX_SUMMARY_CHARS, MIN_SUMMARY_CHARS, minSummaryChars, validateSummary } from "./validate.ts"
|
|
9
10
|
|
|
@@ -31,12 +32,11 @@ function promptRules(options: SummaryPromptOptions | undefined): string[] {
|
|
|
31
32
|
const lines =
|
|
32
33
|
options?.format === "babel"
|
|
33
34
|
? [
|
|
34
|
-
"-
|
|
35
|
-
"- Only `T=<short title>|G=<one-line goal>`
|
|
36
|
-
"-
|
|
37
|
-
"-
|
|
38
|
-
"-
|
|
39
|
-
"- Target 400-800 tokens for T1; drop explanations before exact facts.",
|
|
35
|
+
"- Audience=peer LLM. Notation only, not content: drop grammar/connectives, mix languages/emoji/math(forall、exists、in、=>).",
|
|
36
|
+
"- Only `T=<short title>|G=<one-line goal>` fixed; invent anchors and reuse them; no headings/paragraphs/sentences/JSON/fences.",
|
|
37
|
+
"- Anchors: `S[module]` `D[decision]:why` `F[path:line]` `C[step>step]` `Config[target]:k=v` `?cond=>action` `!edge:detail` `Eval[A/B]:result`; flatten parallel items `[A,B,C]`.",
|
|
38
|
+
"- Files `F:[...]` complete, never \"various files\"; commands `C:[...]` verbatim, never paraphrase.",
|
|
39
|
+
"- Lossless: entity graph, exact numbers (no rounding), versions, config keys, thresholds, formulas, conditions, causality, exceptions, comparisons, placeholders, paths, signatures, error text; unknown=?/NULL, never invent.",
|
|
40
40
|
`- Keep this summary under ${maxChars.toString()} characters.`,
|
|
41
41
|
]
|
|
42
42
|
: options?.format === "markdown"
|
|
@@ -99,74 +99,26 @@ export function sessionGenerator(session: SessionGenerateLike, sessionID: string
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const SHAPE = `{
|
|
103
|
-
"title": "optional short label for this block",
|
|
104
|
-
"goal": "required: the objective and any pivots it went through",
|
|
105
|
-
"constraints": ["optional: hard limits and user preferences"],
|
|
106
|
-
"decisions": ["optional: what was decided — why"],
|
|
107
|
-
"progress": {
|
|
108
|
-
"completed": ["optional: work actually finished"],
|
|
109
|
-
"active": ["optional: work in progress, including what the user still expects"],
|
|
110
|
-
"blocked": ["optional: what is blocked — why"]
|
|
111
|
-
},
|
|
112
|
-
"open_items": ["optional: unanswered questions, TODOs, next actions"],
|
|
113
|
-
"files": ["optional: real file paths only, full/path.ext:line — role in the work"],
|
|
114
|
-
"facts": ["optional: exact errors, signatures, versions, config keys, thresholds, measured values"]
|
|
115
|
-
}`
|
|
116
|
-
|
|
117
|
-
const MARKDOWN_SHAPE = `# Short title
|
|
118
|
-
## Goal
|
|
119
|
-
...
|
|
120
|
-
## Constraints
|
|
121
|
-
- ...
|
|
122
|
-
## Decisions
|
|
123
|
-
- ...
|
|
124
|
-
## Progress
|
|
125
|
-
### Completed
|
|
126
|
-
- ...
|
|
127
|
-
### Active
|
|
128
|
-
- ...
|
|
129
|
-
### Blocked
|
|
130
|
-
- ...
|
|
131
|
-
## Open Items
|
|
132
|
-
- ...
|
|
133
|
-
## Files
|
|
134
|
-
- ...
|
|
135
|
-
## Facts
|
|
136
|
-
- ...`
|
|
137
|
-
|
|
138
|
-
/** 输入:格式 + 是否要 candidate 行;输出:格式说明行。 */
|
|
139
|
-
function shapeLines(format: SummaryFormat | undefined, withCandidate: boolean): string[] {
|
|
140
|
-
if (format === "babel") {
|
|
141
|
-
return [
|
|
142
|
-
withCandidate
|
|
143
|
-
? "Return ONLY Babel text. First line must be `c=N`, where N is the chosen candidate; second line must be `T=<short title>|G=<one-line goal>`."
|
|
144
|
-
: "Return ONLY Babel text. First line must be `T=<short title>|G=<one-line goal>`.",
|
|
145
|
-
"After the header, use adaptive Babel: only T/G is fixed; invent the shortest anchors/delimiters for this source. No fixed key schema, no Markdown headings, no JSON, no code fences, no commentary.",
|
|
146
|
-
]
|
|
147
|
-
}
|
|
148
|
-
if (format === "markdown") {
|
|
149
|
-
return [
|
|
150
|
-
withCandidate
|
|
151
|
-
? "Return ONLY markdown. First line must be `candidate: N`, then a self-contained markdown checkpoint."
|
|
152
|
-
: "Return ONLY markdown. Do not return JSON. Do not wrap in code fences.",
|
|
153
|
-
MARKDOWN_SHAPE,
|
|
154
|
-
]
|
|
155
|
-
}
|
|
156
|
-
return [
|
|
157
|
-
withCandidate
|
|
158
|
-
? "Return ONLY a JSON object with the normal summary shape PLUS a top-level `candidate` number:"
|
|
159
|
-
: "Return ONLY a JSON object with this exact shape (no markdown fences, no commentary):",
|
|
160
|
-
SHAPE,
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
|
|
164
102
|
const BABEL_T1_RULES = `Guidance:
|
|
165
|
-
- This checkpoint replaces the messages above;
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
103
|
+
- This checkpoint replaces the messages above; the next reader sees it instead of them, so it must stand on its own. Describe what already happened as history, not as instructions to act now.
|
|
104
|
+
- Keep the objective and hard constraints, decisions with their reasons, outcomes, open items, work the user still expects, and failure lessons.
|
|
105
|
+
- Copy exact identifiers instead of describing them: full paths with line numbers, the line of code that carries the logic, signatures, error text, versions, config keys, thresholds, measured values.
|
|
106
|
+
- Omit build/test logs, repeated reads, exploration that produced nothing, abandoned attempts (one line on why they failed), repeated status checks, and settled discussion.
|
|
107
|
+
- If space is tight, keep in this order: objective and constraints; decisions and reasons; exact technical facts; conclusions; lessons from failures.`
|
|
108
|
+
|
|
109
|
+
const BABEL_T2_RULES = `Guidance for merging existing checkpoints:
|
|
110
|
+
- The sources are already compressed; merge them into one checkpoint covering all of them, without re-telling the process.
|
|
111
|
+
- Keep the latest objective and hard constraints, decisions with their reasons, final outcomes, open items, blocked work, and the user's still-open expectations.
|
|
112
|
+
- Merge duplicate facts and keep the latest status; mark superseded content with one short marker and drop its details.
|
|
113
|
+
- Drop process, logs, full diffs, code listings, and non-load-bearing line numbers. A failed attempt may stay as one line on why it failed.
|
|
114
|
+
- Target roughly 50-150 tokens per source block.`
|
|
115
|
+
|
|
116
|
+
const BABEL_T3_RULES = `Guidance for ultra-condensing existing checkpoints:
|
|
117
|
+
- Reduce the sources to a lookup index: one to three single-line facts per source block, subject plus outcome.
|
|
118
|
+
- Keep only shipped outcomes, open work, architectural decisions, and critical constraints.
|
|
119
|
+
- Drop explanations, rationale details, process, logs, and anything already obsolete or superseded.
|
|
120
|
+
- Open user-requested objectives survive until a later source marks them completed or superseded.
|
|
121
|
+
- Target roughly 30-60 tokens per source block.`
|
|
170
122
|
|
|
171
123
|
const TIER1_RULES = `Guidance:
|
|
172
124
|
- This checkpoint replaces the messages above; the next reader sees it instead of them, so it must stand on its own.
|
|
@@ -194,7 +146,7 @@ const TIER3_RULES = `Guidance for ultra-condensing existing checkpoints:
|
|
|
194
146
|
- Target roughly 30-60 tokens per source block.`
|
|
195
147
|
|
|
196
148
|
function rulesFor(tier: Tier, format?: SummaryFormat): string {
|
|
197
|
-
if (format === "babel") return tier === 1 ? BABEL_T1_RULES : tier === 2 ?
|
|
149
|
+
if (format === "babel") return tier === 1 ? BABEL_T1_RULES : tier === 2 ? BABEL_T2_RULES : BABEL_T3_RULES
|
|
198
150
|
if (tier === 2) return TIER2_RULES
|
|
199
151
|
if (tier === 3) return TIER3_RULES
|
|
200
152
|
return TIER1_RULES
|
|
@@ -291,13 +243,12 @@ const MARKDOWN_CHOICE_RULES = `Guidance for choosing and merging:
|
|
|
291
243
|
|
|
292
244
|
const BABEL_CHOICE_RULES = `Guidance for choosing and merging:
|
|
293
245
|
- Exactly one adjacent pair may be merged. Pick one candidate from the list below and merge ONLY that pair; every other block is context for the decision, not part of the merge.
|
|
294
|
-
- First line must be \`c=N\`, where N is the chosen candidate number.
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
- Target 150-300 tokens for T2 and 60-120 tokens for T3; drop explanations before exact facts.
|
|
298
|
-
- Keep the latest objective and hard constraints, decisions with reasons, final outcomes, open items, blocked work, and exact facts that still matter.
|
|
246
|
+
- First line must be \`c=N\`, where N is the chosen candidate number. Second line must be \`T=<short title>|G=<one-line goal>\`; after that write adaptive Babel with anchors you invent and reuse.
|
|
247
|
+
- Babel changes only notation, not content: same policy as a normal merge checkpoint. No Markdown headings, no JSON, no prose/sentences, no code fences.
|
|
248
|
+
- Keep the latest objective and hard constraints, decisions with their reasons, final outcomes, open items, blocked work, and exact facts that still matter.
|
|
299
249
|
- Merge duplicate facts across the chosen pair, keep the latest status, and drop process, logs, repeated reads, full diffs, and non-load-bearing line numbers.
|
|
300
|
-
-
|
|
250
|
+
- If the chosen candidate's output tier is 2, target roughly 50-150 tokens per source block. If it is tier 3, be ultra-condensed: one to three single-line facts per source block.
|
|
251
|
+
- Do not invent facts.`
|
|
301
252
|
|
|
302
253
|
function buildChoicePrompt(
|
|
303
254
|
context: string,
|
package/src/compression/tool.ts
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin"
|
|
2
2
|
import { Schema } from "effect"
|
|
3
3
|
|
|
4
|
-
import { resolveSummaryLimits } from "../shared/settings.ts"
|
|
4
|
+
import { resolveSummaryLimits, type SummaryFormat } from "../shared/settings.ts"
|
|
5
5
|
import { createT1Block } from "./block.ts"
|
|
6
6
|
import { toSummary } from "./normalize.ts"
|
|
7
|
-
import {
|
|
7
|
+
import { summaryInputFor } from "./schema.ts"
|
|
8
8
|
import { RESOLVED_WINDOW_KEY, loadServerSettings, windowKey } from "./settings.ts"
|
|
9
9
|
import { eligibleMessages, messageRange, type MessageLike } from "./select.ts"
|
|
10
10
|
import { sessionMessagesToMessageLikes } from "./session-message.ts"
|
|
11
|
+
import { summaryShape } from "./shape.ts"
|
|
11
12
|
import { loadState, saveState, withPending } from "./state.ts"
|
|
12
13
|
import type { Disposable } from "./settings-rpc.ts"
|
|
13
14
|
import { buildTranscript } from "./transcript.ts"
|
|
14
15
|
import { saveTranscript } from "./transcript-store.ts"
|
|
15
16
|
import { MIN_SOURCE_CHARS, validateSummary } from "./validate.ts"
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const COMMON =
|
|
18
19
|
"Compress the conversation so far into a structured checkpoint. Call this when the context is getting full. " +
|
|
19
|
-
"It covers every message since the last compressed block, up to the current user message, and replaces them with " +
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"pending. " +
|
|
24
|
-
"Add a short title if useful. " +
|
|
20
|
+
"It covers every message since the last compressed block, up to the current user message, and replaces them with one block. " +
|
|
21
|
+
"Do not call while a compression is already pending. Add a short title if useful."
|
|
22
|
+
|
|
23
|
+
const CONTENT =
|
|
25
24
|
"The block is the only remaining record of those messages, so copy exact paths with line numbers, signatures, the " +
|
|
26
25
|
"code lines that carry the logic, error text, versions, config keys, thresholds, and measured values. " +
|
|
27
|
-
"Use `files` only for real file paths; put config keys and function names in `facts`. " +
|
|
28
|
-
"Put work in `progress.completed` only when it was actually finished; plans go in `progress.active` or `open_items`. " +
|
|
29
26
|
"Give the reason for each decision, keep the objective and any pivots, why important actions were taken, unanswered " +
|
|
30
27
|
"questions, and work the user still expects. Describe the past as past, not as a new instruction. " +
|
|
31
28
|
"Leave out logs, repeated reads, exploration that produced nothing, abandoned attempts (one line on why), and " +
|
|
32
29
|
"repeated status checks. If space is tight, keep objective and constraints first, then decisions and reasons, then " +
|
|
33
30
|
"exact technical facts, then conclusions, then lessons. Write short factual entries, not prose."
|
|
34
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 输入:摘要格式。
|
|
34
|
+
* 输出:该格式的字段使用规则。
|
|
35
|
+
* 预期:只讲这个格式用得上的字段,不把三种格式混在一起讲。
|
|
36
|
+
*/
|
|
37
|
+
function fieldRules(format: SummaryFormat): string {
|
|
38
|
+
if (format === "babel") {
|
|
39
|
+
return (
|
|
40
|
+
"Only `T=<short title>|G=<one-line goal>` is fixed; the body is an adaptive latent index built from anchors you " +
|
|
41
|
+
"invent for this source. Keep shipped work and still-open work in that body, and never invent facts."
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
if (format === "markdown") {
|
|
45
|
+
return (
|
|
46
|
+
"Only include sections with real content; never invent entries to fill one. " +
|
|
47
|
+
"Use real file paths in `## Files`; put config keys and function names in `## Facts`. " +
|
|
48
|
+
"Put work in `### Completed` only when it was actually finished; plans go in `### Active` or `## Open Items`."
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
"Omit any optional field that has no real content instead of inventing entries. " +
|
|
53
|
+
"Use `files` only for real file paths; put config keys and function names in `facts`. " +
|
|
54
|
+
"Put work in `progress.completed` only when it was actually finished; plans go in `progress.active` or `open_items`."
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 输入:摘要格式;输出:compress 工具描述;写法部分与压缩提示词共用同一份文案。 */
|
|
59
|
+
export function descriptionFor(format: SummaryFormat): string {
|
|
60
|
+
return [COMMON, summaryShape(format), fieldRules(format), CONTENT].join("\n\n")
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
function failure(message: string) {
|
|
36
64
|
return {
|
|
37
65
|
output: { block: "", status: "error" },
|
|
@@ -40,21 +68,29 @@ function failure(message: string) {
|
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
|
|
71
|
+
/** compress 工具的注册句柄:注销 + 切换摘要格式。 */
|
|
72
|
+
export interface CompressToolHandle extends Disposable {
|
|
73
|
+
/** 输入:新的摘要格式;输出:无;预期:更新工具描述与参数,并让宿主重算工具列表。 */
|
|
74
|
+
setFormat(format: SummaryFormat): Promise<void>
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
/**
|
|
44
78
|
* 输入:服务端上下文。
|
|
45
|
-
* 输出:注册 compress
|
|
46
|
-
*
|
|
79
|
+
* 输出:注册 compress 工具,并返回可切换格式的句柄。
|
|
80
|
+
* 预期:执行时从会话上下文按需取消息,不再把整段历史常驻内存;
|
|
81
|
+
* 描述与参数按当前摘要格式生成,切换后由宿主重新计算工具列表。
|
|
47
82
|
*/
|
|
48
83
|
export async function registerCompressTool(
|
|
49
84
|
ctx: Plugin.Context,
|
|
50
85
|
legacyCache?: ReadonlyMap<string, readonly MessageLike[]>,
|
|
51
|
-
): Promise<
|
|
86
|
+
): Promise<CompressToolHandle> {
|
|
87
|
+
const initial = await loadServerSettings(ctx.storage)
|
|
88
|
+
let format: SummaryFormat = initial.summaryFormat
|
|
52
89
|
const registration = await ctx.tool.transform((tools) => {
|
|
53
90
|
tools.add({
|
|
54
91
|
name: "compress",
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
input: SummaryInput,
|
|
92
|
+
description: descriptionFor(format),
|
|
93
|
+
input: summaryInputFor(format),
|
|
58
94
|
output: Schema.Struct({ block: Schema.String, status: Schema.String }),
|
|
59
95
|
execute: async (input, toolCtx) => {
|
|
60
96
|
const sessionID = toolCtx.sessionID
|
|
@@ -138,5 +174,11 @@ export async function registerCompressTool(
|
|
|
138
174
|
},
|
|
139
175
|
})
|
|
140
176
|
})
|
|
141
|
-
return
|
|
177
|
+
return {
|
|
178
|
+
dispose: () => registration.dispose(),
|
|
179
|
+
setFormat: async (next) => {
|
|
180
|
+
format = next
|
|
181
|
+
await ctx.tool.reload()
|
|
182
|
+
},
|
|
183
|
+
}
|
|
142
184
|
}
|
package/src/i18n/en.ts
CHANGED
|
@@ -55,9 +55,9 @@ export const en: Messages = {
|
|
|
55
55
|
windowUnknown: "Current window unknown (run one model request first)",
|
|
56
56
|
summaryLanguage: "Summary language",
|
|
57
57
|
summaryFormat: "Summary format",
|
|
58
|
-
summaryFormatJson: "JSON
|
|
59
|
-
summaryFormatMarkdown: "Markdown
|
|
60
|
-
summaryFormatBabel: "Babel
|
|
58
|
+
summaryFormatJson: "JSON",
|
|
59
|
+
summaryFormatMarkdown: "Markdown",
|
|
60
|
+
summaryFormatBabel: "Babel",
|
|
61
61
|
autoCompress: "Auto compression",
|
|
62
62
|
enableCompressTool: "compress tool",
|
|
63
63
|
enableDecompressTool: "decompress tool",
|
package/src/i18n/zh.ts
CHANGED
|
@@ -55,9 +55,9 @@ export const zh: Messages = {
|
|
|
55
55
|
windowUnknown: "当前窗口未知(先跑一次模型请求)",
|
|
56
56
|
summaryLanguage: "摘要语言",
|
|
57
57
|
summaryFormat: "摘要格式",
|
|
58
|
-
summaryFormatJson: "JSON
|
|
59
|
-
summaryFormatMarkdown: "Markdown
|
|
60
|
-
summaryFormatBabel: "Babel
|
|
58
|
+
summaryFormatJson: "JSON",
|
|
59
|
+
summaryFormatMarkdown: "Markdown",
|
|
60
|
+
summaryFormatBabel: "Babel",
|
|
61
61
|
autoCompress: "自动压缩",
|
|
62
62
|
enableCompressTool: "compress 工具",
|
|
63
63
|
enableDecompressTool: "decompress 工具",
|
package/src/index.ts
CHANGED
|
@@ -37,9 +37,15 @@ export default Plugin.define({
|
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
const compressTool = await registerCompressTool(ctx)
|
|
41
|
+
disposables.push(compressTool)
|
|
41
42
|
disposables.push(await registerDecompressTool(ctx))
|
|
42
|
-
|
|
43
|
+
// 摘要格式存在设置里;保存后让 compress 工具的描述与参数立刻跟着换。
|
|
44
|
+
disposables.push(
|
|
45
|
+
await registerSettingsRpc(ctx, {
|
|
46
|
+
onSaved: (settings) => compressTool.setFormat(settings.summaryFormat),
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
43
49
|
const stopEvents = startSessionEvents(ctx, {
|
|
44
50
|
onSessionIdle: forgetMemory,
|
|
45
51
|
onSessionDeleted: forgetMemory,
|
|
@@ -49,6 +55,8 @@ export default Plugin.define({
|
|
|
49
55
|
await maybeAutoCompressStep({ ctx, sessionID, tokens })
|
|
50
56
|
})(),
|
|
51
57
|
})
|
|
58
|
+
// 工具注册后刷新一次工具表,让已存在的会话在下一次请求也能看到 compress。
|
|
59
|
+
await ctx.tool.reload()
|
|
52
60
|
void reconcileAll(ctx)
|
|
53
61
|
|
|
54
62
|
disposables.push(
|
package/src/shared/settings.ts
CHANGED
|
@@ -149,8 +149,6 @@ export interface Settings extends CompressionSettings {
|
|
|
149
149
|
showSummaries: boolean
|
|
150
150
|
/** 摘要区是否展开 */
|
|
151
151
|
summariesExpanded: boolean
|
|
152
|
-
/** 当前展开的摘要块 id;undefined 表示全部收起 */
|
|
153
|
-
expandedBlockId?: string
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
export const DEFAULT_SETTINGS: Settings = {
|
|
@@ -336,9 +334,6 @@ export function normalizeSettings(value: Readonly<Record<string, unknown>>): Set
|
|
|
336
334
|
showSummaries: typeof value["showSummaries"] === "boolean" ? value["showSummaries"] : DEFAULT_SHOW_SUMMARIES,
|
|
337
335
|
summariesExpanded:
|
|
338
336
|
typeof value["summariesExpanded"] === "boolean" ? value["summariesExpanded"] : DEFAULT_SUMMARIES_EXPANDED,
|
|
339
|
-
...(typeof value["expandedBlockId"] === "string" && value["expandedBlockId"].length > 0
|
|
340
|
-
? { expandedBlockId: value["expandedBlockId"] }
|
|
341
|
-
: {}),
|
|
342
337
|
decompressInlineMax: readInt(
|
|
343
338
|
value["decompressInlineMax"],
|
|
344
339
|
MIN_DECOMPRESS_INLINE_MAX,
|
package/src/tui/index.tsx
CHANGED
|
@@ -33,7 +33,6 @@ function migrate(settings: Settings, update: UpdateSettings): void {
|
|
|
33
33
|
cacheLogExpanded: settings.cacheLogExpanded,
|
|
34
34
|
showSummaries: settings.showSummaries,
|
|
35
35
|
summariesExpanded: settings.summariesExpanded,
|
|
36
|
-
...(settings.expandedBlockId === undefined ? {} : { expandedBlockId: settings.expandedBlockId }),
|
|
37
36
|
decompressInlineMax: settings.decompressInlineMax,
|
|
38
37
|
decompressTempCap: settings.decompressTempCap,
|
|
39
38
|
decompressShowTokens: settings.decompressShowTokens,
|
|
@@ -60,7 +59,6 @@ function migrate(settings: Settings, update: UpdateSettings): void {
|
|
|
60
59
|
settings.cacheLogExpanded === normalized.cacheLogExpanded &&
|
|
61
60
|
settings.showSummaries === normalized.showSummaries &&
|
|
62
61
|
settings.summariesExpanded === normalized.summariesExpanded &&
|
|
63
|
-
settings.expandedBlockId === normalized.expandedBlockId &&
|
|
64
62
|
settings.decompressInlineMax === normalized.decompressInlineMax &&
|
|
65
63
|
settings.decompressTempCap === normalized.decompressTempCap &&
|
|
66
64
|
settings.decompressShowTokens === normalized.decompressShowTokens
|
|
@@ -15,10 +15,12 @@ function tierColor(tier: SummaryBlock["tier"], theme: Context["theme"]): RGBA {
|
|
|
15
15
|
return theme.text.feedback.error.base
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** 输入:TUI renderer;输出:共享的 SyntaxStyle,拿不到 resource owner 时返回 undefined。 */
|
|
19
19
|
let sharedSyntaxStyle: SyntaxStyle | undefined
|
|
20
|
-
function getSyntaxStyle(): SyntaxStyle {
|
|
21
|
-
|
|
20
|
+
function getSyntaxStyle(renderer: Context["renderer"]): SyntaxStyle | undefined {
|
|
21
|
+
const owner = (renderer as { readonly nativeScene?: unknown }).nativeScene
|
|
22
|
+
if (owner === undefined) return undefined
|
|
23
|
+
sharedSyntaxStyle ??= SyntaxStyle.create(owner as Parameters<typeof SyntaxStyle.create>[0])
|
|
22
24
|
return sharedSyntaxStyle
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -49,18 +51,27 @@ export function SummaryDialog(props: {
|
|
|
49
51
|
</text>
|
|
50
52
|
</box>
|
|
51
53
|
<scrollbox height={height()} scrollY={true} focused={true} width="100%">
|
|
52
|
-
<Show
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
<Show when={props.block.format !== "babel"}>
|
|
55
|
+
<Show
|
|
56
|
+
when={getSyntaxStyle(props.context.renderer)}
|
|
57
|
+
fallback={
|
|
58
|
+
<text fg={props.theme.text.base} wrapMode="word">
|
|
59
|
+
{props.block.body}
|
|
60
|
+
</text>
|
|
61
|
+
}
|
|
62
|
+
>
|
|
63
|
+
{(style) => (
|
|
64
|
+
<markdown
|
|
65
|
+
content={props.block.body}
|
|
66
|
+
syntaxStyle={style()}
|
|
67
|
+
fg={props.theme.text.base}
|
|
68
|
+
conceal={true}
|
|
69
|
+
tableOptions={{ widthMode: "full", style: "columns" }}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</Show>
|
|
73
|
+
</Show>
|
|
74
|
+
<Show when={props.block.format === "babel"}>
|
|
64
75
|
<text fg={props.theme.text.base} wrapMode="word">
|
|
65
76
|
{props.block.body}
|
|
66
77
|
</text>
|