@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.6-beta0

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 (72) hide show
  1. package/index.ts +141 -0
  2. package/lib/analysis/tokens.ts +225 -0
  3. package/lib/auth.ts +37 -0
  4. package/lib/commands/compression-targets.ts +137 -0
  5. package/lib/commands/context.ts +132 -0
  6. package/lib/commands/decompress.ts +275 -0
  7. package/lib/commands/help.ts +76 -0
  8. package/lib/commands/index.ts +11 -0
  9. package/lib/commands/manual.ts +125 -0
  10. package/lib/commands/recompress.ts +224 -0
  11. package/lib/commands/stats.ts +148 -0
  12. package/lib/commands/sweep.ts +268 -0
  13. package/lib/compress/index.ts +3 -0
  14. package/lib/compress/message-utils.ts +250 -0
  15. package/lib/compress/message.ts +137 -0
  16. package/lib/compress/pipeline.ts +106 -0
  17. package/lib/compress/protected-content.ts +154 -0
  18. package/lib/compress/range-utils.ts +308 -0
  19. package/lib/compress/range.ts +180 -0
  20. package/lib/compress/search.ts +267 -0
  21. package/lib/compress/state.ts +268 -0
  22. package/lib/compress/timing.ts +77 -0
  23. package/lib/compress/types.ts +108 -0
  24. package/lib/compress-permission.ts +25 -0
  25. package/lib/config.ts +1071 -0
  26. package/lib/hooks.ts +378 -0
  27. package/lib/host-permissions.ts +101 -0
  28. package/lib/logger.ts +235 -0
  29. package/lib/message-ids.ts +172 -0
  30. package/lib/messages/index.ts +8 -0
  31. package/lib/messages/inject/inject.ts +215 -0
  32. package/lib/messages/inject/subagent-results.ts +82 -0
  33. package/lib/messages/inject/utils.ts +374 -0
  34. package/lib/messages/priority.ts +102 -0
  35. package/lib/messages/prune.ts +238 -0
  36. package/lib/messages/query.ts +56 -0
  37. package/lib/messages/reasoning-strip.ts +40 -0
  38. package/lib/messages/sync.ts +124 -0
  39. package/lib/messages/utils.ts +187 -0
  40. package/lib/prompts/compress-message.ts +42 -0
  41. package/lib/prompts/compress-range.ts +60 -0
  42. package/lib/prompts/context-limit-nudge.ts +18 -0
  43. package/lib/prompts/extensions/nudge.ts +43 -0
  44. package/lib/prompts/extensions/system.ts +32 -0
  45. package/lib/prompts/extensions/tool.ts +35 -0
  46. package/lib/prompts/index.ts +29 -0
  47. package/lib/prompts/iteration-nudge.ts +6 -0
  48. package/lib/prompts/store.ts +467 -0
  49. package/lib/prompts/system.ts +33 -0
  50. package/lib/prompts/turn-nudge.ts +10 -0
  51. package/lib/protected-patterns.ts +128 -0
  52. package/lib/state/index.ts +4 -0
  53. package/lib/state/persistence.ts +256 -0
  54. package/lib/state/state.ts +190 -0
  55. package/lib/state/tool-cache.ts +98 -0
  56. package/lib/state/types.ts +112 -0
  57. package/lib/state/utils.ts +334 -0
  58. package/lib/strategies/deduplication.ts +127 -0
  59. package/lib/strategies/index.ts +2 -0
  60. package/lib/strategies/purge-errors.ts +88 -0
  61. package/lib/subagents/subagent-results.ts +74 -0
  62. package/lib/token-utils.ts +162 -0
  63. package/lib/ui/notification.ts +346 -0
  64. package/lib/ui/utils.ts +287 -0
  65. package/package.json +9 -2
  66. package/tui/data/context.ts +177 -0
  67. package/tui/index.tsx +34 -0
  68. package/tui/routes/summary.tsx +175 -0
  69. package/tui/shared/names.ts +9 -0
  70. package/tui/shared/theme.ts +58 -0
  71. package/tui/shared/types.ts +38 -0
  72. package/tui/slots/sidebar-content.tsx +502 -0
@@ -0,0 +1,275 @@
1
+ import type { Logger } from "../logger"
2
+ import type { CompressionBlock, 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
+ getActiveCompressionTargets,
11
+ resolveCompressionTarget,
12
+ type CompressionTarget,
13
+ } from "./compression-targets"
14
+
15
+ export interface DecompressCommandContext {
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)
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 findActiveParentBlockId(
40
+ messagesState: PruneMessagesState,
41
+ block: CompressionBlock,
42
+ ): number | null {
43
+ const queue = [...block.parentBlockIds]
44
+ const visited = new Set<number>()
45
+
46
+ while (queue.length > 0) {
47
+ const parentBlockId = queue.shift()
48
+ if (parentBlockId === undefined || visited.has(parentBlockId)) {
49
+ continue
50
+ }
51
+ visited.add(parentBlockId)
52
+
53
+ const parent = messagesState.blocksById.get(parentBlockId)
54
+ if (!parent) {
55
+ continue
56
+ }
57
+
58
+ if (parent.active) {
59
+ return parent.blockId
60
+ }
61
+
62
+ for (const ancestorId of parent.parentBlockIds) {
63
+ if (!visited.has(ancestorId)) {
64
+ queue.push(ancestorId)
65
+ }
66
+ }
67
+ }
68
+
69
+ return null
70
+ }
71
+
72
+ function findActiveAncestorBlockId(
73
+ messagesState: PruneMessagesState,
74
+ target: CompressionTarget,
75
+ ): number | null {
76
+ for (const block of target.blocks) {
77
+ const activeAncestorBlockId = findActiveParentBlockId(messagesState, block)
78
+ if (activeAncestorBlockId !== null) {
79
+ return activeAncestorBlockId
80
+ }
81
+ }
82
+
83
+ return null
84
+ }
85
+
86
+ function snapshotActiveMessages(messagesState: PruneMessagesState): Map<string, number> {
87
+ const activeMessages = new Map<string, number>()
88
+ for (const [messageId, entry] of messagesState.byMessageId) {
89
+ if (entry.activeBlockIds.length > 0) {
90
+ activeMessages.set(messageId, entry.tokenCount)
91
+ }
92
+ }
93
+ return activeMessages
94
+ }
95
+
96
+ function formatDecompressMessage(
97
+ target: CompressionTarget,
98
+ restoredMessageCount: number,
99
+ restoredTokens: number,
100
+ reactivatedBlockIds: number[],
101
+ ): string {
102
+ const lines: string[] = []
103
+
104
+ lines.push(`Restored compression ${target.displayId}.`)
105
+ if (target.runId !== target.displayId || target.grouped) {
106
+ lines.push(`Tool call label: Compression #${target.runId}.`)
107
+ }
108
+ if (reactivatedBlockIds.length > 0) {
109
+ const refs = reactivatedBlockIds.map((id) => String(id)).join(", ")
110
+ lines.push(`Also restored nested compression(s): ${refs}.`)
111
+ }
112
+
113
+ if (restoredMessageCount > 0) {
114
+ lines.push(
115
+ `Restored ${restoredMessageCount} message(s) (~${formatTokenCount(restoredTokens)}).`,
116
+ )
117
+ } else {
118
+ lines.push("No messages were restored.")
119
+ }
120
+
121
+ return lines.join("\n")
122
+ }
123
+
124
+ function formatAvailableBlocksMessage(availableTargets: CompressionTarget[]): string {
125
+ const lines: string[] = []
126
+
127
+ lines.push("Usage: /dcp decompress <n>")
128
+ lines.push("")
129
+
130
+ if (availableTargets.length === 0) {
131
+ lines.push("No compressions are available to restore.")
132
+ return lines.join("\n")
133
+ }
134
+
135
+ lines.push("Available compressions:")
136
+ const entries = availableTargets.map((target) => {
137
+ const topic = target.topic.replace(/\s+/g, " ").trim() || "(no topic)"
138
+ const label = `${target.displayId} (${formatTokenCount(target.compressedTokens)})`
139
+ const details = target.grouped
140
+ ? `Compression #${target.runId} - ${target.blocks.length} messages`
141
+ : `Compression #${target.runId}`
142
+ return { label, topic: `${details} - ${topic}` }
143
+ })
144
+
145
+ const labelWidth = Math.max(...entries.map((entry) => entry.label.length)) + 4
146
+ for (const entry of entries) {
147
+ lines.push(` ${entry.label.padEnd(labelWidth)}${entry.topic}`)
148
+ }
149
+
150
+ return lines.join("\n")
151
+ }
152
+
153
+ export async function handleDecompressCommand(ctx: DecompressCommandContext): Promise<void> {
154
+ const { client, state, logger, sessionId, messages, args } = ctx
155
+
156
+ const params = getCurrentParams(state, messages, logger)
157
+ const targetArg = args[0]
158
+
159
+ if (args.length > 1) {
160
+ await sendIgnoredMessage(
161
+ client,
162
+ sessionId,
163
+ "Invalid arguments. Usage: /dcp decompress <n>",
164
+ params,
165
+ logger,
166
+ )
167
+ return
168
+ }
169
+
170
+ syncCompressionBlocks(state, logger, messages)
171
+ const messagesState = state.prune.messages
172
+
173
+ if (!targetArg) {
174
+ const availableTargets = getActiveCompressionTargets(messagesState)
175
+ const message = formatAvailableBlocksMessage(availableTargets)
176
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
177
+ return
178
+ }
179
+
180
+ const targetBlockId = parseBlockIdArg(targetArg)
181
+ if (targetBlockId === null) {
182
+ await sendIgnoredMessage(
183
+ client,
184
+ sessionId,
185
+ `Please enter a compression number. Example: /dcp decompress 2`,
186
+ params,
187
+ logger,
188
+ )
189
+ return
190
+ }
191
+
192
+ const target = resolveCompressionTarget(messagesState, targetBlockId)
193
+ if (!target) {
194
+ await sendIgnoredMessage(
195
+ client,
196
+ sessionId,
197
+ `Compression ${targetBlockId} does not exist.`,
198
+ params,
199
+ logger,
200
+ )
201
+ return
202
+ }
203
+
204
+ const activeBlocks = target.blocks.filter((block) => block.active)
205
+ if (activeBlocks.length === 0) {
206
+ const activeAncestorBlockId = findActiveAncestorBlockId(messagesState, target)
207
+ if (activeAncestorBlockId !== null) {
208
+ await sendIgnoredMessage(
209
+ client,
210
+ sessionId,
211
+ `Compression ${target.displayId} is inside compression ${activeAncestorBlockId}. Restore compression ${activeAncestorBlockId} first.`,
212
+ params,
213
+ logger,
214
+ )
215
+ return
216
+ }
217
+
218
+ await sendIgnoredMessage(
219
+ client,
220
+ sessionId,
221
+ `Compression ${target.displayId} is not active.`,
222
+ params,
223
+ logger,
224
+ )
225
+ return
226
+ }
227
+
228
+ const activeMessagesBefore = snapshotActiveMessages(messagesState)
229
+ const activeBlockIdsBefore = new Set(messagesState.activeBlockIds)
230
+ const deactivatedAt = Date.now()
231
+
232
+ for (const block of target.blocks) {
233
+ block.active = false
234
+ block.deactivatedByUser = true
235
+ block.deactivatedAt = deactivatedAt
236
+ block.deactivatedByBlockId = undefined
237
+ }
238
+
239
+ syncCompressionBlocks(state, logger, messages)
240
+
241
+ let restoredMessageCount = 0
242
+ let restoredTokens = 0
243
+ for (const [messageId, tokenCount] of activeMessagesBefore) {
244
+ const entry = messagesState.byMessageId.get(messageId)
245
+ const isActiveNow = entry ? entry.activeBlockIds.length > 0 : false
246
+ if (!isActiveNow) {
247
+ restoredMessageCount++
248
+ restoredTokens += tokenCount
249
+ }
250
+ }
251
+
252
+ state.stats.totalPruneTokens = Math.max(0, state.stats.totalPruneTokens - restoredTokens)
253
+
254
+ const reactivatedBlockIds = Array.from(messagesState.activeBlockIds)
255
+ .filter((blockId) => !activeBlockIdsBefore.has(blockId))
256
+ .sort((a, b) => a - b)
257
+
258
+ await saveSessionState(state, logger)
259
+
260
+ const message = formatDecompressMessage(
261
+ target,
262
+ restoredMessageCount,
263
+ restoredTokens,
264
+ reactivatedBlockIds,
265
+ )
266
+ await sendIgnoredMessage(client, sessionId, message, params, logger)
267
+
268
+ logger.info("Decompress command completed", {
269
+ targetBlockId: target.displayId,
270
+ targetRunId: target.runId,
271
+ restoredMessageCount,
272
+ restoredTokens,
273
+ reactivatedBlockIds,
274
+ })
275
+ }
@@ -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 BASE_COMMANDS: [string, string][] = [
23
+ ["/dcp context", "Show token usage breakdown for current session"],
24
+ ["/dcp stats", "Show DCP pruning statistics"],
25
+ ["/dcp sweep [n]", "Prune tools since last user message, or last n tools"],
26
+ ["/dcp manual [on|off]", "Toggle manual mode or set explicit state"],
27
+ ]
28
+
29
+ const TOOL_COMMANDS: Record<string, [string, string]> = {
30
+ compress: ["/dcp compress [focus]", "Trigger manual compress tool execution"],
31
+ decompress: ["/dcp decompress <n>", "Restore selected compression"],
32
+ recompress: ["/dcp recompress <n>", "Re-apply a user-decompressed compression"],
33
+ }
34
+
35
+ function getVisibleCommands(state: SessionState, config: PluginConfig): [string, string][] {
36
+ const commands = [...BASE_COMMANDS]
37
+
38
+ if (compressPermission(state, config) !== "deny") {
39
+ commands.push(TOOL_COMMANDS.compress)
40
+ commands.push(TOOL_COMMANDS.decompress)
41
+ commands.push(TOOL_COMMANDS.recompress)
42
+ }
43
+
44
+ return commands
45
+ }
46
+
47
+ function formatHelpMessage(state: SessionState, config: PluginConfig): string {
48
+ const commands = getVisibleCommands(state, config)
49
+ const colWidth = Math.max(...commands.map(([cmd]) => cmd.length)) + 4
50
+ const lines: string[] = []
51
+
52
+ lines.push("╭─────────────────────────────────────────────────────────────────────────╮")
53
+ lines.push("│ DCP Commands │")
54
+ lines.push("╰─────────────────────────────────────────────────────────────────────────╯")
55
+ lines.push("")
56
+ lines.push(` ${"Manual mode:".padEnd(colWidth)}${state.manualMode ? "ON" : "OFF"}`)
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,125 @@
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 { getCurrentParams } from "../token-utils"
15
+ import { buildCompressedBlockGuidance } from "../prompts/extensions/nudge"
16
+ import { isIgnoredUserMessage } from "../messages/query"
17
+
18
+ const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp compress to trigger context tools manually."
19
+
20
+ const MANUAL_MODE_OFF = "Manual mode is now OFF."
21
+
22
+ const COMPRESS_TRIGGER_PROMPT = [
23
+ "<compress triggered manually>",
24
+ "Manual mode trigger received. You must now use the compress tool.",
25
+ "Find the most significant completed conversation content that can be compressed into a high-fidelity technical summary.",
26
+ "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
27
+ "Return after compress with a brief explanation of what content was compressed.",
28
+ ].join("\n\n")
29
+
30
+ function getTriggerPrompt(
31
+ tool: "compress",
32
+ state: SessionState,
33
+ config: PluginConfig,
34
+ userFocus?: string,
35
+ ): string {
36
+ const base = COMPRESS_TRIGGER_PROMPT
37
+ const compressedBlockGuidance =
38
+ config.compress.mode === "message" ? "" : 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
+
80
+ logger.info("Manual mode toggled", { manualMode: state.manualMode })
81
+ }
82
+
83
+ export async function handleManualTriggerCommand(
84
+ ctx: ManualCommandContext,
85
+ tool: "compress",
86
+ userFocus?: string,
87
+ ): Promise<string | null> {
88
+ return getTriggerPrompt(tool, ctx.state, ctx.config, userFocus)
89
+ }
90
+
91
+ export function applyPendingManualTrigger(
92
+ state: SessionState,
93
+ messages: WithParts[],
94
+ logger: Logger,
95
+ ): void {
96
+ const pending = state.pendingManualTrigger
97
+ if (!pending) {
98
+ return
99
+ }
100
+
101
+ if (!state.sessionId || pending.sessionId !== state.sessionId) {
102
+ state.pendingManualTrigger = null
103
+ return
104
+ }
105
+
106
+ for (let i = messages.length - 1; i >= 0; i--) {
107
+ const msg = messages[i]
108
+ if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) {
109
+ continue
110
+ }
111
+
112
+ for (const part of msg.parts) {
113
+ if (part.type !== "text" || part.ignored || part.synthetic) {
114
+ continue
115
+ }
116
+
117
+ part.text = pending.prompt
118
+ state.pendingManualTrigger = null
119
+ logger.debug("Applied manual prompt", { sessionId: pending.sessionId })
120
+ return
121
+ }
122
+ }
123
+
124
+ state.pendingManualTrigger = null
125
+ }