compressed-context 0.1.0 → 0.1.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.
Files changed (55) hide show
  1. package/package.json +64 -64
  2. package/src/compression/active-summary-cache.ts +50 -50
  3. package/src/compression/adopt.ts +57 -57
  4. package/src/compression/auto.ts +372 -372
  5. package/src/compression/babel.ts +85 -85
  6. package/src/compression/block.ts +25 -25
  7. package/src/compression/cache-history.ts +75 -75
  8. package/src/compression/compaction.ts +189 -189
  9. package/src/compression/decompress-tool.ts +74 -74
  10. package/src/compression/decompress.ts +292 -292
  11. package/src/compression/deleted-sessions.ts +24 -24
  12. package/src/compression/distill.ts +220 -220
  13. package/src/compression/events.ts +275 -275
  14. package/src/compression/markdown.ts +53 -53
  15. package/src/compression/normalize.ts +114 -114
  16. package/src/compression/render.ts +70 -70
  17. package/src/compression/schema.ts +84 -84
  18. package/src/compression/select.ts +123 -123
  19. package/src/compression/session-message.ts +112 -112
  20. package/src/compression/settings-rpc.ts +111 -111
  21. package/src/compression/settings.ts +175 -175
  22. package/src/compression/state.ts +248 -248
  23. package/src/compression/summarize.ts +595 -595
  24. package/src/compression/summary.ts +73 -73
  25. package/src/compression/tool.ts +142 -142
  26. package/src/compression/transcript-store.ts +258 -258
  27. package/src/compression/transcript.ts +63 -63
  28. package/src/compression/validate.ts +81 -81
  29. package/src/i18n/en.ts +93 -93
  30. package/src/i18n/index.ts +28 -28
  31. package/src/i18n/types.ts +89 -89
  32. package/src/i18n/zh.ts +93 -93
  33. package/src/index.ts +108 -108
  34. package/src/shared/blocks.ts +17 -17
  35. package/src/shared/cache.ts +199 -199
  36. package/src/shared/format.ts +23 -23
  37. package/src/shared/media.ts +184 -184
  38. package/src/shared/parse.ts +25 -25
  39. package/src/shared/parts.ts +72 -72
  40. package/src/shared/rpc.ts +145 -145
  41. package/src/shared/scan.ts +32 -32
  42. package/src/shared/settings.ts +471 -471
  43. package/src/shared/stats.ts +142 -142
  44. package/src/shared/tokens.ts +259 -259
  45. package/src/tui/chart/cache-chart.ts +237 -237
  46. package/src/tui/chart/color.ts +58 -58
  47. package/src/tui/index.tsx +111 -111
  48. package/src/tui/panel/cache.tsx +203 -203
  49. package/src/tui/panel/data.ts +105 -105
  50. package/src/tui/panel/index.tsx +176 -176
  51. package/src/tui/panel/summaries.tsx +192 -192
  52. package/src/tui/settings/dialogs.tsx +215 -215
  53. package/src/tui/settings/options.ts +238 -238
  54. package/src/tui/settings.tsx +418 -418
  55. package/src/tui/types.ts +4 -4
@@ -1,105 +1,105 @@
1
- import type { Context } from "@opencode/plugin/tui/context"
2
- import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
3
-
4
- import type { SummaryBlock } from "../../shared/blocks.ts"
5
- import type { CacheSample } from "../../shared/cache.ts"
6
- import { contextRpc } from "../../shared/rpc.ts"
7
- import type { Settings } from "../../shared/settings.ts"
8
- import { parseStats, STATS_KEY, TERMINAL_KEY, type ContextStats } from "../../shared/stats.ts"
9
-
10
- /** Panel 需要的服务端数据;全部是响应式 accessor。 */
11
- export interface PanelData {
12
- readonly stats: Accessor<ContextStats | undefined>
13
- readonly terminal: Accessor<string | undefined>
14
- readonly cache: Accessor<readonly CacheSample[]>
15
- readonly blocks: Accessor<readonly SummaryBlock[]>
16
- }
17
-
18
- /**
19
- * 输入:TUI 上下文 + sessionID + 设置 accessor。
20
- * 输出:统计 / 终止原因 / 缓存历史 / 摘要块。
21
- * 预期:挂载时拉取,usage/compaction/idle 时刷新;设置开关变化时重新拉取。
22
- */
23
- export function usePanelData(input: {
24
- readonly context: Context
25
- readonly sessionID: string
26
- readonly settings: Accessor<Settings>
27
- }): PanelData {
28
- const [stats, setStats] = createSignal<ContextStats>()
29
- const [terminal, setTerminal] = createSignal<string>()
30
- const [cache, setCache] = createSignal<readonly CacheSample[]>([])
31
- const [blocks, setBlocks] = createSignal<readonly SummaryBlock[]>([])
32
- const rpc = () => contextRpc(input.context.client)
33
-
34
- /** 输入:无;输出:无;预期:从服务端拉取该会话持久化的缓存历史。 */
35
- const refreshCache = async (): Promise<void> => {
36
- try {
37
- const got = await rpc().cacheHistoryGet({ sessionID: input.sessionID })
38
- setCache(got.samples)
39
- } catch {
40
- // server plugin not loaded;保留上一次数据
41
- }
42
- }
43
-
44
- /** 输入:无;输出:无;预期:从服务端拉取 active 摘要链。 */
45
- const refreshBlocks = async (): Promise<void> => {
46
- if (!input.settings().showSummaries) return
47
- try {
48
- const got = await rpc().blocksGet({ sessionID: input.sessionID })
49
- setBlocks(got.blocks)
50
- } catch {
51
- // server plugin not loaded;保留上一次数据
52
- }
53
- }
54
-
55
- /** 输入:无;输出:无;预期:从服务端重新拉取 session metadata 里的统计。 */
56
- const refresh = async (): Promise<void> => {
57
- try {
58
- const info = await input.context.client.session.get({ sessionID: input.sessionID })
59
- setStats(parseStats(info.metadata?.[STATS_KEY]))
60
- const rawTerminal = info.metadata?.[TERMINAL_KEY]
61
- setTerminal(typeof rawTerminal === "string" ? rawTerminal : undefined)
62
- } catch {
63
- // 统计缺失不影响主界面,保持上一次的值
64
- }
65
- }
66
-
67
- // 显示开关变化时重新拉取;首次挂载也会跑一次。
68
- createEffect(() => {
69
- if (input.settings().showCacheChart || input.settings().showCacheLog) void refreshCache()
70
- })
71
- createEffect(() => {
72
- if (input.settings().showSummaries) void refreshBlocks()
73
- })
74
-
75
- createEffect(() => {
76
- void refresh()
77
- })
78
-
79
- onCleanup(
80
- input.context.data.on("session.usage.updated", (event) => {
81
- if (event.data.sessionID !== input.sessionID) return
82
- void refresh()
83
- // 缓存历史由服务端在 usage.updated 时补写;这里同步刷新面板。
84
- if (input.settings().showCacheChart || input.settings().showCacheLog) void refreshCache()
85
- }),
86
- )
87
- onCleanup(
88
- input.context.data.on("session.compaction.ended", (event) => {
89
- if (event.data.sessionID !== input.sessionID) return
90
- void refresh()
91
- void refreshCache()
92
- void refreshBlocks()
93
- }),
94
- )
95
- onCleanup(
96
- input.context.data.on("session.idle", (event) => {
97
- if (event.data.sessionID !== input.sessionID) return
98
- void refresh()
99
- void refreshCache()
100
- void refreshBlocks()
101
- }),
102
- )
103
-
104
- return { stats, terminal, cache, blocks }
105
- }
1
+ import type { Context } from "@opencode/plugin/tui/context"
2
+ import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
3
+
4
+ import type { SummaryBlock } from "../../shared/blocks.ts"
5
+ import type { CacheSample } from "../../shared/cache.ts"
6
+ import { contextRpc } from "../../shared/rpc.ts"
7
+ import type { Settings } from "../../shared/settings.ts"
8
+ import { parseStats, STATS_KEY, TERMINAL_KEY, type ContextStats } from "../../shared/stats.ts"
9
+
10
+ /** Panel 需要的服务端数据;全部是响应式 accessor。 */
11
+ export interface PanelData {
12
+ readonly stats: Accessor<ContextStats | undefined>
13
+ readonly terminal: Accessor<string | undefined>
14
+ readonly cache: Accessor<readonly CacheSample[]>
15
+ readonly blocks: Accessor<readonly SummaryBlock[]>
16
+ }
17
+
18
+ /**
19
+ * 输入:TUI 上下文 + sessionID + 设置 accessor。
20
+ * 输出:统计 / 终止原因 / 缓存历史 / 摘要块。
21
+ * 预期:挂载时拉取,usage/compaction/idle 时刷新;设置开关变化时重新拉取。
22
+ */
23
+ export function usePanelData(input: {
24
+ readonly context: Context
25
+ readonly sessionID: string
26
+ readonly settings: Accessor<Settings>
27
+ }): PanelData {
28
+ const [stats, setStats] = createSignal<ContextStats>()
29
+ const [terminal, setTerminal] = createSignal<string>()
30
+ const [cache, setCache] = createSignal<readonly CacheSample[]>([])
31
+ const [blocks, setBlocks] = createSignal<readonly SummaryBlock[]>([])
32
+ const rpc = () => contextRpc(input.context.client)
33
+
34
+ /** 输入:无;输出:无;预期:从服务端拉取该会话持久化的缓存历史。 */
35
+ const refreshCache = async (): Promise<void> => {
36
+ try {
37
+ const got = await rpc().cacheHistoryGet({ sessionID: input.sessionID })
38
+ setCache(got.samples)
39
+ } catch {
40
+ // server plugin not loaded;保留上一次数据
41
+ }
42
+ }
43
+
44
+ /** 输入:无;输出:无;预期:从服务端拉取 active 摘要链。 */
45
+ const refreshBlocks = async (): Promise<void> => {
46
+ if (!input.settings().showSummaries) return
47
+ try {
48
+ const got = await rpc().blocksGet({ sessionID: input.sessionID })
49
+ setBlocks(got.blocks)
50
+ } catch {
51
+ // server plugin not loaded;保留上一次数据
52
+ }
53
+ }
54
+
55
+ /** 输入:无;输出:无;预期:从服务端重新拉取 session metadata 里的统计。 */
56
+ const refresh = async (): Promise<void> => {
57
+ try {
58
+ const info = await input.context.client.session.get({ sessionID: input.sessionID })
59
+ setStats(parseStats(info.metadata?.[STATS_KEY]))
60
+ const rawTerminal = info.metadata?.[TERMINAL_KEY]
61
+ setTerminal(typeof rawTerminal === "string" ? rawTerminal : undefined)
62
+ } catch {
63
+ // 统计缺失不影响主界面,保持上一次的值
64
+ }
65
+ }
66
+
67
+ // 显示开关变化时重新拉取;首次挂载也会跑一次。
68
+ createEffect(() => {
69
+ if (input.settings().showCacheChart || input.settings().showCacheLog) void refreshCache()
70
+ })
71
+ createEffect(() => {
72
+ if (input.settings().showSummaries) void refreshBlocks()
73
+ })
74
+
75
+ createEffect(() => {
76
+ void refresh()
77
+ })
78
+
79
+ onCleanup(
80
+ input.context.data.on("session.usage.updated", (event) => {
81
+ if (event.data.sessionID !== input.sessionID) return
82
+ void refresh()
83
+ // 缓存历史由服务端在 usage.updated 时补写;这里同步刷新面板。
84
+ if (input.settings().showCacheChart || input.settings().showCacheLog) void refreshCache()
85
+ }),
86
+ )
87
+ onCleanup(
88
+ input.context.data.on("session.compaction.ended", (event) => {
89
+ if (event.data.sessionID !== input.sessionID) return
90
+ void refresh()
91
+ void refreshCache()
92
+ void refreshBlocks()
93
+ }),
94
+ )
95
+ onCleanup(
96
+ input.context.data.on("session.idle", (event) => {
97
+ if (event.data.sessionID !== input.sessionID) return
98
+ void refresh()
99
+ void refreshCache()
100
+ void refreshBlocks()
101
+ }),
102
+ )
103
+
104
+ return { stats, terminal, cache, blocks }
105
+ }
@@ -1,176 +1,176 @@
1
- import type { Context } from "@opencode/plugin/tui/context"
2
- import { createMemo, For, Show } from "solid-js"
3
-
4
- import { resolveMessages } from "../../i18n/index.ts"
5
- import { formatTokens } from "../../shared/format.ts"
6
- import { effectiveLimit, localeArgument, type Settings } from "../../shared/settings.ts"
7
- import { scaleCategories } from "../../shared/stats.ts"
8
- import { mix, toHex, toRgb, type Rgb } from "../chart/color.ts"
9
- import type { UpdateSettings } from "../types.ts"
10
- import { CacheSection } from "./cache.tsx"
11
- import { usePanelData } from "./data.ts"
12
- import { SummarySection } from "./summaries.tsx"
13
-
14
- /**
15
- * 输入:该分类占上下文总量的百分比 + 主题。
16
- * 输出:0%-50%-100% 依次绿-黄-红的连续插值。
17
- * 预期:每个百分比都有独立颜色;主题色不可解析时回退到内置色。
18
- */
19
- function occupancyColor(pct: number, theme: Context["theme"]): string {
20
- const green = toRgb(theme.text.feedback.success.base) ?? ([34, 197, 94] as Rgb)
21
- const yellow = toRgb(theme.text.feedback.warning.base) ?? ([234, 179, 8] as Rgb)
22
- const red = toRgb(theme.text.feedback.error.base) ?? ([239, 68, 68] as Rgb)
23
- const ratio = Math.min(1, Math.max(0, pct / 100))
24
- if (ratio <= 0.5) return toHex(mix(green, yellow, ratio * 2))
25
- return toHex(mix(yellow, red, (ratio - 0.5) * 2))
26
- }
27
-
28
- /**
29
- * 输入:0-100 的百分比。
30
- * 输出:OpenTUI 的百分比宽度类型 `${number}%`。
31
- * 预期:先转字符串再断言——模板里直接插数字会被 lint 规则拦住,
32
- * 而 OpenTUI 只接受 `${number}%` 这种字面量类型。
33
- */
34
- function percentWidth(value: number): `${number}%` {
35
- const clamped = Math.min(100, Math.max(0, value))
36
- return `${clamped.toString()}%` as `${number}%`
37
- }
38
-
39
- /**
40
- * 输入:TUI 上下文 + sessionID + 设置。
41
- * 输出:侧栏里的上下文分类面板(可折叠),外加缓存命中与摘要块。
42
- * 预期:分类条总量交给内置 Context 区块;缓存历史由服务端持久化,TUI 只读。
43
- */
44
- export function Panel(props: {
45
- readonly context: Context
46
- readonly sessionID: string
47
- readonly settings: Settings
48
- readonly updateSettings: UpdateSettings
49
- }) {
50
- const theme = props.context.theme
51
- const copy = createMemo(() => resolveMessages(localeArgument(props.settings.locale)))
52
- const data = usePanelData({
53
- context: props.context,
54
- sessionID: props.sessionID,
55
- settings: () => props.settings,
56
- })
57
-
58
- const session = createMemo(() => props.context.data.session.get(props.sessionID))
59
- const messages = createMemo(() => props.context.data.session.message.list(props.sessionID))
60
-
61
- /** 最后一条 assistant;模型上限和 usage 分开取,避免 provider 没给 usage 时连上限都丢。 */
62
- const lastAssistant = createMemo(() => {
63
- const list = messages()
64
- for (let index = list.length - 1; index >= 0; index -= 1) {
65
- const message = list[index]
66
- if (message?.type === "assistant") return message
67
- }
68
- return undefined
69
- })
70
-
71
- /** 模型原生窗口:从最后一条 assistant 的模型解析。 */
72
- const nativeLimit = createMemo(() => {
73
- const message = lastAssistant()
74
- if (message === undefined) return 0
75
- const model = props.context.data.location.model
76
- .list(session()?.location)
77
- ?.find((item) => item.providerID === message.model.providerID && item.id === message.model.id)
78
- return model?.limit.context ?? 0
79
- })
80
-
81
- /** 真实 prompt token:只算 input + cache,output/reasoning 不属于本轮输入。 */
82
- const realUsed = createMemo(() => {
83
- const tokens = lastAssistant()?.tokens
84
- if (tokens === undefined) return undefined
85
- const used = tokens.input + tokens.cache.read + tokens.cache.write
86
- return used > 0 ? used : undefined
87
- })
88
-
89
- /** 上下文占用:真实 usage 优先,缺失时才回退到服务端估算。 */
90
- const contextTokens = createMemo(() => realUsed() ?? data.stats()?.estimatedTotal ?? 0)
91
-
92
- /** 实际生效的上下文上限:自定义优先,否则模型原生。 */
93
- const limit = createMemo(() => effectiveLimit(props.settings, nativeLimit()))
94
-
95
- /** 仅当用户自定义上限时展示百分比(auto 时内置区块已经显示了)。 */
96
- const percent = createMemo(() => {
97
- const max = limit()
98
- if (max === undefined || max <= 0) return undefined
99
- return Math.round((contextTokens() / max) * 100)
100
- })
101
-
102
- const rows = createMemo(() => {
103
- const value = data.stats()
104
- if (value === undefined || value.estimatedTotal <= 0) return []
105
- return scaleCategories(value, contextTokens()).filter((row) => row.value > 0)
106
- })
107
-
108
- const toggleExpanded = (): void => {
109
- void props.updateSettings((draft) => {
110
- draft.expanded = !draft.expanded
111
- })
112
- }
113
-
114
- return (
115
- <Show when={props.settings.enabled}>
116
- <box width="100%" flexDirection="column">
117
- <box flexDirection="row" height={1} onMouseDown={toggleExpanded}>
118
- <box flexDirection="row" gap={1}>
119
- <text fg={theme.text.base}>{props.settings.expanded ? "▼" : "▶"}</text>
120
- <text fg={theme.text.base}>
121
- <b>{copy().title}</b>
122
- </text>
123
- <Show when={props.settings.contextLimit !== "auto" && percent() !== undefined}>
124
- <text fg={occupancyColor(percent() ?? 0, theme)}>
125
- {`(${(percent() ?? 0).toString()}%)`}
126
- </text>
127
- </Show>
128
- </box>
129
- </box>
130
- <Show when={data.terminal()}>
131
- <text fg={theme.text.feedback.error.base} wrapMode="word">
132
- {`⚠ ${copy().terminal}`}
133
- </text>
134
- </Show>
135
- <Show when={props.settings.expanded}>
136
- <box flexDirection="column" width="100%" paddingLeft={2}>
137
- <For each={rows()}>
138
- {(row) => (
139
- <box flexDirection="row" height={1} gap={1}>
140
- <text fg={theme.text.muted} width={10} flexShrink={0}>
141
- {copy().labels[row.key]}
142
- </text>
143
- <box flexGrow={1} height={1} backgroundColor={theme.background.raised.high}>
144
- <box height={1} width={percentWidth(row.pct)} backgroundColor={occupancyColor(row.pct, theme)} />
145
- </box>
146
- <text fg={theme.text.muted} width={4} flexShrink={0} textAlign="right">
147
- {`${row.pct.toString()}%`}
148
- </text>
149
- <text fg={theme.text.muted} width={7} flexShrink={0} textAlign="right">
150
- {`${copy().approximate}${formatTokens(row.value)}`}
151
- </text>
152
- </box>
153
- )}
154
- </For>
155
- </box>
156
- </Show>
157
-
158
- <CacheSection
159
- theme={theme}
160
- copy={copy()}
161
- settings={props.settings}
162
- updateSettings={props.updateSettings}
163
- samples={data.cache()}
164
- />
165
- <SummarySection
166
- context={props.context}
167
- theme={theme}
168
- copy={copy()}
169
- settings={props.settings}
170
- updateSettings={props.updateSettings}
171
- blocks={data.blocks()}
172
- />
173
- </box>
174
- </Show>
175
- )
176
- }
1
+ import type { Context } from "@opencode/plugin/tui/context"
2
+ import { createMemo, For, Show } from "solid-js"
3
+
4
+ import { resolveMessages } from "../../i18n/index.ts"
5
+ import { formatTokens } from "../../shared/format.ts"
6
+ import { effectiveLimit, localeArgument, type Settings } from "../../shared/settings.ts"
7
+ import { scaleCategories } from "../../shared/stats.ts"
8
+ import { mix, toHex, toRgb, type Rgb } from "../chart/color.ts"
9
+ import type { UpdateSettings } from "../types.ts"
10
+ import { CacheSection } from "./cache.tsx"
11
+ import { usePanelData } from "./data.ts"
12
+ import { SummarySection } from "./summaries.tsx"
13
+
14
+ /**
15
+ * 输入:该分类占上下文总量的百分比 + 主题。
16
+ * 输出:0%-50%-100% 依次绿-黄-红的连续插值。
17
+ * 预期:每个百分比都有独立颜色;主题色不可解析时回退到内置色。
18
+ */
19
+ function occupancyColor(pct: number, theme: Context["theme"]): string {
20
+ const green = toRgb(theme.text.feedback.success.base) ?? ([34, 197, 94] as Rgb)
21
+ const yellow = toRgb(theme.text.feedback.warning.base) ?? ([234, 179, 8] as Rgb)
22
+ const red = toRgb(theme.text.feedback.error.base) ?? ([239, 68, 68] as Rgb)
23
+ const ratio = Math.min(1, Math.max(0, pct / 100))
24
+ if (ratio <= 0.5) return toHex(mix(green, yellow, ratio * 2))
25
+ return toHex(mix(yellow, red, (ratio - 0.5) * 2))
26
+ }
27
+
28
+ /**
29
+ * 输入:0-100 的百分比。
30
+ * 输出:OpenTUI 的百分比宽度类型 `${number}%`。
31
+ * 预期:先转字符串再断言——模板里直接插数字会被 lint 规则拦住,
32
+ * 而 OpenTUI 只接受 `${number}%` 这种字面量类型。
33
+ */
34
+ function percentWidth(value: number): `${number}%` {
35
+ const clamped = Math.min(100, Math.max(0, value))
36
+ return `${clamped.toString()}%` as `${number}%`
37
+ }
38
+
39
+ /**
40
+ * 输入:TUI 上下文 + sessionID + 设置。
41
+ * 输出:侧栏里的上下文分类面板(可折叠),外加缓存命中与摘要块。
42
+ * 预期:分类条总量交给内置 Context 区块;缓存历史由服务端持久化,TUI 只读。
43
+ */
44
+ export function Panel(props: {
45
+ readonly context: Context
46
+ readonly sessionID: string
47
+ readonly settings: Settings
48
+ readonly updateSettings: UpdateSettings
49
+ }) {
50
+ const theme = props.context.theme
51
+ const copy = createMemo(() => resolveMessages(localeArgument(props.settings.locale)))
52
+ const data = usePanelData({
53
+ context: props.context,
54
+ sessionID: props.sessionID,
55
+ settings: () => props.settings,
56
+ })
57
+
58
+ const session = createMemo(() => props.context.data.session.get(props.sessionID))
59
+ const messages = createMemo(() => props.context.data.session.message.list(props.sessionID))
60
+
61
+ /** 最后一条 assistant;模型上限和 usage 分开取,避免 provider 没给 usage 时连上限都丢。 */
62
+ const lastAssistant = createMemo(() => {
63
+ const list = messages()
64
+ for (let index = list.length - 1; index >= 0; index -= 1) {
65
+ const message = list[index]
66
+ if (message?.type === "assistant") return message
67
+ }
68
+ return undefined
69
+ })
70
+
71
+ /** 模型原生窗口:从最后一条 assistant 的模型解析。 */
72
+ const nativeLimit = createMemo(() => {
73
+ const message = lastAssistant()
74
+ if (message === undefined) return 0
75
+ const model = props.context.data.location.model
76
+ .list(session()?.location)
77
+ ?.find((item) => item.providerID === message.model.providerID && item.id === message.model.id)
78
+ return model?.limit.context ?? 0
79
+ })
80
+
81
+ /** 真实 prompt token:只算 input + cache,output/reasoning 不属于本轮输入。 */
82
+ const realUsed = createMemo(() => {
83
+ const tokens = lastAssistant()?.tokens
84
+ if (tokens === undefined) return undefined
85
+ const used = tokens.input + tokens.cache.read + tokens.cache.write
86
+ return used > 0 ? used : undefined
87
+ })
88
+
89
+ /** 上下文占用:真实 usage 优先,缺失时才回退到服务端估算。 */
90
+ const contextTokens = createMemo(() => realUsed() ?? data.stats()?.estimatedTotal ?? 0)
91
+
92
+ /** 实际生效的上下文上限:自定义优先,否则模型原生。 */
93
+ const limit = createMemo(() => effectiveLimit(props.settings, nativeLimit()))
94
+
95
+ /** 仅当用户自定义上限时展示百分比(auto 时内置区块已经显示了)。 */
96
+ const percent = createMemo(() => {
97
+ const max = limit()
98
+ if (max === undefined || max <= 0) return undefined
99
+ return Math.round((contextTokens() / max) * 100)
100
+ })
101
+
102
+ const rows = createMemo(() => {
103
+ const value = data.stats()
104
+ if (value === undefined || value.estimatedTotal <= 0) return []
105
+ return scaleCategories(value, contextTokens()).filter((row) => row.value > 0)
106
+ })
107
+
108
+ const toggleExpanded = (): void => {
109
+ void props.updateSettings((draft) => {
110
+ draft.expanded = !draft.expanded
111
+ })
112
+ }
113
+
114
+ return (
115
+ <Show when={props.settings.enabled}>
116
+ <box width="100%" flexDirection="column">
117
+ <box flexDirection="row" height={1} onMouseDown={toggleExpanded}>
118
+ <box flexDirection="row" gap={1}>
119
+ <text fg={theme.text.base}>{props.settings.expanded ? "▼" : "▶"}</text>
120
+ <text fg={theme.text.base}>
121
+ <b>{copy().title}</b>
122
+ </text>
123
+ <Show when={props.settings.contextLimit !== "auto" && percent() !== undefined}>
124
+ <text fg={occupancyColor(percent() ?? 0, theme)}>
125
+ {`(${(percent() ?? 0).toString()}%)`}
126
+ </text>
127
+ </Show>
128
+ </box>
129
+ </box>
130
+ <Show when={data.terminal()}>
131
+ <text fg={theme.text.feedback.error.base} wrapMode="word">
132
+ {`⚠ ${copy().terminal}`}
133
+ </text>
134
+ </Show>
135
+ <Show when={props.settings.expanded}>
136
+ <box flexDirection="column" width="100%" paddingLeft={2}>
137
+ <For each={rows()}>
138
+ {(row) => (
139
+ <box flexDirection="row" height={1} gap={1}>
140
+ <text fg={theme.text.muted} width={10} flexShrink={0}>
141
+ {copy().labels[row.key]}
142
+ </text>
143
+ <box flexGrow={1} height={1} backgroundColor={theme.background.raised.high}>
144
+ <box height={1} width={percentWidth(row.pct)} backgroundColor={occupancyColor(row.pct, theme)} />
145
+ </box>
146
+ <text fg={theme.text.muted} width={4} flexShrink={0} textAlign="right">
147
+ {`${row.pct.toString()}%`}
148
+ </text>
149
+ <text fg={theme.text.muted} width={7} flexShrink={0} textAlign="right">
150
+ {`${copy().approximate}${formatTokens(row.value)}`}
151
+ </text>
152
+ </box>
153
+ )}
154
+ </For>
155
+ </box>
156
+ </Show>
157
+
158
+ <CacheSection
159
+ theme={theme}
160
+ copy={copy()}
161
+ settings={props.settings}
162
+ updateSettings={props.updateSettings}
163
+ samples={data.cache()}
164
+ />
165
+ <SummarySection
166
+ context={props.context}
167
+ theme={theme}
168
+ copy={copy()}
169
+ settings={props.settings}
170
+ updateSettings={props.updateSettings}
171
+ blocks={data.blocks()}
172
+ />
173
+ </box>
174
+ </Show>
175
+ )
176
+ }