@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,252 @@
1
+ import type { Message, ContentPart, ToolResultPart } from "@opencode/ai/schema/messages"
2
+ import type { Plugin } from "@opencode/plugin"
3
+ import type { WithParts } from "../state"
4
+
5
+ type History = Awaited<ReturnType<Plugin.Context["session"]["context"]>>
6
+ type Session = Awaited<ReturnType<Plugin.Context["session"]["get"]>>
7
+ type Part = WithParts["parts"][number]
8
+
9
+ const emptyUsage = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
10
+
11
+ function info(id: string, role: string, session: Session, entry?: History[number]) {
12
+ const model = entry?.type === "assistant" ? entry.model : session.model
13
+ return {
14
+ id,
15
+ role,
16
+ sessionID: session.id,
17
+ time: { created: entry?.time.created ?? 0 },
18
+ agent: entry?.type === "assistant" ? entry.agent : session.agent,
19
+ model: { providerID: model?.providerID, modelID: model?.id, variant: model?.variant },
20
+ providerID: model?.providerID,
21
+ modelID: model?.id,
22
+ tokens: entry?.type === "assistant" ? (entry.tokens ?? emptyUsage) : emptyUsage,
23
+ summary: entry?.type === "compaction" && entry.status === "completed",
24
+ } as unknown as WithParts["info"]
25
+ }
26
+
27
+ // Bookkeeping projections are never sent to a provider. Native messages remain
28
+ // authoritative for reasoning, media, checkpoints, and provider metadata.
29
+ export function history(entries: History, session: Session): WithParts[] {
30
+ return entries.flatMap((entry): WithParts[] => {
31
+ const base = { sessionID: session.id, messageID: entry.id }
32
+ if (entry.type === "assistant") {
33
+ const parts = entry.content.map((part, index) => {
34
+ if (part.type !== "tool")
35
+ return { ...base, id: `${entry.id}:${index}`, type: part.type, text: part.text }
36
+ const state = part.state
37
+ return {
38
+ ...base,
39
+ id: part.id,
40
+ type: "tool",
41
+ callID: part.id,
42
+ tool: part.name,
43
+ state: {
44
+ ...state,
45
+ input: structuredClone(state.input),
46
+ ...(state.status === "completed" ? { output: output(state.content) } : {}),
47
+ },
48
+ }
49
+ })
50
+ return [
51
+ {
52
+ info: info(entry.id, "assistant", session, entry),
53
+ parts: [{ ...base, id: `${entry.id}:step`, type: "step-start" }, ...parts],
54
+ } as WithParts,
55
+ ]
56
+ }
57
+ let text: string | undefined
58
+ if (entry.type === "user")
59
+ text = [
60
+ ...(entry.skills ?? []).flatMap((skill) =>
61
+ skill.text === undefined ? [] : [skill.text],
62
+ ),
63
+ entry.text,
64
+ ].join("\n\n")
65
+ if (entry.type === "synthetic" || entry.type === "skill") text = entry.text
66
+ if (entry.type === "location-switched")
67
+ text = `The working directory has been changed to ${entry.location.directory}.`
68
+ if (entry.type === "shell" && entry.metadata?.background !== true)
69
+ text = `The following shell command was executed by the user:\n\nCommand:\n${entry.command}\n\nOutput:\n${entry.output?.output ?? ""}`
70
+ if (text !== undefined) {
71
+ return [
72
+ {
73
+ info: info(entry.id, "user", session, entry),
74
+ parts: [{ ...base, id: `${entry.id}:text`, type: "text", text }],
75
+ } as WithParts,
76
+ ]
77
+ }
78
+ if (entry.type === "compaction" && entry.status === "completed") {
79
+ return [{ info: info(entry.id, "assistant", session, entry), parts: [] } as WithParts]
80
+ }
81
+ return []
82
+ })
83
+ }
84
+
85
+ function output(content: ReadonlyArray<{ type: string; text?: string }>): unknown {
86
+ return content.length === 1 && content[0]?.type === "text"
87
+ ? content[0].text
88
+ : structuredClone(content)
89
+ }
90
+
91
+ export function project(native: Message[], entries: History, session: Session) {
92
+ const byID = new Map(entries.map((entry) => [entry.id, entry]))
93
+ const results = new Map<string, ToolResultPart>()
94
+ const owners = new Map<string, string>()
95
+ const links = new Map<ContentPart, Part>()
96
+ const originals = new Map<string, WithParts>()
97
+ const messages: WithParts[] = []
98
+ for (const message of native)
99
+ for (const part of message.content) {
100
+ if (part.type === "tool-result") results.set(part.id, part)
101
+ if (part.type === "tool-call" && message.id) owners.set(part.id, message.id)
102
+ }
103
+ for (const message of native) {
104
+ if (!message.id || !["user", "assistant"].includes(message.role)) continue
105
+ const entry = byID.get(message.id)
106
+ if (!entry || entry.type === "compaction") continue
107
+ const base = { sessionID: session.id, messageID: message.id }
108
+ const parts: Part[] = []
109
+ if (message.role === "assistant")
110
+ parts.push({ ...base, id: `${message.id}:step`, type: "step-start" })
111
+ for (const [index, part] of message.content.entries()) {
112
+ let projected: Part | undefined
113
+ if (part.type === "text")
114
+ projected = { ...base, id: `${message.id}:${index}`, type: "text", text: part.text }
115
+ if (part.type === "media") {
116
+ // Tolerate both media shapes: older hosts expose mediaType/data
117
+ // directly, newer ones nest them under `media`.
118
+ const media: any = (part as any).media
119
+ const legacy: any = part as any
120
+ const mime = media?.mediaType ?? legacy.mediaType ?? ""
121
+ const source = media?.source
122
+ const url =
123
+ typeof legacy.data === "string"
124
+ ? legacy.data
125
+ : typeof source?.data === "string"
126
+ ? source.data
127
+ : typeof source?.url === "string"
128
+ ? source.url
129
+ : ""
130
+ projected = {
131
+ ...base,
132
+ id: `${message.id}:${index}`,
133
+ type: "file",
134
+ mime,
135
+ url,
136
+ }
137
+ }
138
+ if (part.type === "tool-call") {
139
+ const result = results.get(part.id)
140
+ const entry = byID.get(message.id)
141
+ const durable =
142
+ entry?.type === "assistant"
143
+ ? entry.content.find((item) => item.type === "tool" && item.id === part.id)
144
+ : undefined
145
+ projected = {
146
+ ...base,
147
+ id: part.id,
148
+ type: "tool",
149
+ callID: part.id,
150
+ tool: part.name,
151
+ state: {
152
+ status: !result
153
+ ? "running"
154
+ : result.result.type === "error"
155
+ ? "error"
156
+ : "completed",
157
+ input: structuredClone(part.input),
158
+ ...(result?.result.type === "error"
159
+ ? { error: result.result.value }
160
+ : {
161
+ output: structuredClone(
162
+ result?.result.type === "text"
163
+ ? result.result.value
164
+ : result?.result,
165
+ ),
166
+ }),
167
+ metadata:
168
+ durable?.type === "tool" && "metadata" in durable.state
169
+ ? durable.state.metadata
170
+ : {},
171
+ },
172
+ } as Part
173
+ if (result) links.set(result, projected)
174
+ }
175
+ if (projected) {
176
+ parts.push(projected)
177
+ links.set(part, projected)
178
+ }
179
+ }
180
+ const projected = {
181
+ info: info(message.id, message.role, session, byID.get(message.id)),
182
+ parts,
183
+ } as WithParts
184
+ messages.push(projected)
185
+ originals.set(message.id, projected)
186
+ }
187
+
188
+ function restore(): Message[] {
189
+ const retained = new Set(messages.map((message) => message.info.id))
190
+ const linked = new Set(links.values())
191
+ const output: Message[] = []
192
+ let next = 0
193
+ const synthetic = () => {
194
+ while (next < messages.length && !originals.has(messages[next]!.info.id)) {
195
+ const message = messages[next++]!
196
+ output.push({
197
+ id: message.info.id,
198
+ role: "user",
199
+ content: message.parts
200
+ .filter((part) => part.type === "text")
201
+ .map((part) => ({ type: "text", text: part.text })),
202
+ })
203
+ }
204
+ }
205
+ for (const message of native) {
206
+ const projected = message.id ? originals.get(message.id) : undefined
207
+ if (projected) {
208
+ synthetic()
209
+ if (!retained.has(message.id!)) continue
210
+ next++
211
+ }
212
+ const content: ContentPart[] = []
213
+ const added =
214
+ projected?.parts.flatMap((part) =>
215
+ part.type === "text" && !linked.has(part)
216
+ ? [{ type: "text" as const, text: part.text }]
217
+ : [],
218
+ ) ?? []
219
+ for (const part of message.content) {
220
+ if (part.type === "tool-call") content.push(...added.splice(0))
221
+ if (
222
+ part.type === "tool-result" &&
223
+ originals.has(owners.get(part.id)!) &&
224
+ !retained.has(owners.get(part.id)!)
225
+ )
226
+ continue
227
+ const edit = links.get(part)
228
+ if (part.type === "text" && edit?.type === "text")
229
+ content.push({ ...part, text: edit.text })
230
+ else if (part.type === "tool-call" && edit?.type === "tool")
231
+ content.push({ ...part, input: edit.state.input })
232
+ else if (
233
+ part.type === "tool-result" &&
234
+ edit?.type === "tool" &&
235
+ edit.state.status === "completed" &&
236
+ typeof edit.state.output === "string" &&
237
+ edit.state.output !== part.result.value
238
+ ) {
239
+ content.push({ ...part, result: { type: "text", value: edit.state.output } })
240
+ } else content.push(part)
241
+ }
242
+ content.push(...added)
243
+ if (content.length || message.content.length === 0) output.push({ ...message, content })
244
+ }
245
+ synthetic()
246
+ return output
247
+ }
248
+ // A native checkpoint can replace every user message. Summary construction
249
+ // still needs session/agent/model fields, without projecting that checkpoint.
250
+ const summaryBase: WithParts = { info: info("msg_dcp_base", "user", session), parts: [] }
251
+ return { messages, restore, summaryBase }
252
+ }
package/lib/v2/rpc.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
3
+ const z = tool.schema
4
+ const session = z.object({ sessionID: z.string() })
5
+ const compressionUsage = z.object({
6
+ calls: z.number(),
7
+ providerCalls: z.number(),
8
+ estimatedCalls: z.number(),
9
+ inputTokens: z.number(),
10
+ outputTokens: z.number(),
11
+ cacheReadTokens: z.number(),
12
+ cacheWriteTokens: z.number(),
13
+ reasoningTokens: z.number(),
14
+ })
15
+ const stats = z.object({
16
+ sessionTokens: z.number(),
17
+ sessionSummaryTokens: z.number(),
18
+ sessionDurationMs: z.number(),
19
+ sessionTools: z.number(),
20
+ sessionMessages: z.number(),
21
+ sessionCompressionUsage: compressionUsage,
22
+ allTime: z.object({
23
+ totalTokens: z.number(),
24
+ totalTools: z.number(),
25
+ totalMessages: z.number(),
26
+ sessionCount: z.number(),
27
+ compressionUsage,
28
+ }),
29
+ })
30
+ const context = z.object({
31
+ system: z.number(),
32
+ user: z.number(),
33
+ assistant: z.number(),
34
+ tools: z.number(),
35
+ toolCount: z.number(),
36
+ toolsInContextCount: z.number(),
37
+ prunedTokens: z.number(),
38
+ prunedToolCount: z.number(),
39
+ prunedMessageCount: z.number(),
40
+ total: z.number(),
41
+ })
42
+
43
+ export const rpc = {
44
+ id: "dcp-rc",
45
+ methods: {
46
+ status: { input: z.object({}), output: z.object({ enabled: z.boolean() }) },
47
+ snapshot: {
48
+ input: session,
49
+ output: z.object({
50
+ manualMode: z.boolean(),
51
+ canCompress: z.boolean(),
52
+ blockedReason: z.string().optional(),
53
+ context,
54
+ stats,
55
+ }),
56
+ },
57
+ manual: { input: session.extend({ enabled: z.boolean() }), output: z.object({}) },
58
+ },
59
+ events: {},
60
+ } as const
@@ -0,0 +1,67 @@
1
+ import type { Theme } from "../tui/types"
2
+
3
+ type Color = Theme["text"]
4
+ type Feedback = "success" | "warning" | "error"
5
+ type Action = "primary" | "secondary"
6
+
7
+ type CurrentTheme = {
8
+ surface(name: "dialog"): {
9
+ text: {
10
+ base: Color
11
+ muted: Color
12
+ action: Record<Action, { base: Color }>
13
+ feedback: Record<Feedback, { base: Color }>
14
+ }
15
+ background: { base: Color; raised: { base: Color } }
16
+ border: { base: Color }
17
+ }
18
+ }
19
+
20
+ type EarlierTheme = {
21
+ contextual: {
22
+ overlay: {
23
+ text: {
24
+ default: Color
25
+ subdued: Color
26
+ action: Record<Action, { default: Color }>
27
+ feedback: Record<Feedback, { default: Color }>
28
+ }
29
+ background: { default: Color; surface: { offset: Color } }
30
+ border: { default: Color }
31
+ }
32
+ }
33
+ }
34
+
35
+ export function panelTheme(source: CurrentTheme | EarlierTheme): Theme {
36
+ if ("surface" in source) {
37
+ const theme = source.surface("dialog")
38
+ return {
39
+ primary: theme.text.action.primary.base,
40
+ accent: theme.text.action.secondary.base,
41
+ text: theme.text.base,
42
+ textMuted: theme.text.muted,
43
+ background: theme.background.base,
44
+ backgroundElement: theme.background.raised.base,
45
+ borderSubtle: theme.border.base,
46
+ selectedListItemText: theme.background.base,
47
+ success: theme.text.feedback.success.base,
48
+ warning: theme.text.feedback.warning.base,
49
+ error: theme.text.feedback.error.base,
50
+ }
51
+ }
52
+
53
+ const theme = source.contextual.overlay
54
+ return {
55
+ primary: theme.text.action.primary.default,
56
+ accent: theme.text.action.secondary.default,
57
+ text: theme.text.default,
58
+ textMuted: theme.text.subdued,
59
+ background: theme.background.default,
60
+ backgroundElement: theme.background.surface.offset,
61
+ borderSubtle: theme.border.default,
62
+ selectedListItemText: theme.background.default,
63
+ success: theme.text.feedback.success.default,
64
+ warning: theme.text.feedback.warning.default,
65
+ error: theme.text.feedback.error.default,
66
+ }
67
+ }
package/lib/v2/tui.tsx ADDED
@@ -0,0 +1,115 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Plugin } from "@opencode/plugin/tui"
3
+ import { ContextDialog, PanelDialog, StatsDialog, StatusDialog } from "../tui/dialogs"
4
+ import type { ViewApi } from "../tui/types"
5
+ import { rpc } from "./rpc"
6
+ import { panelTheme } from "./theme"
7
+
8
+ export async function setup(ctx: Plugin.Context) {
9
+ const client = ctx.client.rpc(rpc)
10
+ const options = () => ({ location: ctx.location ?? ctx.data.location.default() })
11
+ if (!(await client.status({}, options())).enabled) return
12
+ const api: ViewApi = {
13
+ renderer: ctx.renderer,
14
+ theme: {
15
+ get current() {
16
+ return panelTheme(ctx.theme)
17
+ },
18
+ },
19
+ ui: { dialog: { clear: () => ctx.ui.dialog.clear() } },
20
+ }
21
+ function show(render: Parameters<typeof ctx.ui.dialog.show>[0]) {
22
+ ctx.ui.dialog.set({ size: "xlarge" })
23
+ ctx.ui.dialog.show(render)
24
+ }
25
+ async function open(page: "panel" | "context" | "stats" = "panel") {
26
+ const route = ctx.ui.router.current()
27
+ if (route.type !== "session") {
28
+ show(() => (
29
+ <StatusDialog
30
+ api={api}
31
+ title="DCP"
32
+ eyebrow="No session"
33
+ message="Open a session first."
34
+ />
35
+ ))
36
+ return
37
+ }
38
+ const sessionID = route.sessionID
39
+ try {
40
+ const data = await client.snapshot({ sessionID }, options())
41
+ const back = () => {
42
+ void open()
43
+ }
44
+ if (page === "context")
45
+ show(() => <ContextDialog api={api} breakdown={data.context} onBack={back} />)
46
+ else if (page === "stats")
47
+ show(() => <StatsDialog api={api} report={data.stats} onBack={back} />)
48
+ else
49
+ show(() => (
50
+ <PanelDialog
51
+ api={api}
52
+ manualMode={data.manualMode}
53
+ canCompress={data.canCompress}
54
+ blockedReason={data.blockedReason}
55
+ onContext={() => {
56
+ void open("context")
57
+ }}
58
+ onStats={() => {
59
+ void open("stats")
60
+ }}
61
+ onManual={(enabled) => {
62
+ void client
63
+ .manual({ sessionID, enabled }, options())
64
+ .then(back)
65
+ .catch(error)
66
+ }}
67
+ />
68
+ ))
69
+ } catch (cause) {
70
+ error(cause)
71
+ }
72
+ }
73
+ function error(cause: unknown) {
74
+ const message =
75
+ cause instanceof Error
76
+ ? cause.message
77
+ : typeof cause === "object" && cause && "message" in cause
78
+ ? String(cause.message)
79
+ : String(cause)
80
+ show(() => <StatusDialog api={api} title="DCP" eyebrow="DCP Error" message={message} />)
81
+ }
82
+ ctx.ui.slot({
83
+ append: "app",
84
+ render() {
85
+ ctx.keymap.layer(() => ({
86
+ mode: "global",
87
+ commands: [
88
+ {
89
+ id: "dcp.panel",
90
+ title: "DCP",
91
+ description: "Open DCP panel",
92
+ group: "DCP",
93
+ palette: true,
94
+ slash: { name: "dcp", arguments: true },
95
+ run: async (input) => {
96
+ if (!input?.trim()) return open()
97
+ const route = ctx.ui.router.current()
98
+ if (route.type !== "session") return open()
99
+ try {
100
+ await ctx.client.session.command({
101
+ sessionID: route.sessionID,
102
+ name: "dcp",
103
+ text: input,
104
+ })
105
+ } catch (cause) {
106
+ error(cause)
107
+ }
108
+ },
109
+ },
110
+ ],
111
+ }))
112
+ return null
113
+ },
114
+ })
115
+ }