@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,76 @@
1
+ /**
2
+ * DCP Help command handler.
3
+ * Shows available DCP commands and their descriptions.
4
+ */
5
+
6
+ import type { Logger } from "../logger"
7
+ import type { PluginConfig } from "../config"
8
+ import type { SessionState, WithParts } from "../state"
9
+ import { compressPermission } from "../compress-permission"
10
+ import { sendIgnoredMessage } from "../ui/notification"
11
+ import { getCurrentParams } from "../token-utils"
12
+
13
+ export interface HelpCommandContext {
14
+ client: any
15
+ state: SessionState
16
+ config: PluginConfig
17
+ logger: Logger
18
+ sessionId: string
19
+ messages: WithParts[]
20
+ }
21
+
22
+ const TUI_COMMANDS: [string, string][] = [
23
+ ["DCP Context", "Show token usage breakdown for current session"],
24
+ ["DCP Stats", "Show DCP pruning statistics"],
25
+ ["DCP Help", "Show this help in a modal"],
26
+ ]
27
+
28
+ const TOOL_COMMANDS: Record<string, [string, string]> = {
29
+ compress: ["/dcp-compress [focus]", "Trigger manual compress tool execution"],
30
+ decompress: ["/dcp decompress <n>", "Restore selected compression"],
31
+ recompress: ["/dcp recompress <n>", "Re-apply a user-decompressed compression"],
32
+ }
33
+
34
+ function getVisibleCommands(state: SessionState, config: PluginConfig): [string, string][] {
35
+ const commands = [...TUI_COMMANDS]
36
+
37
+ if (compressPermission(state, config) !== "deny") {
38
+ commands.push(TOOL_COMMANDS.compress)
39
+ }
40
+
41
+ return commands
42
+ }
43
+
44
+ export function formatHelpMessage(state: SessionState, config: PluginConfig): string {
45
+ const commands = getVisibleCommands(state, config)
46
+ const colWidth = Math.max(...commands.map(([cmd]) => cmd.length)) + 4
47
+ const lines: string[] = []
48
+
49
+ lines.push("╭─────────────────────────────────────────────────────────────────────────╮")
50
+ lines.push("│ DCP Commands │")
51
+ lines.push("╰─────────────────────────────────────────────────────────────────────────╯")
52
+ lines.push("")
53
+ lines.push(` ${"Manual mode:".padEnd(colWidth)}${state.manualMode ? "ON" : "OFF"}`)
54
+ lines.push("")
55
+ lines.push(" Open the command palette for DCP modal commands.")
56
+ lines.push(" Use /dcp-compress [focus] when you want DCP to ask the model to run compression.")
57
+ lines.push("")
58
+ for (const [cmd, desc] of commands) {
59
+ lines.push(` ${cmd.padEnd(colWidth)}${desc}`)
60
+ }
61
+ lines.push("")
62
+
63
+ return lines.join("\n")
64
+ }
65
+
66
+ export async function handleHelpCommand(ctx: HelpCommandContext): Promise<void> {
67
+ const { client, state, logger, sessionId, messages } = ctx
68
+
69
+ const { config } = ctx
70
+ const message = formatHelpMessage(state, config)
71
+
72
+ const params = getCurrentParams(state, messages, logger)
73
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
74
+
75
+ logger.info("Help command executed")
76
+ }
@@ -0,0 +1,11 @@
1
+ export { handleContextCommand } from "./context"
2
+ export { handleDecompressCommand } from "./decompress"
3
+ export { handleHelpCommand } from "./help"
4
+ export {
5
+ applyPendingManualTrigger,
6
+ handleManualToggleCommand,
7
+ handleManualTriggerCommand,
8
+ } from "./manual"
9
+ export { handleRecompressCommand } from "./recompress"
10
+ export { handleStatsCommand } from "./stats"
11
+ export { handleSweepCommand } from "./sweep"
@@ -0,0 +1,126 @@
1
+ /**
2
+ * DCP Manual mode command handler.
3
+ * Handles toggling manual mode and triggering individual tool executions.
4
+ *
5
+ * Usage:
6
+ * /dcp manual [on|off] - Toggle manual mode or set explicit state
7
+ * /dcp-compress [focus] - Trigger manual compress execution
8
+ */
9
+
10
+ import type { Logger } from "../logger"
11
+ import type { SessionState, WithParts } from "../state"
12
+ import type { PluginConfig } from "../config"
13
+ import { sendIgnoredMessage } from "../ui/notification"
14
+ import { saveManualModeSetting } from "../state/persistence"
15
+ import { getCurrentParams } from "../token-utils"
16
+ import { buildCompressedBlockGuidance } from "../prompts/extensions/nudge"
17
+ import { isIgnoredUserMessage } from "../messages/query"
18
+
19
+ const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp-compress to trigger context tools manually."
20
+
21
+ const MANUAL_MODE_OFF = "Manual mode is now OFF."
22
+
23
+ const COMPRESS_TRIGGER_PROMPT = [
24
+ "<compress triggered manually>",
25
+ "Manual mode trigger received. You must now use the compress tool.",
26
+ "Find the most significant completed conversation content that can be compressed into a high-fidelity technical summary.",
27
+ "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
28
+ "Return after compress with a brief explanation of what content was compressed.",
29
+ ].join("\n\n")
30
+
31
+ function getTriggerPrompt(
32
+ tool: "compress",
33
+ state: SessionState,
34
+ config: PluginConfig,
35
+ userFocus?: string,
36
+ ): string {
37
+ const base = COMPRESS_TRIGGER_PROMPT
38
+ const compressedBlockGuidance = buildCompressedBlockGuidance(state)
39
+
40
+ const sections = [base, compressedBlockGuidance]
41
+ if (userFocus && userFocus.trim().length > 0) {
42
+ sections.push(`Additional user focus:\n${userFocus.trim()}`)
43
+ }
44
+
45
+ return sections.join("\n\n")
46
+ }
47
+
48
+ export interface ManualCommandContext {
49
+ client: any
50
+ state: SessionState
51
+ config: PluginConfig
52
+ logger: Logger
53
+ sessionId: string
54
+ messages: WithParts[]
55
+ }
56
+
57
+ export async function handleManualToggleCommand(
58
+ ctx: ManualCommandContext,
59
+ modeArg?: string,
60
+ ): Promise<void> {
61
+ const { client, state, logger, sessionId, messages } = ctx
62
+
63
+ if (modeArg === "on") {
64
+ state.manualMode = "active"
65
+ } else if (modeArg === "off") {
66
+ state.manualMode = false
67
+ } else {
68
+ state.manualMode = state.manualMode ? false : "active"
69
+ }
70
+
71
+ const params = getCurrentParams(state, messages, logger)
72
+ await sendIgnoredMessage(
73
+ client,
74
+ sessionId,
75
+ state.manualMode ? MANUAL_MODE_ON : MANUAL_MODE_OFF,
76
+ params,
77
+ logger,
78
+ )
79
+ await saveManualModeSetting(sessionId, !!state.manualMode, logger)
80
+
81
+ logger.info("Manual mode toggled", { manualMode: state.manualMode })
82
+ }
83
+
84
+ export async function handleManualTriggerCommand(
85
+ ctx: ManualCommandContext,
86
+ tool: "compress",
87
+ userFocus?: string,
88
+ ): Promise<string | null> {
89
+ return getTriggerPrompt(tool, ctx.state, ctx.config, userFocus)
90
+ }
91
+
92
+ export function applyPendingManualTrigger(
93
+ state: SessionState,
94
+ messages: WithParts[],
95
+ logger: Logger,
96
+ ): void {
97
+ const pending = state.pendingManualTrigger
98
+ if (!pending) {
99
+ return
100
+ }
101
+
102
+ if (!state.sessionId || pending.sessionId !== state.sessionId) {
103
+ state.pendingManualTrigger = null
104
+ return
105
+ }
106
+
107
+ for (let i = messages.length - 1; i >= 0; i--) {
108
+ const msg = messages[i]
109
+ if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) {
110
+ continue
111
+ }
112
+
113
+ for (const part of msg.parts) {
114
+ if (part.type !== "text" || part.ignored || part.synthetic) {
115
+ continue
116
+ }
117
+
118
+ part.text = pending.prompt
119
+ state.pendingManualTrigger = null
120
+ logger.debug("Applied manual prompt", { sessionId: pending.sessionId })
121
+ return
122
+ }
123
+ }
124
+
125
+ state.pendingManualTrigger = null
126
+ }
@@ -0,0 +1,222 @@
1
+ import type { Logger } from "../logger"
2
+ import type { PruneMessagesState, SessionState, WithParts } from "../state"
3
+ import { syncCompressionBlocks } from "../messages"
4
+ import { parseBlockRef } from "../message-ids"
5
+ import { getCurrentParams } from "../token-utils"
6
+ import { saveSessionState } from "../state/persistence"
7
+ import { sendIgnoredMessage } from "../ui/notification"
8
+ import { formatTokenCount } from "../ui/utils"
9
+ import {
10
+ getRecompressibleCompressionTargets,
11
+ resolveCompressionTarget,
12
+ type CompressionTarget,
13
+ } from "./compression-targets"
14
+
15
+ export interface RecompressCommandContext {
16
+ client: any
17
+ state: SessionState
18
+ logger: Logger
19
+ sessionId: string
20
+ messages: WithParts[]
21
+ args: string[]
22
+ }
23
+
24
+ function parseBlockIdArg(arg: string): number | null {
25
+ const normalized = arg.trim().toLowerCase()
26
+ const blockRef = parseBlockRef(normalized) ?? parseBlockRef(normalized, "compact")
27
+ if (blockRef !== null) {
28
+ return blockRef
29
+ }
30
+
31
+ if (!/^[1-9]\d*$/.test(normalized)) {
32
+ return null
33
+ }
34
+
35
+ const parsed = Number.parseInt(normalized, 10)
36
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null
37
+ }
38
+
39
+ function snapshotActiveMessages(messagesState: PruneMessagesState): Set<string> {
40
+ const activeMessages = new Set<string>()
41
+ for (const [messageId, entry] of messagesState.byMessageId) {
42
+ if (entry.activeBlockIds.length > 0) {
43
+ activeMessages.add(messageId)
44
+ }
45
+ }
46
+ return activeMessages
47
+ }
48
+
49
+ function formatRecompressMessage(
50
+ target: CompressionTarget,
51
+ recompressedMessageCount: number,
52
+ recompressedTokens: number,
53
+ deactivatedBlockIds: number[],
54
+ ): string {
55
+ const lines: string[] = []
56
+
57
+ lines.push(`Re-applied compression ${target.displayId}.`)
58
+ if (target.runId !== target.displayId) {
59
+ lines.push(`Tool call label: Compression #${target.runId}.`)
60
+ }
61
+ if (deactivatedBlockIds.length > 0) {
62
+ const refs = deactivatedBlockIds.map((id) => String(id)).join(", ")
63
+ lines.push(`Also re-compressed nested compression(s): ${refs}.`)
64
+ }
65
+
66
+ if (recompressedMessageCount > 0) {
67
+ lines.push(
68
+ `Re-compressed ${recompressedMessageCount} message(s) (~${formatTokenCount(recompressedTokens)}).`,
69
+ )
70
+ } else {
71
+ lines.push("No messages were re-compressed.")
72
+ }
73
+
74
+ return lines.join("\n")
75
+ }
76
+
77
+ function formatAvailableBlocksMessage(availableTargets: CompressionTarget[]): string {
78
+ const lines: string[] = []
79
+
80
+ lines.push("Usage: /dcp recompress <n>")
81
+ lines.push("")
82
+
83
+ if (availableTargets.length === 0) {
84
+ lines.push("No user-decompressed blocks are available to re-compress.")
85
+ return lines.join("\n")
86
+ }
87
+
88
+ lines.push("Available user-decompressed compressions:")
89
+ const entries = availableTargets.map((target) => {
90
+ const topic = target.topic.replace(/\s+/g, " ").trim() || "(no topic)"
91
+ const label = `${target.displayId} (${formatTokenCount(target.compressedTokens)})`
92
+ const details = `Compression #${target.runId}`
93
+ return { label, topic: `${details} - ${topic}` }
94
+ })
95
+
96
+ const labelWidth = Math.max(...entries.map((entry) => entry.label.length)) + 4
97
+ for (const entry of entries) {
98
+ lines.push(` ${entry.label.padEnd(labelWidth)}${entry.topic}`)
99
+ }
100
+
101
+ return lines.join("\n")
102
+ }
103
+
104
+ export async function handleRecompressCommand(ctx: RecompressCommandContext): Promise<void> {
105
+ const { client, state, logger, sessionId, messages, args } = ctx
106
+
107
+ const params = getCurrentParams(state, messages, logger)
108
+ const targetArg = args[0]
109
+
110
+ if (args.length > 1) {
111
+ await sendIgnoredMessage(
112
+ client,
113
+ sessionId,
114
+ "Invalid arguments. Usage: /dcp recompress <n>",
115
+ params,
116
+ logger,
117
+ )
118
+ return
119
+ }
120
+
121
+ syncCompressionBlocks(state, logger, messages)
122
+ const messagesState = state.prune.messages
123
+ const availableMessageIds = new Set(messages.map((msg) => msg.info.id))
124
+
125
+ if (!targetArg) {
126
+ const availableTargets = getRecompressibleCompressionTargets(
127
+ messagesState,
128
+ availableMessageIds,
129
+ )
130
+ const message = formatAvailableBlocksMessage(availableTargets)
131
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
132
+ return
133
+ }
134
+
135
+ const targetBlockId = parseBlockIdArg(targetArg)
136
+ if (targetBlockId === null) {
137
+ await sendIgnoredMessage(
138
+ client,
139
+ sessionId,
140
+ `Please enter a compression number. Example: /dcp recompress 2`,
141
+ params,
142
+ logger,
143
+ )
144
+ return
145
+ }
146
+
147
+ const target = resolveCompressionTarget(messagesState, targetBlockId)
148
+ if (!target) {
149
+ await sendIgnoredMessage(
150
+ client,
151
+ sessionId,
152
+ `Compression ${targetBlockId} does not exist.`,
153
+ params,
154
+ logger,
155
+ )
156
+ return
157
+ }
158
+
159
+ if (target.blocks.some((block) => !availableMessageIds.has(block.compressMessageId))) {
160
+ await sendIgnoredMessage(
161
+ client,
162
+ sessionId,
163
+ `Compression ${target.displayId} can no longer be re-applied because its origin message is no longer in this session.`,
164
+ params,
165
+ logger,
166
+ )
167
+ return
168
+ }
169
+
170
+ if (!target.blocks.some((block) => block.deactivatedByUser)) {
171
+ const message = target.blocks.some((block) => block.active)
172
+ ? `Compression ${target.displayId} is already active.`
173
+ : `Compression ${target.displayId} is not user-decompressed.`
174
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
175
+ return
176
+ }
177
+
178
+ const activeMessagesBefore = snapshotActiveMessages(messagesState)
179
+ const activeBlockIdsBefore = new Set(messagesState.activeBlockIds)
180
+
181
+ for (const block of target.blocks) {
182
+ block.deactivatedByUser = false
183
+ block.deactivatedAt = undefined
184
+ block.deactivatedByBlockId = undefined
185
+ }
186
+
187
+ syncCompressionBlocks(state, logger, messages)
188
+
189
+ let recompressedMessageCount = 0
190
+ let recompressedTokens = 0
191
+ for (const [messageId, entry] of messagesState.byMessageId) {
192
+ const isActiveNow = entry.activeBlockIds.length > 0
193
+ if (isActiveNow && !activeMessagesBefore.has(messageId)) {
194
+ recompressedMessageCount++
195
+ recompressedTokens += entry.tokenCount
196
+ }
197
+ }
198
+
199
+ state.stats.totalPruneTokens += recompressedTokens
200
+
201
+ const deactivatedBlockIds = Array.from(activeBlockIdsBefore)
202
+ .filter((blockId) => !messagesState.activeBlockIds.has(blockId))
203
+ .sort((a, b) => a - b)
204
+
205
+ await saveSessionState(state, logger)
206
+
207
+ const message = formatRecompressMessage(
208
+ target,
209
+ recompressedMessageCount,
210
+ recompressedTokens,
211
+ deactivatedBlockIds,
212
+ )
213
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
214
+
215
+ logger.info("Recompress command completed", {
216
+ targetBlockId: target.displayId,
217
+ targetRunId: target.runId,
218
+ recompressedMessageCount,
219
+ recompressedTokens,
220
+ deactivatedBlockIds,
221
+ })
222
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * DCP Stats command handler.
3
+ * Shows pruning statistics for the current session and all-time totals.
4
+ */
5
+
6
+ import type { Logger } from "../logger"
7
+ import type { SessionState, WithParts } from "../state"
8
+ import { sendIgnoredMessage } from "../ui/notification"
9
+ import { formatTokenCount } from "../ui/utils"
10
+ import { loadAllSessionStats, type AggregatedStats } from "../state/persistence"
11
+ import { getCurrentParams } from "../token-utils"
12
+ import { normalizeCompressionUsageTotals, type CompressionUsageTotals } from "../compress/usage"
13
+ import { getActiveCompressionTargets } from "./compression-targets"
14
+
15
+ export interface StatsCommandContext {
16
+ client: any
17
+ state: SessionState
18
+ logger: Logger
19
+ sessionId: string
20
+ messages: WithParts[]
21
+ }
22
+
23
+ export function formatStatsMessage(
24
+ sessionTokens: number,
25
+ sessionSummaryTokens: number,
26
+ sessionTools: number,
27
+ sessionMessages: number,
28
+ sessionDurationMs: number,
29
+ sessionUsage: CompressionUsageTotals,
30
+ allTime: AggregatedStats,
31
+ ): string {
32
+ const lines: string[] = []
33
+
34
+ lines.push("╭───────────────────────────────────────────────────────────╮")
35
+ lines.push("│ DCP Statistics │")
36
+ lines.push("╰───────────────────────────────────────────────────────────╯")
37
+ lines.push("")
38
+ lines.push("Compression:")
39
+ lines.push("─".repeat(60))
40
+ lines.push(
41
+ ` Tokens in|out: ~${formatTokenCount(sessionTokens)} | ~${formatTokenCount(sessionSummaryTokens)}`,
42
+ )
43
+ lines.push(` Ratio: ${formatCompressionRatio(sessionTokens, sessionSummaryTokens)}`)
44
+ lines.push(` Time: ${formatCompressionTime(sessionDurationMs)}`)
45
+ lines.push(` Messages: ${sessionMessages}`)
46
+ lines.push(` Tools: ${sessionTools}`)
47
+ lines.push(
48
+ ` Summary requests: ${sessionUsage.calls} (${sessionUsage.providerCalls} provider, ${sessionUsage.estimatedCalls} estimated)`,
49
+ )
50
+ lines.push(
51
+ ` Request in|out: ~${formatTokenCount(sessionUsage.inputTokens)} | ~${formatTokenCount(sessionUsage.outputTokens)}`,
52
+ )
53
+ lines.push(
54
+ ` Request cache: ~${formatTokenCount(sessionUsage.cacheReadTokens)} read | ~${formatTokenCount(sessionUsage.cacheWriteTokens)} write`,
55
+ )
56
+ lines.push(` Request reason: ~${formatTokenCount(sessionUsage.reasoningTokens)}`)
57
+ lines.push("")
58
+ lines.push("All-time:")
59
+ lines.push("─".repeat(60))
60
+ lines.push(` Tokens saved: ~${formatTokenCount(allTime.totalTokens)}`)
61
+ lines.push(` Tools pruned: ${allTime.totalTools}`)
62
+ lines.push(` Messages pruned: ${allTime.totalMessages}`)
63
+ lines.push(` Sessions: ${allTime.sessionCount}`)
64
+ lines.push(
65
+ ` Summary requests: ${allTime.compressionUsage.calls} (~${formatTokenCount(allTime.compressionUsage.inputTokens)} in | ~${formatTokenCount(allTime.compressionUsage.outputTokens)} out)`,
66
+ )
67
+
68
+ return lines.join("\n")
69
+ }
70
+
71
+ function formatCompressionRatio(inputTokens: number, outputTokens: number): string {
72
+ if (inputTokens <= 0) {
73
+ return "0:1"
74
+ }
75
+
76
+ if (outputTokens <= 0) {
77
+ return "∞:1"
78
+ }
79
+
80
+ const ratio = Math.max(1, Math.round(inputTokens / outputTokens))
81
+ return `${ratio}:1`
82
+ }
83
+
84
+ function formatCompressionTime(ms: number): string {
85
+ const safeMs = Math.max(0, Math.round(ms))
86
+ if (safeMs < 1000) {
87
+ return `${safeMs} ms`
88
+ }
89
+
90
+ const totalSeconds = safeMs / 1000
91
+ if (totalSeconds < 60) {
92
+ return `${totalSeconds.toFixed(1)} s`
93
+ }
94
+
95
+ const wholeSeconds = Math.floor(totalSeconds)
96
+ const hours = Math.floor(wholeSeconds / 3600)
97
+ const minutes = Math.floor((wholeSeconds % 3600) / 60)
98
+ const seconds = wholeSeconds % 60
99
+
100
+ if (hours > 0) {
101
+ return `${hours}h ${minutes}m ${seconds}s`
102
+ }
103
+
104
+ return `${minutes}m ${seconds}s`
105
+ }
106
+
107
+ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void> {
108
+ const { client, state, logger, sessionId, messages } = ctx
109
+
110
+ const report = await buildStatsReport(state, logger)
111
+ const message = formatStatsMessage(
112
+ report.sessionTokens,
113
+ report.sessionSummaryTokens,
114
+ report.sessionTools,
115
+ report.sessionMessages,
116
+ report.sessionDurationMs,
117
+ report.sessionCompressionUsage,
118
+ report.allTime,
119
+ )
120
+
121
+ const params = getCurrentParams(state, messages, logger)
122
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
123
+
124
+ logger.info("Stats command executed", {
125
+ sessionTokens: report.sessionTokens,
126
+ sessionSummaryTokens: report.sessionSummaryTokens,
127
+ sessionTools: report.sessionTools,
128
+ sessionMessages: report.sessionMessages,
129
+ sessionDurationMs: report.sessionDurationMs,
130
+ sessionCompressionUsage: report.sessionCompressionUsage,
131
+ allTimeTokens: report.allTime.totalTokens,
132
+ allTimeTools: report.allTime.totalTools,
133
+ allTimeMessages: report.allTime.totalMessages,
134
+ allTimeCompressionUsage: report.allTime.compressionUsage,
135
+ })
136
+ }
137
+
138
+ export async function buildStatsReport(state: SessionState, logger: Logger) {
139
+ const sessionTokens = state.stats.totalPruneTokens
140
+ const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
141
+ (total, block) => (block.active ? total + block.summaryTokens : total),
142
+ 0,
143
+ )
144
+ const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
145
+ (total, target) => total + target.durationMs,
146
+ 0,
147
+ )
148
+
149
+ const prunedToolIds = new Set<string>(state.prune.tools.keys())
150
+ for (const block of state.prune.messages.blocksById.values()) {
151
+ if (block.active) {
152
+ for (const toolId of block.effectiveToolIds) {
153
+ prunedToolIds.add(toolId)
154
+ }
155
+ }
156
+ }
157
+ const sessionTools = prunedToolIds.size
158
+
159
+ const sessionCompressionUsage = normalizeCompressionUsageTotals(state.stats.compressionUsage)
160
+
161
+ let sessionMessages = 0
162
+ for (const entry of state.prune.messages.byMessageId.values()) {
163
+ if (entry.activeBlockIds.length > 0) {
164
+ sessionMessages++
165
+ }
166
+ }
167
+
168
+ // All-time stats from storage files
169
+ const allTime = await loadAllSessionStats(logger)
170
+
171
+ return {
172
+ sessionTokens,
173
+ sessionSummaryTokens,
174
+ sessionTools,
175
+ sessionMessages,
176
+ sessionDurationMs,
177
+ sessionCompressionUsage,
178
+ allTime,
179
+ }
180
+ }