@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/LICENSE +619 -0
  2. package/README.md +348 -2
  3. package/dist/index.d.ts +7 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +7611 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/commands/compression-targets.d.ts +13 -0
  8. package/dist/lib/commands/compression-targets.d.ts.map +1 -0
  9. package/dist/lib/commands/context.d.ts +67 -0
  10. package/dist/lib/commands/context.d.ts.map +1 -0
  11. package/dist/lib/commands/decompress.d.ts +12 -0
  12. package/dist/lib/commands/decompress.d.ts.map +1 -0
  13. package/dist/lib/commands/help.d.ts +18 -0
  14. package/dist/lib/commands/help.d.ts.map +1 -0
  15. package/dist/lib/commands/index.d.ts +8 -0
  16. package/dist/lib/commands/index.d.ts.map +1 -0
  17. package/dist/lib/commands/manual.d.ts +23 -0
  18. package/dist/lib/commands/manual.d.ts.map +1 -0
  19. package/dist/lib/commands/recompress.d.ts +12 -0
  20. package/dist/lib/commands/recompress.d.ts.map +1 -0
  21. package/dist/lib/commands/stats.d.ts +25 -0
  22. package/dist/lib/commands/stats.d.ts.map +1 -0
  23. package/dist/lib/commands/sweep.d.ts +23 -0
  24. package/dist/lib/commands/sweep.d.ts.map +1 -0
  25. package/dist/lib/compress/index.d.ts +3 -0
  26. package/dist/lib/compress/index.d.ts.map +1 -0
  27. package/dist/lib/compress/pipeline.d.ts +29 -0
  28. package/dist/lib/compress/pipeline.d.ts.map +1 -0
  29. package/dist/lib/compress/protected-content.d.ts +6 -0
  30. package/dist/lib/compress/protected-content.d.ts.map +1 -0
  31. package/dist/lib/compress/rc.d.ts +15 -0
  32. package/dist/lib/compress/rc.d.ts.map +1 -0
  33. package/dist/lib/compress/search.d.ts +12 -0
  34. package/dist/lib/compress/search.d.ts.map +1 -0
  35. package/dist/lib/compress/state.d.ts +10 -0
  36. package/dist/lib/compress/state.d.ts.map +1 -0
  37. package/dist/lib/compress/summary.d.ts +58 -0
  38. package/dist/lib/compress/summary.d.ts.map +1 -0
  39. package/dist/lib/compress/timing.d.ts +18 -0
  40. package/dist/lib/compress/timing.d.ts.map +1 -0
  41. package/dist/lib/compress/types.d.ts +55 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress-permission.d.ts +4 -0
  44. package/dist/lib/compress-permission.d.ts.map +1 -0
  45. package/dist/lib/config.d.ts +83 -0
  46. package/dist/lib/config.d.ts.map +1 -0
  47. package/dist/lib/hooks.d.ts +22 -0
  48. package/dist/lib/hooks.d.ts.map +1 -0
  49. package/dist/lib/logger.d.ts +31 -0
  50. package/dist/lib/logger.d.ts.map +1 -0
  51. package/dist/lib/message-ids.d.ts +20 -0
  52. package/dist/lib/message-ids.d.ts.map +1 -0
  53. package/dist/lib/messages/index.d.ts +8 -0
  54. package/dist/lib/messages/index.d.ts.map +1 -0
  55. package/dist/lib/messages/inject/inject.d.ts +7 -0
  56. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  58. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/utils.d.ts +23 -0
  60. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  61. package/dist/lib/messages/prune.d.ts +4 -0
  62. package/dist/lib/messages/prune.d.ts.map +1 -0
  63. package/dist/lib/messages/query.d.ts +7 -0
  64. package/dist/lib/messages/query.d.ts.map +1 -0
  65. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  66. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  67. package/dist/lib/messages/shape.d.ts +5 -0
  68. package/dist/lib/messages/shape.d.ts.map +1 -0
  69. package/dist/lib/messages/sync.d.ts +4 -0
  70. package/dist/lib/messages/sync.d.ts.map +1 -0
  71. package/dist/lib/messages/utils.d.ts +27 -0
  72. package/dist/lib/messages/utils.d.ts.map +1 -0
  73. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  74. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  75. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  76. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  77. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  78. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  82. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  83. package/dist/lib/prompts/index.d.ts +4 -0
  84. package/dist/lib/prompts/index.d.ts.map +1 -0
  85. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  86. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  87. package/dist/lib/prompts/store.d.ts +26 -0
  88. package/dist/lib/prompts/store.d.ts.map +1 -0
  89. package/dist/lib/prompts/system.d.ts +4 -0
  90. package/dist/lib/prompts/system.d.ts.map +1 -0
  91. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  92. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  93. package/dist/lib/protected-patterns.d.ts +6 -0
  94. package/dist/lib/protected-patterns.d.ts.map +1 -0
  95. package/dist/lib/state/index.d.ts +5 -0
  96. package/dist/lib/state/index.d.ts.map +1 -0
  97. package/dist/lib/state/persistence.d.ts +45 -0
  98. package/dist/lib/state/persistence.d.ts.map +1 -0
  99. package/dist/lib/state/state.d.ts +9 -0
  100. package/dist/lib/state/state.d.ts.map +1 -0
  101. package/dist/lib/state/tool-cache.d.ts +13 -0
  102. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  103. package/dist/lib/state/types.d.ts +102 -0
  104. package/dist/lib/state/types.d.ts.map +1 -0
  105. package/dist/lib/state/utils.d.ts +22 -0
  106. package/dist/lib/state/utils.d.ts.map +1 -0
  107. package/dist/lib/strategies/deduplication.d.ts +10 -0
  108. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  109. package/dist/lib/strategies/index.d.ts +3 -0
  110. package/dist/lib/strategies/index.d.ts.map +1 -0
  111. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  112. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  113. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  114. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  115. package/dist/lib/token-utils.d.ts +19 -0
  116. package/dist/lib/token-utils.d.ts.map +1 -0
  117. package/dist/lib/tui/dialogs.d.ts +29 -0
  118. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  119. package/dist/lib/tui/format.d.ts +4 -0
  120. package/dist/lib/tui/format.d.ts.map +1 -0
  121. package/dist/lib/tui/types.d.ts +18 -0
  122. package/dist/lib/tui/types.d.ts.map +1 -0
  123. package/dist/lib/tui/ui.d.ts +48 -0
  124. package/dist/lib/tui/ui.d.ts.map +1 -0
  125. package/dist/lib/ui/notification.d.ts +17 -0
  126. package/dist/lib/ui/notification.d.ts.map +1 -0
  127. package/dist/lib/ui/utils.d.ts +10 -0
  128. package/dist/lib/ui/utils.d.ts.map +1 -0
  129. package/dist/lib/v2/index.d.ts +5 -0
  130. package/dist/lib/v2/index.d.ts.map +1 -0
  131. package/dist/lib/v2/messages.d.ts +13 -0
  132. package/dist/lib/v2/messages.d.ts.map +1 -0
  133. package/dist/lib/v2/rpc.d.ts +55 -0
  134. package/dist/lib/v2/rpc.d.ts.map +1 -0
  135. package/dist/lib/v2/theme.d.ts +55 -0
  136. package/dist/lib/v2/theme.d.ts.map +1 -0
  137. package/dist/lib/v2/tui.d.ts +4 -0
  138. package/dist/lib/v2/tui.d.ts.map +1 -0
  139. package/dist/tui.d.ts +7 -0
  140. package/dist/tui.d.ts.map +1 -0
  141. package/lib/commands/compression-targets.ts +54 -0
  142. package/lib/commands/context.ts +305 -0
  143. package/lib/commands/decompress.ts +273 -0
  144. package/lib/commands/help.ts +76 -0
  145. package/lib/commands/index.ts +11 -0
  146. package/lib/commands/manual.ts +126 -0
  147. package/lib/commands/recompress.ts +222 -0
  148. package/lib/commands/stats.ts +159 -0
  149. package/lib/commands/sweep.ts +266 -0
  150. package/lib/compress/index.ts +2 -0
  151. package/lib/compress/pipeline.ts +116 -0
  152. package/lib/compress/protected-content.ts +163 -0
  153. package/lib/compress/rc.ts +458 -0
  154. package/lib/compress/search.ts +277 -0
  155. package/lib/compress/state.ts +272 -0
  156. package/lib/compress/summary.ts +293 -0
  157. package/lib/compress/timing.ts +77 -0
  158. package/lib/compress/types.ts +57 -0
  159. package/lib/compress-permission.ts +9 -0
  160. package/lib/config.ts +995 -0
  161. package/lib/hooks.ts +205 -0
  162. package/lib/logger.ts +226 -0
  163. package/lib/message-ids.ts +181 -0
  164. package/lib/messages/index.ts +7 -0
  165. package/lib/messages/inject/inject.ts +215 -0
  166. package/lib/messages/inject/subagent-results.ts +84 -0
  167. package/lib/messages/inject/utils.ts +304 -0
  168. package/lib/messages/prune.ts +238 -0
  169. package/lib/messages/query.ts +71 -0
  170. package/lib/messages/reasoning-strip.ts +40 -0
  171. package/lib/messages/shape.ts +50 -0
  172. package/lib/messages/sync.ts +124 -0
  173. package/lib/messages/utils.ts +190 -0
  174. package/lib/prompts/compress-rc.ts +25 -0
  175. package/lib/prompts/context-limit-nudge.ts +18 -0
  176. package/lib/prompts/extensions/nudge.ts +34 -0
  177. package/lib/prompts/extensions/system.ts +32 -0
  178. package/lib/prompts/extensions/tool.ts +21 -0
  179. package/lib/prompts/index.ts +29 -0
  180. package/lib/prompts/iteration-nudge.ts +6 -0
  181. package/lib/prompts/store.ts +452 -0
  182. package/lib/prompts/system.ts +41 -0
  183. package/lib/prompts/turn-nudge.ts +10 -0
  184. package/lib/protected-patterns.ts +159 -0
  185. package/lib/state/index.ts +4 -0
  186. package/lib/state/persistence.ts +305 -0
  187. package/lib/state/state.ts +210 -0
  188. package/lib/state/tool-cache.ts +99 -0
  189. package/lib/state/types.ts +114 -0
  190. package/lib/state/utils.ts +345 -0
  191. package/lib/strategies/deduplication.ts +125 -0
  192. package/lib/strategies/index.ts +2 -0
  193. package/lib/strategies/purge-errors.ts +86 -0
  194. package/lib/subagents/subagent-results.ts +75 -0
  195. package/lib/token-utils.ts +164 -0
  196. package/lib/tui/dialogs.tsx +237 -0
  197. package/lib/tui/format.ts +25 -0
  198. package/lib/tui/types.ts +25 -0
  199. package/lib/tui/ui.tsx +245 -0
  200. package/lib/ui/notification.ts +347 -0
  201. package/lib/ui/utils.ts +304 -0
  202. package/lib/v2/index.ts +401 -0
  203. package/lib/v2/messages.ts +252 -0
  204. package/lib/v2/rpc.ts +48 -0
  205. package/lib/v2/theme.ts +67 -0
  206. package/lib/v2/tui.tsx +115 -0
  207. package/package.json +91 -5
  208. package/server.js +1 -0
  209. package/tui.tsx +7 -0
@@ -0,0 +1,75 @@
1
+ import type { WithParts } from "../state"
2
+
3
+ const SUB_AGENT_RESULT_BLOCK_REGEX =
4
+ /(<(?:task_result|subagent\b[^>]*)>\s*)([\s\S]*?)(\s*<\/(?:task_result|subagent)>)/i
5
+
6
+ export function getSubAgentId(part: any): string | null {
7
+ const sessionId = part?.state?.metadata?.sessionId ?? part?.state?.metadata?.sessionID
8
+ if (typeof sessionId !== "string") {
9
+ return null
10
+ }
11
+
12
+ const value = sessionId.trim()
13
+ return value.length > 0 ? value : null
14
+ }
15
+
16
+ export function buildSubagentResultText(messages: WithParts[]): string {
17
+ const assistantMessages = messages.filter((message) => message.info.role === "assistant")
18
+ if (assistantMessages.length === 0) {
19
+ return ""
20
+ }
21
+
22
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]
23
+ const lastText = getLastTextPart(lastAssistant)
24
+
25
+ if (assistantMessages.length < 2) {
26
+ return lastText
27
+ }
28
+
29
+ const secondToLastAssistant = assistantMessages[assistantMessages.length - 2]
30
+ if (!assistantMessageHasCompressTool(secondToLastAssistant)) {
31
+ return lastText
32
+ }
33
+
34
+ const secondToLastText = getLastTextPart(secondToLastAssistant)
35
+ return [secondToLastText, lastText].filter((text) => text.length > 0).join("\n\n")
36
+ }
37
+
38
+ export function mergeSubagentResult(output: string, subAgentResultText: string): string {
39
+ if (!subAgentResultText || typeof output !== "string") {
40
+ return output
41
+ }
42
+
43
+ return output.replace(
44
+ SUB_AGENT_RESULT_BLOCK_REGEX,
45
+ (_match, openTag: string, _body: string, closeTag: string) =>
46
+ `${openTag}${subAgentResultText}${closeTag}`,
47
+ )
48
+ }
49
+
50
+ function getLastTextPart(message: WithParts): string {
51
+ const parts = Array.isArray(message.parts) ? message.parts : []
52
+ for (let index = parts.length - 1; index >= 0; index--) {
53
+ const part = parts[index]
54
+ if (part.type !== "text" || typeof part.text !== "string") {
55
+ continue
56
+ }
57
+
58
+ const text = part.text.trim()
59
+ if (!text) {
60
+ continue
61
+ }
62
+
63
+ return text
64
+ }
65
+
66
+ return ""
67
+ }
68
+
69
+ function assistantMessageHasCompressTool(message: WithParts): boolean {
70
+ const parts = Array.isArray(message.parts) ? message.parts : []
71
+ return parts.some(
72
+ (part) =>
73
+ part.type === "tool" && part.tool === "compress" && part.state?.status === "completed",
74
+ )
75
+ }
@@ -0,0 +1,164 @@
1
+ import { SessionState, WithParts } from "./state"
2
+ import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
3
+ import { Logger } from "./logger"
4
+ import * as _anthropicTokenizer from "@anthropic-ai/tokenizer"
5
+ const anthropicCountTokens = (_anthropicTokenizer.countTokens ??
6
+ (_anthropicTokenizer as any).default?.countTokens) as typeof _anthropicTokenizer.countTokens
7
+ import { getLastUserMessage } from "./messages/query"
8
+
9
+ export function getCurrentTokenUsage(state: SessionState, messages: WithParts[]): number {
10
+ for (let i = messages.length - 1; i >= 0; i--) {
11
+ const msg = messages[i]
12
+ if (msg.info.role !== "assistant") {
13
+ continue
14
+ }
15
+
16
+ const assistantInfo = msg.info as AssistantMessage
17
+ if ((assistantInfo.tokens?.output || 0) <= 0) {
18
+ continue
19
+ }
20
+
21
+ if (
22
+ state.lastCompaction > 0 &&
23
+ (msg.info.time.created < state.lastCompaction ||
24
+ (msg.info.summary === true && msg.info.time.created === state.lastCompaction))
25
+ ) {
26
+ return 0
27
+ }
28
+
29
+ const input = assistantInfo.tokens?.input || 0
30
+ const output = assistantInfo.tokens?.output || 0
31
+ const reasoning = assistantInfo.tokens?.reasoning || 0
32
+ const cacheRead = assistantInfo.tokens?.cache?.read || 0
33
+ const cacheWrite = assistantInfo.tokens?.cache?.write || 0
34
+ return input + output + reasoning + cacheRead + cacheWrite
35
+ }
36
+
37
+ return 0
38
+ }
39
+
40
+ export function getCurrentParams(
41
+ state: SessionState,
42
+ messages: WithParts[],
43
+ logger: Logger,
44
+ ): {
45
+ providerId: string | undefined
46
+ modelId: string | undefined
47
+ agent: string | undefined
48
+ variant: string | undefined
49
+ } {
50
+ const userMsg = getLastUserMessage(messages)
51
+ if (!userMsg) {
52
+ logger.debug("No user message found when determining current params")
53
+ return {
54
+ providerId: undefined,
55
+ modelId: undefined,
56
+ agent: undefined,
57
+ variant: undefined,
58
+ }
59
+ }
60
+ const userInfo = userMsg.info as UserMessage
61
+ const agent: string = userInfo.agent
62
+ const providerId: string | undefined = userInfo.model.providerID
63
+ const modelId: string | undefined = userInfo.model.modelID
64
+ const variant: string | undefined = userInfo.model.variant
65
+
66
+ return { providerId, modelId, agent, variant }
67
+ }
68
+
69
+ export function countTokens(text: string): number {
70
+ if (!text) return 0
71
+ try {
72
+ return anthropicCountTokens(text)
73
+ } catch {
74
+ return Math.round(text.length / 4)
75
+ }
76
+ }
77
+
78
+ export function estimateTokensBatch(texts: string[]): number {
79
+ if (texts.length === 0) return 0
80
+ return countTokens(texts.join(" "))
81
+ }
82
+
83
+ export const COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]"
84
+
85
+ function stringifyToolContent(value: unknown): string {
86
+ return typeof value === "string" ? value : JSON.stringify(value)
87
+ }
88
+
89
+ export function extractCompletedToolOutput(part: any): string | undefined {
90
+ if (
91
+ part?.type !== "tool" ||
92
+ part.state?.status !== "completed" ||
93
+ part.state?.output === undefined
94
+ ) {
95
+ return undefined
96
+ }
97
+
98
+ if (part.state?.time?.compacted) {
99
+ return COMPACTED_TOOL_OUTPUT_PLACEHOLDER
100
+ }
101
+
102
+ return stringifyToolContent(part.state.output)
103
+ }
104
+
105
+ export function extractToolContent(part: any): string[] {
106
+ const contents: string[] = []
107
+
108
+ if (part?.type !== "tool") {
109
+ return contents
110
+ }
111
+
112
+ if (part.state?.input !== undefined) {
113
+ contents.push(stringifyToolContent(part.state.input))
114
+ }
115
+
116
+ const completedOutput = extractCompletedToolOutput(part)
117
+ if (completedOutput !== undefined) {
118
+ contents.push(completedOutput)
119
+ } else if (part.state?.status === "error" && part.state?.error) {
120
+ contents.push(stringifyToolContent(part.state.error))
121
+ }
122
+
123
+ return contents
124
+ }
125
+
126
+ export function countToolTokens(part: any): number {
127
+ const contents = extractToolContent(part)
128
+ return estimateTokensBatch(contents)
129
+ }
130
+
131
+ export function getTotalToolTokens(state: SessionState, toolIds: string[]): number {
132
+ let total = 0
133
+ for (const id of toolIds) {
134
+ const entry = state.toolParameters.get(id)
135
+ total += entry?.tokenCount ?? 0
136
+ }
137
+ return total
138
+ }
139
+
140
+ export function countMessageTextTokens(msg: WithParts): number {
141
+ const texts: string[] = []
142
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
143
+ for (const part of parts) {
144
+ if (part.type === "text") {
145
+ texts.push(part.text)
146
+ }
147
+ }
148
+ if (texts.length === 0) return 0
149
+ return estimateTokensBatch(texts)
150
+ }
151
+
152
+ export function countAllMessageTokens(msg: WithParts): number {
153
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
154
+ const texts: string[] = []
155
+ for (const part of parts) {
156
+ if (part.type === "text") {
157
+ texts.push(part.text)
158
+ } else {
159
+ texts.push(...extractToolContent(part))
160
+ }
161
+ }
162
+ if (texts.length === 0) return 0
163
+ return estimateTokensBatch(texts)
164
+ }
@@ -0,0 +1,237 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+
3
+ import type { analyzeContextTokens } from "../commands/context"
4
+ import { formatTokenCount } from "../ui/utils"
5
+ import { formatDuration, formatRatio } from "./format"
6
+ import { ActionRow, Card, DcpFrame, Metric, Progress, PromptRow, StatusPill } from "./ui"
7
+ import type { StatsReport, ViewApi } from "./types"
8
+
9
+ export function StatusDialog(props: {
10
+ api: ViewApi
11
+ title: string
12
+ eyebrow: string
13
+ message: string
14
+ }) {
15
+ return (
16
+ <DcpFrame api={props.api} title={props.title} eyebrow={props.eyebrow}>
17
+ <box paddingTop={1} paddingBottom={1}>
18
+ <text fg={props.api.theme.current.textMuted}>{props.message}</text>
19
+ </box>
20
+ </DcpFrame>
21
+ )
22
+ }
23
+
24
+ export function ContextDialog(props: {
25
+ api: ViewApi
26
+ breakdown: ReturnType<typeof analyzeContextTokens>
27
+ onBack: () => void
28
+ }) {
29
+ const theme = props.api.theme.current
30
+ const breakdown = props.breakdown
31
+ const total = Math.max(0, breakdown.total)
32
+ const activePruned = breakdown.prunedToolCount + breakdown.prunedMessageCount
33
+
34
+ return (
35
+ <DcpFrame api={props.api} title="Context" eyebrow="DCP" onBack={props.onBack}>
36
+ <Card theme={theme} title="Current">
37
+ <Metric
38
+ theme={theme}
39
+ label="Total in context"
40
+ value={`~${formatTokenCount(total)}`}
41
+ hint="tokens"
42
+ />
43
+ <Metric
44
+ theme={theme}
45
+ label="Tools in context"
46
+ value={`${breakdown.toolsInContextCount}`}
47
+ />
48
+ <Metric theme={theme} label="Active pruned targets" value={`${activePruned}`} />
49
+ <Metric
50
+ theme={theme}
51
+ label="Tokens pruned"
52
+ value={`~${formatTokenCount(breakdown.prunedTokens)}`}
53
+ hint="tokens"
54
+ />
55
+ </Card>
56
+ <Card theme={theme} title="Breakdown">
57
+ <Progress
58
+ theme={theme}
59
+ label="System"
60
+ value={breakdown.system}
61
+ total={total}
62
+ color="primary"
63
+ detail={`~${formatTokenCount(breakdown.system)} tokens`}
64
+ />
65
+ <Progress
66
+ theme={theme}
67
+ label="User"
68
+ value={breakdown.user}
69
+ total={total}
70
+ color="primary"
71
+ detail={`~${formatTokenCount(breakdown.user)} tokens`}
72
+ />
73
+ <Progress
74
+ theme={theme}
75
+ label="Assistant"
76
+ value={breakdown.assistant}
77
+ total={total}
78
+ color="primary"
79
+ detail={`~${formatTokenCount(breakdown.assistant)} tokens`}
80
+ />
81
+ <Progress
82
+ theme={theme}
83
+ label={`Tools (${breakdown.toolsInContextCount})`}
84
+ value={breakdown.tools}
85
+ total={total}
86
+ color="primary"
87
+ detail={`~${formatTokenCount(breakdown.tools)} tokens`}
88
+ />
89
+ </Card>
90
+ </DcpFrame>
91
+ )
92
+ }
93
+
94
+ export function StatsDialog(props: { api: ViewApi; report: StatsReport; onBack: () => void }) {
95
+ const theme = props.api.theme.current
96
+ const ratio = formatRatio(props.report.sessionTokens, props.report.sessionSummaryTokens)
97
+ return (
98
+ <DcpFrame api={props.api} title="Stats" eyebrow="DCP" onBack={props.onBack}>
99
+ <Card theme={theme} title="Session">
100
+ <Metric
101
+ theme={theme}
102
+ label="Tokens saved"
103
+ value={`~${formatTokenCount(props.report.sessionTokens)}`}
104
+ hint="tokens"
105
+ />
106
+ <Metric
107
+ theme={theme}
108
+ label="Summary size"
109
+ value={`~${formatTokenCount(props.report.sessionSummaryTokens)}`}
110
+ hint="tokens"
111
+ />
112
+ <Metric theme={theme} label="Compression ratio" value={ratio} />
113
+ <Metric
114
+ theme={theme}
115
+ label="Compression time"
116
+ value={formatDuration(props.report.sessionDurationMs)}
117
+ />
118
+ <Metric theme={theme} label="Tools pruned" value={`${props.report.sessionTools}`} />
119
+ <Metric
120
+ theme={theme}
121
+ label="Messages pruned"
122
+ value={`${props.report.sessionMessages}`}
123
+ />
124
+ </Card>
125
+ <Card theme={theme} title="All time">
126
+ <Metric
127
+ theme={theme}
128
+ label="Tokens saved"
129
+ value={`~${formatTokenCount(props.report.allTime.totalTokens)}`}
130
+ hint="tokens"
131
+ />
132
+ <Metric
133
+ theme={theme}
134
+ label="Tools pruned"
135
+ value={`${props.report.allTime.totalTools}`}
136
+ />
137
+ <Metric
138
+ theme={theme}
139
+ label="Messages pruned"
140
+ value={`${props.report.allTime.totalMessages}`}
141
+ />
142
+ <Metric
143
+ theme={theme}
144
+ label="Sessions with DCP history"
145
+ value={`${props.report.allTime.sessionCount}`}
146
+ />
147
+ </Card>
148
+ </DcpFrame>
149
+ )
150
+ }
151
+
152
+ export function PanelDialog(props: {
153
+ api: ViewApi
154
+ manualMode: boolean
155
+ canCompress: boolean
156
+ blockedReason?: string
157
+ onContext: () => void
158
+ onStats: () => void
159
+ onManual: (enabled: boolean) => void
160
+ }) {
161
+ const theme = props.api.theme.current
162
+ const canCompress = props.canCompress
163
+ return (
164
+ <DcpFrame api={props.api} eyebrow="DCP">
165
+ <Card theme={theme} title="Views">
166
+ <box flexDirection="column" gap={1}>
167
+ <ActionRow
168
+ theme={theme}
169
+ title="Context"
170
+ detail="Token usage"
171
+ onClick={props.onContext}
172
+ />
173
+ <ActionRow
174
+ theme={theme}
175
+ title="Stats"
176
+ detail="Savings"
177
+ onClick={props.onStats}
178
+ />
179
+ </box>
180
+ </Card>
181
+ <Card theme={theme} title="Prompt">
182
+ {canCompress ? (
183
+ <PromptRow
184
+ theme={theme}
185
+ command="/dcp-compress [focus]"
186
+ description="Ask the model to compress"
187
+ accent="primary"
188
+ />
189
+ ) : (
190
+ <text fg={theme.textMuted}>
191
+ {props.blockedReason ?? "Compression is denied by permissions."}
192
+ </text>
193
+ )}
194
+ </Card>
195
+ <Card theme={theme} title="Session State">
196
+ <ManualModeToggle
197
+ api={props.api}
198
+ enabled={props.manualMode}
199
+ onToggle={props.onManual}
200
+ />
201
+ <StatusPill
202
+ theme={theme}
203
+ label="Compression command"
204
+ value={canCompress ? "enabled" : "disabled"}
205
+ accent={canCompress ? "success" : "warning"}
206
+ />
207
+ </Card>
208
+ </DcpFrame>
209
+ )
210
+ }
211
+
212
+ function ManualModeToggle(props: {
213
+ api: ViewApi
214
+ enabled: boolean
215
+ onToggle: (enabled: boolean) => void
216
+ }) {
217
+ const theme = props.api.theme.current
218
+ const enabled = props.enabled
219
+ const track = enabled ? theme.success : theme.error
220
+ return (
221
+ <box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
222
+ <box width={22}>
223
+ <text fg={theme.primary}>
224
+ <b>Manual mode</b>
225
+ </text>
226
+ </box>
227
+ <box
228
+ backgroundColor={track}
229
+ paddingLeft={1}
230
+ paddingRight={1}
231
+ onMouseUp={() => props.onToggle(!enabled)}
232
+ >
233
+ <text fg={theme.background}>{enabled ? " ■" : "■ "}</text>
234
+ </box>
235
+ </box>
236
+ )
237
+ }
@@ -0,0 +1,25 @@
1
+ export function formatDuration(ms: number): string {
2
+ const safeMs = Math.max(0, Math.round(ms))
3
+ if (safeMs < 1000) return `${safeMs} ms`
4
+
5
+ const totalSeconds = safeMs / 1000
6
+ if (totalSeconds < 60) return `${totalSeconds.toFixed(1)} s`
7
+
8
+ const wholeSeconds = Math.floor(totalSeconds)
9
+ const hours = Math.floor(wholeSeconds / 3600)
10
+ const minutes = Math.floor((wholeSeconds % 3600) / 60)
11
+ const seconds = wholeSeconds % 60
12
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`
13
+ return `${minutes}m ${seconds}s`
14
+ }
15
+
16
+ export function formatRatio(inputTokens: number, outputTokens: number): string {
17
+ if (inputTokens <= 0) return "0:1"
18
+ if (outputTokens <= 0) return "∞:1"
19
+ return `${Math.max(1, Math.round(inputTokens / outputTokens))}:1`
20
+ }
21
+
22
+ export function pct(value: number, total: number): string {
23
+ if (total <= 0) return "0.0%"
24
+ return `${((value / total) * 100).toFixed(1)}%`
25
+ }
@@ -0,0 +1,25 @@
1
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
2
+ import type { buildStatsReport } from "../commands/stats"
3
+
4
+ export type TuiApi = Parameters<NonNullable<TuiPluginModule["tui"]>>[0]
5
+ export type Theme = Pick<
6
+ TuiApi["theme"]["current"],
7
+ | "primary"
8
+ | "accent"
9
+ | "text"
10
+ | "textMuted"
11
+ | "background"
12
+ | "backgroundElement"
13
+ | "borderSubtle"
14
+ | "selectedListItemText"
15
+ | "success"
16
+ | "warning"
17
+ | "error"
18
+ >
19
+ export type ThemeColor = keyof Theme
20
+ export type ViewApi = {
21
+ theme: { readonly current: Theme }
22
+ renderer: Pick<TuiApi["renderer"], "height" | "on" | "off">
23
+ ui: { dialog: { clear(): void } }
24
+ }
25
+ export type StatsReport = Awaited<ReturnType<typeof buildStatsReport>>