@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.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 (215) 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 +8129 -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 +27 -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 +68 -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 +64 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress/usage.d.ts +45 -0
  44. package/dist/lib/compress/usage.d.ts.map +1 -0
  45. package/dist/lib/compress-permission.d.ts +4 -0
  46. package/dist/lib/compress-permission.d.ts.map +1 -0
  47. package/dist/lib/config.d.ts +83 -0
  48. package/dist/lib/config.d.ts.map +1 -0
  49. package/dist/lib/hooks.d.ts +22 -0
  50. package/dist/lib/hooks.d.ts.map +1 -0
  51. package/dist/lib/logger.d.ts +31 -0
  52. package/dist/lib/logger.d.ts.map +1 -0
  53. package/dist/lib/message-ids.d.ts +20 -0
  54. package/dist/lib/message-ids.d.ts.map +1 -0
  55. package/dist/lib/messages/index.d.ts +8 -0
  56. package/dist/lib/messages/index.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/inject.d.ts +7 -0
  58. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  60. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  61. package/dist/lib/messages/inject/utils.d.ts +23 -0
  62. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  63. package/dist/lib/messages/prune.d.ts +4 -0
  64. package/dist/lib/messages/prune.d.ts.map +1 -0
  65. package/dist/lib/messages/query.d.ts +7 -0
  66. package/dist/lib/messages/query.d.ts.map +1 -0
  67. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  68. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  69. package/dist/lib/messages/shape.d.ts +5 -0
  70. package/dist/lib/messages/shape.d.ts.map +1 -0
  71. package/dist/lib/messages/sync.d.ts +4 -0
  72. package/dist/lib/messages/sync.d.ts.map +1 -0
  73. package/dist/lib/messages/utils.d.ts +27 -0
  74. package/dist/lib/messages/utils.d.ts.map +1 -0
  75. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  76. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  77. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  78. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  82. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  83. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  84. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  85. package/dist/lib/prompts/index.d.ts +4 -0
  86. package/dist/lib/prompts/index.d.ts.map +1 -0
  87. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  88. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  89. package/dist/lib/prompts/store.d.ts +26 -0
  90. package/dist/lib/prompts/store.d.ts.map +1 -0
  91. package/dist/lib/prompts/system.d.ts +4 -0
  92. package/dist/lib/prompts/system.d.ts.map +1 -0
  93. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  94. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  95. package/dist/lib/protected-patterns.d.ts +6 -0
  96. package/dist/lib/protected-patterns.d.ts.map +1 -0
  97. package/dist/lib/state/index.d.ts +5 -0
  98. package/dist/lib/state/index.d.ts.map +1 -0
  99. package/dist/lib/state/persistence.d.ts +47 -0
  100. package/dist/lib/state/persistence.d.ts.map +1 -0
  101. package/dist/lib/state/state.d.ts +9 -0
  102. package/dist/lib/state/state.d.ts.map +1 -0
  103. package/dist/lib/state/tool-cache.d.ts +13 -0
  104. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  105. package/dist/lib/state/types.d.ts +105 -0
  106. package/dist/lib/state/types.d.ts.map +1 -0
  107. package/dist/lib/state/utils.d.ts +22 -0
  108. package/dist/lib/state/utils.d.ts.map +1 -0
  109. package/dist/lib/strategies/deduplication.d.ts +10 -0
  110. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  111. package/dist/lib/strategies/index.d.ts +3 -0
  112. package/dist/lib/strategies/index.d.ts.map +1 -0
  113. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  114. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  115. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  116. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  117. package/dist/lib/token-utils.d.ts +19 -0
  118. package/dist/lib/token-utils.d.ts.map +1 -0
  119. package/dist/lib/tui/dialogs.d.ts +29 -0
  120. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  121. package/dist/lib/tui/format.d.ts +4 -0
  122. package/dist/lib/tui/format.d.ts.map +1 -0
  123. package/dist/lib/tui/types.d.ts +18 -0
  124. package/dist/lib/tui/types.d.ts.map +1 -0
  125. package/dist/lib/tui/ui.d.ts +48 -0
  126. package/dist/lib/tui/ui.d.ts.map +1 -0
  127. package/dist/lib/ui/notification.d.ts +17 -0
  128. package/dist/lib/ui/notification.d.ts.map +1 -0
  129. package/dist/lib/ui/utils.d.ts +10 -0
  130. package/dist/lib/ui/utils.d.ts.map +1 -0
  131. package/dist/lib/v2/index.d.ts +5 -0
  132. package/dist/lib/v2/index.d.ts.map +1 -0
  133. package/dist/lib/v2/messages.d.ts +13 -0
  134. package/dist/lib/v2/messages.d.ts.map +1 -0
  135. package/dist/lib/v2/rpc.d.ts +75 -0
  136. package/dist/lib/v2/rpc.d.ts.map +1 -0
  137. package/dist/lib/v2/theme.d.ts +55 -0
  138. package/dist/lib/v2/theme.d.ts.map +1 -0
  139. package/dist/lib/v2/tui.d.ts +4 -0
  140. package/dist/lib/v2/tui.d.ts.map +1 -0
  141. package/dist/lib/v2/usage.d.ts +44 -0
  142. package/dist/lib/v2/usage.d.ts.map +1 -0
  143. package/dist/tui.d.ts +7 -0
  144. package/dist/tui.d.ts.map +1 -0
  145. package/lib/commands/compression-targets.ts +54 -0
  146. package/lib/commands/context.ts +305 -0
  147. package/lib/commands/decompress.ts +273 -0
  148. package/lib/commands/help.ts +76 -0
  149. package/lib/commands/index.ts +11 -0
  150. package/lib/commands/manual.ts +126 -0
  151. package/lib/commands/recompress.ts +222 -0
  152. package/lib/commands/stats.ts +180 -0
  153. package/lib/commands/sweep.ts +266 -0
  154. package/lib/compress/index.ts +2 -0
  155. package/lib/compress/pipeline.ts +116 -0
  156. package/lib/compress/protected-content.ts +163 -0
  157. package/lib/compress/rc.ts +494 -0
  158. package/lib/compress/search.ts +277 -0
  159. package/lib/compress/state.ts +272 -0
  160. package/lib/compress/summary.ts +320 -0
  161. package/lib/compress/timing.ts +77 -0
  162. package/lib/compress/types.ts +70 -0
  163. package/lib/compress/usage.ts +168 -0
  164. package/lib/compress-permission.ts +9 -0
  165. package/lib/config.ts +995 -0
  166. package/lib/hooks.ts +205 -0
  167. package/lib/logger.ts +226 -0
  168. package/lib/message-ids.ts +181 -0
  169. package/lib/messages/index.ts +7 -0
  170. package/lib/messages/inject/inject.ts +215 -0
  171. package/lib/messages/inject/subagent-results.ts +84 -0
  172. package/lib/messages/inject/utils.ts +304 -0
  173. package/lib/messages/prune.ts +238 -0
  174. package/lib/messages/query.ts +71 -0
  175. package/lib/messages/reasoning-strip.ts +40 -0
  176. package/lib/messages/shape.ts +50 -0
  177. package/lib/messages/sync.ts +124 -0
  178. package/lib/messages/utils.ts +190 -0
  179. package/lib/prompts/compress-rc.ts +25 -0
  180. package/lib/prompts/context-limit-nudge.ts +18 -0
  181. package/lib/prompts/extensions/nudge.ts +34 -0
  182. package/lib/prompts/extensions/system.ts +32 -0
  183. package/lib/prompts/extensions/tool.ts +21 -0
  184. package/lib/prompts/index.ts +29 -0
  185. package/lib/prompts/iteration-nudge.ts +6 -0
  186. package/lib/prompts/store.ts +452 -0
  187. package/lib/prompts/system.ts +41 -0
  188. package/lib/prompts/turn-nudge.ts +10 -0
  189. package/lib/protected-patterns.ts +159 -0
  190. package/lib/state/index.ts +4 -0
  191. package/lib/state/persistence.ts +321 -0
  192. package/lib/state/state.ts +214 -0
  193. package/lib/state/tool-cache.ts +99 -0
  194. package/lib/state/types.ts +117 -0
  195. package/lib/state/utils.ts +345 -0
  196. package/lib/strategies/deduplication.ts +125 -0
  197. package/lib/strategies/index.ts +2 -0
  198. package/lib/strategies/purge-errors.ts +86 -0
  199. package/lib/subagents/subagent-results.ts +75 -0
  200. package/lib/token-utils.ts +164 -0
  201. package/lib/tui/dialogs.tsx +261 -0
  202. package/lib/tui/format.ts +25 -0
  203. package/lib/tui/types.ts +25 -0
  204. package/lib/tui/ui.tsx +245 -0
  205. package/lib/ui/notification.ts +347 -0
  206. package/lib/ui/utils.ts +304 -0
  207. package/lib/v2/index.ts +448 -0
  208. package/lib/v2/messages.ts +252 -0
  209. package/lib/v2/rpc.ts +60 -0
  210. package/lib/v2/theme.ts +67 -0
  211. package/lib/v2/tui.tsx +115 -0
  212. package/lib/v2/usage.ts +423 -0
  213. package/package.json +91 -5
  214. package/server.js +1 -0
  215. 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,261 @@
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
+ <Metric
125
+ theme={theme}
126
+ label="Summary requests"
127
+ value={`${props.report.sessionCompressionUsage.calls}`}
128
+ hint={`${props.report.sessionCompressionUsage.providerCalls} provider · ${props.report.sessionCompressionUsage.estimatedCalls} estimated`}
129
+ />
130
+ <Metric
131
+ theme={theme}
132
+ label="Request in|out"
133
+ value={`~${formatTokenCount(props.report.sessionCompressionUsage.inputTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.outputTokens)}`}
134
+ hint="tokens"
135
+ />
136
+ <Metric
137
+ theme={theme}
138
+ label="Request cache r|w"
139
+ value={`~${formatTokenCount(props.report.sessionCompressionUsage.cacheReadTokens)} | ~${formatTokenCount(props.report.sessionCompressionUsage.cacheWriteTokens)}`}
140
+ hint={`reasoning ~${formatTokenCount(props.report.sessionCompressionUsage.reasoningTokens)}`}
141
+ />
142
+ </Card>
143
+ <Card theme={theme} title="All time">
144
+ <Metric
145
+ theme={theme}
146
+ label="Tokens saved"
147
+ value={`~${formatTokenCount(props.report.allTime.totalTokens)}`}
148
+ hint="tokens"
149
+ />
150
+ <Metric
151
+ theme={theme}
152
+ label="Tools pruned"
153
+ value={`${props.report.allTime.totalTools}`}
154
+ />
155
+ <Metric
156
+ theme={theme}
157
+ label="Messages pruned"
158
+ value={`${props.report.allTime.totalMessages}`}
159
+ />
160
+ <Metric
161
+ theme={theme}
162
+ label="Sessions with DCP history"
163
+ value={`${props.report.allTime.sessionCount}`}
164
+ />
165
+ <Metric
166
+ theme={theme}
167
+ label="Summary requests"
168
+ value={`${props.report.allTime.compressionUsage.calls}`}
169
+ hint={`~${formatTokenCount(props.report.allTime.compressionUsage.inputTokens)} in · ~${formatTokenCount(props.report.allTime.compressionUsage.outputTokens)} out`}
170
+ />
171
+ </Card>
172
+ </DcpFrame>
173
+ )
174
+ }
175
+
176
+ export function PanelDialog(props: {
177
+ api: ViewApi
178
+ manualMode: boolean
179
+ canCompress: boolean
180
+ blockedReason?: string
181
+ onContext: () => void
182
+ onStats: () => void
183
+ onManual: (enabled: boolean) => void
184
+ }) {
185
+ const theme = props.api.theme.current
186
+ const canCompress = props.canCompress
187
+ return (
188
+ <DcpFrame api={props.api} eyebrow="DCP">
189
+ <Card theme={theme} title="Views">
190
+ <box flexDirection="column" gap={1}>
191
+ <ActionRow
192
+ theme={theme}
193
+ title="Context"
194
+ detail="Token usage"
195
+ onClick={props.onContext}
196
+ />
197
+ <ActionRow
198
+ theme={theme}
199
+ title="Stats"
200
+ detail="Savings"
201
+ onClick={props.onStats}
202
+ />
203
+ </box>
204
+ </Card>
205
+ <Card theme={theme} title="Prompt">
206
+ {canCompress ? (
207
+ <PromptRow
208
+ theme={theme}
209
+ command="/dcp-compress [focus]"
210
+ description="Ask the model to compress"
211
+ accent="primary"
212
+ />
213
+ ) : (
214
+ <text fg={theme.textMuted}>
215
+ {props.blockedReason ?? "Compression is denied by permissions."}
216
+ </text>
217
+ )}
218
+ </Card>
219
+ <Card theme={theme} title="Session State">
220
+ <ManualModeToggle
221
+ api={props.api}
222
+ enabled={props.manualMode}
223
+ onToggle={props.onManual}
224
+ />
225
+ <StatusPill
226
+ theme={theme}
227
+ label="Compression command"
228
+ value={canCompress ? "enabled" : "disabled"}
229
+ accent={canCompress ? "success" : "warning"}
230
+ />
231
+ </Card>
232
+ </DcpFrame>
233
+ )
234
+ }
235
+
236
+ function ManualModeToggle(props: {
237
+ api: ViewApi
238
+ enabled: boolean
239
+ onToggle: (enabled: boolean) => void
240
+ }) {
241
+ const theme = props.api.theme.current
242
+ const enabled = props.enabled
243
+ const track = enabled ? theme.success : theme.error
244
+ return (
245
+ <box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
246
+ <box width={22}>
247
+ <text fg={theme.primary}>
248
+ <b>Manual mode</b>
249
+ </text>
250
+ </box>
251
+ <box
252
+ backgroundColor={track}
253
+ paddingLeft={1}
254
+ paddingRight={1}
255
+ onMouseUp={() => props.onToggle(!enabled)}
256
+ >
257
+ <text fg={theme.background}>{enabled ? " ■" : "■ "}</text>
258
+ </box>
259
+ </box>
260
+ )
261
+ }
@@ -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>>