@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7611 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +25 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +58 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +55 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +45 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +102 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +55 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +159 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +458 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +293 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +57 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +305 -0
- package/lib/state/state.ts +210 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +114 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +237 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +401 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +48 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
package/lib/ui/utils.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { SessionState, ToolParameterEntry, WithParts } from "../state"
|
|
2
|
+
import { countTokens } from "../token-utils"
|
|
3
|
+
import { isIgnoredUserMessage } from "../messages/query"
|
|
4
|
+
|
|
5
|
+
function extractParameterKey(tool: string, parameters: any): string {
|
|
6
|
+
if (!parameters) return ""
|
|
7
|
+
|
|
8
|
+
if (tool === "read" && parameters.filePath) {
|
|
9
|
+
const offset = parameters.offset
|
|
10
|
+
const limit = parameters.limit
|
|
11
|
+
if (offset !== undefined && limit !== undefined) {
|
|
12
|
+
return `${parameters.filePath} (lines ${offset}-${offset + limit})`
|
|
13
|
+
}
|
|
14
|
+
if (offset !== undefined) {
|
|
15
|
+
return `${parameters.filePath} (lines ${offset}+)`
|
|
16
|
+
}
|
|
17
|
+
if (limit !== undefined) {
|
|
18
|
+
return `${parameters.filePath} (lines 0-${limit})`
|
|
19
|
+
}
|
|
20
|
+
return parameters.filePath
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ((tool === "write" || tool === "edit" || tool === "multiedit") && parameters.filePath) {
|
|
24
|
+
return parameters.filePath
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (tool === "apply_patch" && typeof parameters.patchText === "string") {
|
|
28
|
+
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g
|
|
29
|
+
const paths: string[] = []
|
|
30
|
+
let match
|
|
31
|
+
while ((match = pathRegex.exec(parameters.patchText)) !== null) {
|
|
32
|
+
paths.push(match[1].trim())
|
|
33
|
+
}
|
|
34
|
+
if (paths.length > 0) {
|
|
35
|
+
const uniquePaths = [...new Set(paths)]
|
|
36
|
+
const count = uniquePaths.length
|
|
37
|
+
const plural = count > 1 ? "s" : ""
|
|
38
|
+
if (count === 1) return uniquePaths[0]
|
|
39
|
+
if (count === 2) return uniquePaths.join(", ")
|
|
40
|
+
return `${count} file${plural}: ${uniquePaths[0]}, ${uniquePaths[1]}...`
|
|
41
|
+
}
|
|
42
|
+
return "patch"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tool === "list") {
|
|
46
|
+
return parameters.path || "(current directory)"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (tool === "glob") {
|
|
50
|
+
if (parameters.pattern) {
|
|
51
|
+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
|
|
52
|
+
return `"${parameters.pattern}"${pathInfo}`
|
|
53
|
+
}
|
|
54
|
+
return "(unknown pattern)"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tool === "grep") {
|
|
58
|
+
if (parameters.pattern) {
|
|
59
|
+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
|
|
60
|
+
return `"${parameters.pattern}"${pathInfo}`
|
|
61
|
+
}
|
|
62
|
+
return "(unknown pattern)"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (tool === "bash") {
|
|
66
|
+
if (parameters.description) return parameters.description
|
|
67
|
+
if (parameters.command) {
|
|
68
|
+
return parameters.command.length > 50
|
|
69
|
+
? parameters.command.substring(0, 50) + "..."
|
|
70
|
+
: parameters.command
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (tool === "webfetch" && parameters.url) {
|
|
75
|
+
return parameters.url
|
|
76
|
+
}
|
|
77
|
+
if (tool === "websearch" && parameters.query) {
|
|
78
|
+
return `"${parameters.query}"`
|
|
79
|
+
}
|
|
80
|
+
if (tool === "codesearch" && parameters.query) {
|
|
81
|
+
return `"${parameters.query}"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (tool === "todowrite") {
|
|
85
|
+
return `${parameters.todos?.length || 0} todos`
|
|
86
|
+
}
|
|
87
|
+
if (tool === "todoread") {
|
|
88
|
+
return "read todo list"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (tool === "task" && parameters.description) {
|
|
92
|
+
return parameters.description
|
|
93
|
+
}
|
|
94
|
+
if (tool === "skill" && parameters.name) {
|
|
95
|
+
return parameters.name
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (tool === "lsp") {
|
|
99
|
+
const op = parameters.operation || "lsp"
|
|
100
|
+
const path = parameters.filePath || ""
|
|
101
|
+
const line = parameters.line
|
|
102
|
+
const char = parameters.character
|
|
103
|
+
if (path && line !== undefined && char !== undefined) {
|
|
104
|
+
return `${op} ${path}:${line}:${char}`
|
|
105
|
+
}
|
|
106
|
+
if (path) {
|
|
107
|
+
return `${op} ${path}`
|
|
108
|
+
}
|
|
109
|
+
return op
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (tool === "question") {
|
|
113
|
+
const questions = parameters.questions
|
|
114
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
115
|
+
const headers = questions
|
|
116
|
+
.map((q: any) => q.header || "")
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.slice(0, 3)
|
|
119
|
+
|
|
120
|
+
const count = questions.length
|
|
121
|
+
const plural = count > 1 ? "s" : ""
|
|
122
|
+
|
|
123
|
+
if (headers.length > 0) {
|
|
124
|
+
const suffix = count > 3 ? ` (+${count - 3} more)` : ""
|
|
125
|
+
return `${count} question${plural}: ${headers.join(", ")}${suffix}`
|
|
126
|
+
}
|
|
127
|
+
return `${count} question${plural}`
|
|
128
|
+
}
|
|
129
|
+
return "question"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const paramStr = JSON.stringify(parameters)
|
|
133
|
+
if (paramStr === "{}" || paramStr === "[]" || paramStr === "null") {
|
|
134
|
+
return ""
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return paramStr.substring(0, 50)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatStatsHeader(totalTokensSaved: number, pruneTokenCounter: number): string {
|
|
141
|
+
const totalTokensSavedStr = `~${formatTokenCount(totalTokensSaved + pruneTokenCounter)}`
|
|
142
|
+
return [`▣ DCP | ${totalTokensSavedStr} saved total`].join("\n")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatTokenCount(tokens: number, compact?: boolean): string {
|
|
146
|
+
const suffix = compact ? "" : " tokens"
|
|
147
|
+
if (tokens >= 1000) {
|
|
148
|
+
return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K") + suffix
|
|
149
|
+
}
|
|
150
|
+
return tokens.toString() + suffix
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function truncate(str: string, maxLen: number = 60): string {
|
|
154
|
+
if (str.length <= maxLen) return str
|
|
155
|
+
return str.slice(0, maxLen - 3) + "..."
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function formatProgressBar(
|
|
159
|
+
messageIds: string[],
|
|
160
|
+
prunedMessages: Map<string, number>,
|
|
161
|
+
recentMessageIds: string[],
|
|
162
|
+
width: number = 50,
|
|
163
|
+
): string {
|
|
164
|
+
const ACTIVE = "█"
|
|
165
|
+
const PRUNED = "░"
|
|
166
|
+
const RECENT = "⣿"
|
|
167
|
+
const recentSet = new Set(recentMessageIds)
|
|
168
|
+
|
|
169
|
+
const total = messageIds.length
|
|
170
|
+
if (total === 0) return `│${PRUNED.repeat(width)}│`
|
|
171
|
+
|
|
172
|
+
const bar = new Array(width).fill(ACTIVE)
|
|
173
|
+
|
|
174
|
+
for (let m = 0; m < total; m++) {
|
|
175
|
+
const msgId = messageIds[m]
|
|
176
|
+
const start = Math.floor((m / total) * width)
|
|
177
|
+
const end = Math.floor(((m + 1) / total) * width)
|
|
178
|
+
|
|
179
|
+
if (recentSet.has(msgId)) {
|
|
180
|
+
for (let i = start; i < end; i++) {
|
|
181
|
+
bar[i] = RECENT
|
|
182
|
+
}
|
|
183
|
+
} else if (prunedMessages.has(msgId)) {
|
|
184
|
+
for (let i = start; i < end; i++) {
|
|
185
|
+
bar[i] = PRUNED
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `│${bar.join("")}│`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function cacheSystemPromptTokens(state: SessionState, messages: WithParts[]): void {
|
|
194
|
+
let firstInputTokens = 0
|
|
195
|
+
for (const msg of messages) {
|
|
196
|
+
if (msg.info.role !== "assistant") {
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
const info = msg.info as any
|
|
200
|
+
const input = info?.tokens?.input || 0
|
|
201
|
+
const cacheRead = info?.tokens?.cache?.read || 0
|
|
202
|
+
const cacheWrite = info?.tokens?.cache?.write || 0
|
|
203
|
+
if (input > 0 || cacheRead > 0 || cacheWrite > 0) {
|
|
204
|
+
firstInputTokens = input + cacheRead + cacheWrite
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (firstInputTokens <= 0) {
|
|
210
|
+
state.systemPromptTokens = undefined
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let firstUserText = ""
|
|
215
|
+
for (const msg of messages) {
|
|
216
|
+
if (msg.info.role !== "user" || isIgnoredUserMessage(msg)) {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
if (part.type === "text" && !(part as any).ignored) {
|
|
222
|
+
firstUserText += part.text
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens(firstUserText))
|
|
229
|
+
state.systemPromptTokens = estimatedSystemTokens > 0 ? estimatedSystemTokens : undefined
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function shortenPath(input: string, workingDirectory?: string): string {
|
|
233
|
+
const inPathMatch = input.match(/^(.+) in (.+)$/)
|
|
234
|
+
if (inPathMatch) {
|
|
235
|
+
const prefix = inPathMatch[1]
|
|
236
|
+
const pathPart = inPathMatch[2]
|
|
237
|
+
const shortenedPath = shortenSinglePath(pathPart, workingDirectory)
|
|
238
|
+
return `${prefix} in ${shortenedPath}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return shortenSinglePath(input, workingDirectory)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function shortenSinglePath(path: string, workingDirectory?: string): string {
|
|
245
|
+
if (workingDirectory) {
|
|
246
|
+
if (path.startsWith(workingDirectory + "/")) {
|
|
247
|
+
return path.slice(workingDirectory.length + 1)
|
|
248
|
+
}
|
|
249
|
+
if (path === workingDirectory) {
|
|
250
|
+
return "."
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return path
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function formatPrunedItemsList(
|
|
258
|
+
pruneToolIds: string[],
|
|
259
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
260
|
+
workingDirectory?: string,
|
|
261
|
+
): string[] {
|
|
262
|
+
const lines: string[] = []
|
|
263
|
+
|
|
264
|
+
for (const id of pruneToolIds) {
|
|
265
|
+
const metadata = toolMetadata.get(id)
|
|
266
|
+
|
|
267
|
+
if (metadata) {
|
|
268
|
+
const paramKey = extractParameterKey(metadata.tool, metadata.parameters)
|
|
269
|
+
if (paramKey) {
|
|
270
|
+
// Use 60 char limit to match notification style
|
|
271
|
+
const displayKey = truncate(shortenPath(paramKey, workingDirectory), 60)
|
|
272
|
+
lines.push(`→ ${metadata.tool}: ${displayKey}`)
|
|
273
|
+
} else {
|
|
274
|
+
lines.push(`→ ${metadata.tool}`)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const knownCount = pruneToolIds.filter((id) => toolMetadata.has(id)).length
|
|
280
|
+
const unknownCount = pruneToolIds.length - knownCount
|
|
281
|
+
|
|
282
|
+
if (unknownCount > 0) {
|
|
283
|
+
lines.push(`→ (${unknownCount} tool${unknownCount > 1 ? "s" : ""} with unknown metadata)`)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return lines
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function formatPruningResultForTool(
|
|
290
|
+
prunedIds: string[],
|
|
291
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
292
|
+
workingDirectory?: string,
|
|
293
|
+
): string {
|
|
294
|
+
const lines: string[] = []
|
|
295
|
+
lines.push(`Context pruning complete. Pruned ${prunedIds.length} tool outputs.`)
|
|
296
|
+
lines.push("")
|
|
297
|
+
|
|
298
|
+
if (prunedIds.length > 0) {
|
|
299
|
+
lines.push(`Semantically pruned (${prunedIds.length}):`)
|
|
300
|
+
lines.push(...formatPrunedItemsList(prunedIds, toolMetadata, workingDirectory))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return lines.join("\n").trim()
|
|
304
|
+
}
|
package/lib/v2/index.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
|
3
|
+
import { getConfig } from "../config"
|
|
4
|
+
import { Logger } from "../logger"
|
|
5
|
+
import { PromptStore } from "../prompts/store"
|
|
6
|
+
import { createRcCompressTool } from "../compress"
|
|
7
|
+
import { RC_SUMMARY_MARKER } from "../compress/summary"
|
|
8
|
+
import { attachCompressionDuration } from "../compress/state"
|
|
9
|
+
import { createCommandExecuteHandler, createSystemPromptHandler } from "../hooks"
|
|
10
|
+
import {
|
|
11
|
+
createSessionState,
|
|
12
|
+
ensureSessionInitialized,
|
|
13
|
+
checkSession,
|
|
14
|
+
saveSessionState,
|
|
15
|
+
syncToolCache,
|
|
16
|
+
type SessionState,
|
|
17
|
+
} from "../state"
|
|
18
|
+
import { assignMessageRefs } from "../message-ids"
|
|
19
|
+
import { applyPendingManualTrigger } from "../commands/manual"
|
|
20
|
+
import {
|
|
21
|
+
buildToolIdList,
|
|
22
|
+
injectCompressNudges,
|
|
23
|
+
injectMessageIds,
|
|
24
|
+
injectExtendedSubAgentResults,
|
|
25
|
+
prune,
|
|
26
|
+
stripHallucinations,
|
|
27
|
+
syncCompressionBlocks,
|
|
28
|
+
} from "../messages"
|
|
29
|
+
import { countTokens } from "../token-utils"
|
|
30
|
+
import { matchesGlob } from "../protected-patterns"
|
|
31
|
+
import { history, project } from "./messages"
|
|
32
|
+
import { analyzeContextTokens } from "../commands/context"
|
|
33
|
+
import { buildStatsReport } from "../commands/stats"
|
|
34
|
+
import { rpc } from "./rpc"
|
|
35
|
+
|
|
36
|
+
// Extension point for future model-invisible V2 reports. Never use synthetic()
|
|
37
|
+
// here: its text would enter the model's context, unlike V1 ignored messages.
|
|
38
|
+
export async function report(logger: Logger, text: string, sessionID?: string) {
|
|
39
|
+
logger.debug("V2 report (display pending)", { sessionID, text })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function setup(ctx: Plugin.Context) {
|
|
43
|
+
const warnings = {
|
|
44
|
+
tui: {
|
|
45
|
+
showToast: async (input: { body: { message: string } }) => {
|
|
46
|
+
console.warn(`DCP: ${input.body.message}`)
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
const config = getConfig({ directory: ctx.location.directory, client: warnings })
|
|
51
|
+
if (!config.enabled) {
|
|
52
|
+
await ctx.rpc.register(
|
|
53
|
+
{ ...rpc, methods: { status: rpc.methods.status } },
|
|
54
|
+
{ status: async () => ({ enabled: false }) },
|
|
55
|
+
)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const logger = new Logger(config.debug)
|
|
59
|
+
const prompts = new PromptStore(
|
|
60
|
+
logger,
|
|
61
|
+
ctx.location.directory,
|
|
62
|
+
config.experimental.customPrompts,
|
|
63
|
+
"compact",
|
|
64
|
+
)
|
|
65
|
+
const sessions = new Map<string, SessionState>()
|
|
66
|
+
const queues = new Map<string, Promise<unknown>>()
|
|
67
|
+
const limits = new Map<string, number>()
|
|
68
|
+
const aliases: Record<string, string> = {
|
|
69
|
+
task: "subagent",
|
|
70
|
+
bash: "shell",
|
|
71
|
+
apply_patch: "patch",
|
|
72
|
+
}
|
|
73
|
+
for (const list of [
|
|
74
|
+
config.compress.protectedTools,
|
|
75
|
+
config.commands.protectedTools,
|
|
76
|
+
config.strategies.deduplication.protectedTools,
|
|
77
|
+
config.strategies.purgeErrors.protectedTools,
|
|
78
|
+
]) {
|
|
79
|
+
for (const name of [...list])
|
|
80
|
+
if (aliases[name] && !list.includes(aliases[name]!)) list.push(aliases[name]!)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function serial<T>(sessionID: string, operation: () => Promise<T>): Promise<T> {
|
|
84
|
+
const pending = (queues.get(sessionID) ?? Promise.resolve()).catch(() => {}).then(operation)
|
|
85
|
+
queues.set(sessionID, pending)
|
|
86
|
+
void pending
|
|
87
|
+
.finally(() => {
|
|
88
|
+
if (queues.get(sessionID) === pending) queues.delete(sessionID)
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {})
|
|
91
|
+
return pending
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const client = {
|
|
95
|
+
session: {
|
|
96
|
+
get: async ({ path }: { path: { id: string } }) => ({
|
|
97
|
+
data: await ctx.session.get({ sessionID: path.id }),
|
|
98
|
+
}),
|
|
99
|
+
messages: async ({ path }: { path: { id: string } }) => {
|
|
100
|
+
const [entries, session] = await Promise.all([
|
|
101
|
+
ctx.session.context({ sessionID: path.id }),
|
|
102
|
+
ctx.session.get({ sessionID: path.id }),
|
|
103
|
+
])
|
|
104
|
+
return { data: history(entries, session) }
|
|
105
|
+
},
|
|
106
|
+
prompt: async (input: {
|
|
107
|
+
path: { id: string }
|
|
108
|
+
body: { parts: Array<{ text: string }> }
|
|
109
|
+
}) =>
|
|
110
|
+
report(logger, input.body.parts.map((part) => part.text).join("\n"), input.path.id),
|
|
111
|
+
},
|
|
112
|
+
tui: {
|
|
113
|
+
showToast: async (input: { body: { message: string } }) =>
|
|
114
|
+
report(logger, input.body.message),
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function load(sessionID: string, agentID?: string) {
|
|
119
|
+
const [session, entries] = await Promise.all([
|
|
120
|
+
ctx.session.get({ sessionID }),
|
|
121
|
+
ctx.session.context({ sessionID }),
|
|
122
|
+
])
|
|
123
|
+
const selected =
|
|
124
|
+
agentID ??
|
|
125
|
+
session.agent ??
|
|
126
|
+
entries.findLast((entry) => entry.type === "assistant")?.agent
|
|
127
|
+
if (!selected) throw new Error("DCP commands require a session with a selected agent")
|
|
128
|
+
const { data: agent } = await ctx.agent.get({ agentID: selected })
|
|
129
|
+
let state = sessions.get(sessionID)
|
|
130
|
+
if (!state) {
|
|
131
|
+
state = createSessionState("compact")
|
|
132
|
+
sessions.set(sessionID, state)
|
|
133
|
+
}
|
|
134
|
+
const messages = history(entries, session)
|
|
135
|
+
await ensureSessionInitialized(
|
|
136
|
+
client,
|
|
137
|
+
state,
|
|
138
|
+
sessionID,
|
|
139
|
+
logger,
|
|
140
|
+
messages,
|
|
141
|
+
config.manualMode.enabled,
|
|
142
|
+
)
|
|
143
|
+
await checkSession(client, state, logger, messages, config.manualMode.enabled)
|
|
144
|
+
const rule = [...agent.permissions, ...(session.permissions ?? [])].findLast(
|
|
145
|
+
(rule) => matchesGlob("compress", rule.action) && matchesGlob("*", rule.resource),
|
|
146
|
+
)
|
|
147
|
+
state.compressPermission =
|
|
148
|
+
config.compress.permission === "deny"
|
|
149
|
+
? "deny"
|
|
150
|
+
: rule?.effect === "deny"
|
|
151
|
+
? "deny"
|
|
152
|
+
: config.compress.permission === "ask" || rule?.effect === "ask"
|
|
153
|
+
? "ask"
|
|
154
|
+
: "allow"
|
|
155
|
+
return { state, entries, session, messages }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function allowed(state: SessionState) {
|
|
159
|
+
if (state.isSubAgent && !config.experimental.allowSubAgents)
|
|
160
|
+
throw new Error("DCP compression is disabled in subagents")
|
|
161
|
+
if (state.compressPermission === "deny") throw new Error("DCP compression is denied")
|
|
162
|
+
if (state.compressPermission === "ask")
|
|
163
|
+
throw new Error(
|
|
164
|
+
"DCP: compress permission 'ask' is not supported by OpenCode V2's public plugin API yet. Compression was not performed.",
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await ctx.model.transform((editor) => {
|
|
169
|
+
limits.clear()
|
|
170
|
+
for (const model of editor.list())
|
|
171
|
+
limits.set(`${model.providerID}/${model.id}`, model.limit.context)
|
|
172
|
+
})
|
|
173
|
+
for (const kind of ["context", "compaction"] as const)
|
|
174
|
+
await ctx.session.hook(kind, (event) =>
|
|
175
|
+
serial(event.sessionID, async () => {
|
|
176
|
+
const { state, entries, session, messages } = await load(
|
|
177
|
+
event.sessionID,
|
|
178
|
+
event.agent,
|
|
179
|
+
)
|
|
180
|
+
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
181
|
+
delete event.tools.compress
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if (state.compressPermission === "deny") delete event.tools.compress
|
|
185
|
+
state.modelContextLimit = limits.get(`${event.model.providerID}/${event.model.id}`)
|
|
186
|
+
state.systemPromptTokens = countTokens(
|
|
187
|
+
event.system.map((part) => part.text).join("\n"),
|
|
188
|
+
)
|
|
189
|
+
const view = project(event.messages, entries, {
|
|
190
|
+
...session,
|
|
191
|
+
agent: event.agent,
|
|
192
|
+
model: event.model,
|
|
193
|
+
})
|
|
194
|
+
stripHallucinations(view.messages, state.idFormat)
|
|
195
|
+
assignMessageRefs(state, view.messages)
|
|
196
|
+
// Compaction may select only a prefix; block origins can be in the retained tail.
|
|
197
|
+
syncCompressionBlocks(state, logger, messages)
|
|
198
|
+
syncToolCache(state, config, logger, view.messages)
|
|
199
|
+
buildToolIdList(state, view.messages)
|
|
200
|
+
prune(state, logger, view.messages, view.summaryBase)
|
|
201
|
+
await injectExtendedSubAgentResults(
|
|
202
|
+
client,
|
|
203
|
+
state,
|
|
204
|
+
logger,
|
|
205
|
+
view.messages,
|
|
206
|
+
config.experimental.allowSubAgents,
|
|
207
|
+
)
|
|
208
|
+
prompts.reload()
|
|
209
|
+
injectCompressNudges(
|
|
210
|
+
state,
|
|
211
|
+
config,
|
|
212
|
+
logger,
|
|
213
|
+
view.messages,
|
|
214
|
+
prompts.getRuntimePrompts(),
|
|
215
|
+
kind === "context",
|
|
216
|
+
)
|
|
217
|
+
injectMessageIds(state, config, view.messages)
|
|
218
|
+
applyPendingManualTrigger(state, view.messages, logger)
|
|
219
|
+
event.messages = view.restore()
|
|
220
|
+
const system = { system: event.system.map((part) => part.text) }
|
|
221
|
+
await createSystemPromptHandler(
|
|
222
|
+
state,
|
|
223
|
+
logger,
|
|
224
|
+
config,
|
|
225
|
+
prompts,
|
|
226
|
+
)(
|
|
227
|
+
{
|
|
228
|
+
sessionID: event.sessionID,
|
|
229
|
+
model: { limit: { context: state.modelContextLimit ?? 0 } },
|
|
230
|
+
},
|
|
231
|
+
system,
|
|
232
|
+
)
|
|
233
|
+
event.system = system.system.map((text, index) => ({
|
|
234
|
+
...event.system[index],
|
|
235
|
+
type: "text",
|
|
236
|
+
text,
|
|
237
|
+
}))
|
|
238
|
+
await logger.saveContext(event.sessionID, view.messages)
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
// Hidden summary generation for rc mode: when our marker is present, drop
|
|
243
|
+
// the session context and every tool so the transient request contains
|
|
244
|
+
// only the prompt we built. This keeps the summarization request invisible
|
|
245
|
+
// and independent of any injected metadata.
|
|
246
|
+
await ctx.session.hook("generate", (event) => {
|
|
247
|
+
const own = event.messages.filter(
|
|
248
|
+
(message) =>
|
|
249
|
+
Array.isArray(message.content) &&
|
|
250
|
+
message.content.some(
|
|
251
|
+
(part: any) =>
|
|
252
|
+
part?.type === "text" &&
|
|
253
|
+
typeof part.text === "string" &&
|
|
254
|
+
part.text.includes(RC_SUMMARY_MARKER),
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
if (own.length === 0) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
event.messages = own as typeof event.messages
|
|
261
|
+
for (const name of Object.keys(event.tools ?? {})) {
|
|
262
|
+
delete (event.tools as Record<string, unknown>)[name]
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
if (config.compress.permission !== "deny") {
|
|
267
|
+
const define = (state: SessionState): ToolDefinition => {
|
|
268
|
+
const context = {
|
|
269
|
+
client,
|
|
270
|
+
state,
|
|
271
|
+
logger,
|
|
272
|
+
config,
|
|
273
|
+
prompts,
|
|
274
|
+
generate: async (input: { sessionID: string; prompt: string }) => {
|
|
275
|
+
const result = await ctx.session.generate(input)
|
|
276
|
+
return result?.text ?? ""
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
return createRcCompressTool(context)
|
|
280
|
+
}
|
|
281
|
+
const definition = define(createSessionState("compact"))
|
|
282
|
+
await ctx.tool.transform((editor) =>
|
|
283
|
+
editor.add({
|
|
284
|
+
name: "compress",
|
|
285
|
+
description: definition.description,
|
|
286
|
+
input: tool.schema.object(definition.args),
|
|
287
|
+
options: { codemode: false, permission: "compress" },
|
|
288
|
+
execute: (input, context) =>
|
|
289
|
+
serial(context.sessionID, async () => {
|
|
290
|
+
const { state } = await load(context.sessionID, context.agent)
|
|
291
|
+
allowed(state)
|
|
292
|
+
const started = Date.now()
|
|
293
|
+
const legacy = define(state)
|
|
294
|
+
const content = await legacy.execute(input, {
|
|
295
|
+
sessionID: context.sessionID,
|
|
296
|
+
messageID: context.messageID,
|
|
297
|
+
callID: context.id,
|
|
298
|
+
agent: context.agent,
|
|
299
|
+
directory: ctx.location.directory,
|
|
300
|
+
worktree: ctx.location.directory,
|
|
301
|
+
abort: new AbortController().signal,
|
|
302
|
+
ask: async () => allowed(state),
|
|
303
|
+
metadata: ({ title }: { title?: string }) => {
|
|
304
|
+
void context.progress({ title })
|
|
305
|
+
},
|
|
306
|
+
} as Parameters<typeof legacy.execute>[1])
|
|
307
|
+
attachCompressionDuration(
|
|
308
|
+
state.prune.messages,
|
|
309
|
+
context.messageID,
|
|
310
|
+
context.id,
|
|
311
|
+
Date.now() - started,
|
|
312
|
+
)
|
|
313
|
+
await saveSessionState(state, logger)
|
|
314
|
+
// The rc executor returns the tool result text.
|
|
315
|
+
return { content: content as string }
|
|
316
|
+
}),
|
|
317
|
+
}),
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
if (config.commands.enabled)
|
|
321
|
+
await ctx.command.transform((editor) => {
|
|
322
|
+
for (const name of ["dcp", "dcp-compress"])
|
|
323
|
+
editor.add({
|
|
324
|
+
name,
|
|
325
|
+
description: name === "dcp" ? "DCP commands" : "Trigger DCP manual compression",
|
|
326
|
+
execute: async (invocation) => {
|
|
327
|
+
const prompt = await serial(invocation.sessionID, async () => {
|
|
328
|
+
const { state } = await load(invocation.sessionID)
|
|
329
|
+
const permission =
|
|
330
|
+
state.compressPermission ?? config.compress.permission
|
|
331
|
+
if (
|
|
332
|
+
name === "dcp-compress" ||
|
|
333
|
+
invocation.prompt.text.trim().split(/\s+/)[0] === "compress"
|
|
334
|
+
)
|
|
335
|
+
allowed(state)
|
|
336
|
+
const output = { parts: [] }
|
|
337
|
+
await createCommandExecuteHandler(
|
|
338
|
+
client,
|
|
339
|
+
state,
|
|
340
|
+
logger,
|
|
341
|
+
config,
|
|
342
|
+
ctx.location.directory,
|
|
343
|
+
)(
|
|
344
|
+
{
|
|
345
|
+
command: name,
|
|
346
|
+
sessionID: invocation.sessionID,
|
|
347
|
+
arguments: invocation.prompt.text,
|
|
348
|
+
},
|
|
349
|
+
output,
|
|
350
|
+
)
|
|
351
|
+
state.compressPermission = permission
|
|
352
|
+
const pending = state.pendingManualTrigger
|
|
353
|
+
if (!pending) return
|
|
354
|
+
allowed(state)
|
|
355
|
+
state.pendingManualTrigger = null
|
|
356
|
+
return pending.prompt
|
|
357
|
+
})
|
|
358
|
+
if (prompt)
|
|
359
|
+
await ctx.session.prompt({
|
|
360
|
+
sessionID: invocation.sessionID,
|
|
361
|
+
text: prompt,
|
|
362
|
+
delivery: invocation.delivery,
|
|
363
|
+
})
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
await ctx.rpc.register(rpc, {
|
|
368
|
+
status: async () => ({ enabled: config.commands.enabled }),
|
|
369
|
+
snapshot: ({ sessionID }) =>
|
|
370
|
+
serial(sessionID, async () => {
|
|
371
|
+
const { state, messages } = await load(sessionID)
|
|
372
|
+
syncCompressionBlocks(state, logger, messages)
|
|
373
|
+
return {
|
|
374
|
+
manualMode: !!state.manualMode,
|
|
375
|
+
canCompress:
|
|
376
|
+
state.compressPermission === "allow" &&
|
|
377
|
+
(!state.isSubAgent || config.experimental.allowSubAgents),
|
|
378
|
+
...(state.compressPermission === "ask"
|
|
379
|
+
? {
|
|
380
|
+
blockedReason:
|
|
381
|
+
"Permission 'ask' is not supported by the V2 plugin API yet.",
|
|
382
|
+
}
|
|
383
|
+
: {}),
|
|
384
|
+
context: analyzeContextTokens(state, messages),
|
|
385
|
+
stats: await buildStatsReport(state, logger),
|
|
386
|
+
}
|
|
387
|
+
}),
|
|
388
|
+
manual: ({ sessionID, enabled }) =>
|
|
389
|
+
serial(sessionID, async () => {
|
|
390
|
+
const { state } = await load(sessionID)
|
|
391
|
+
state.manualMode = enabled ? "active" : false
|
|
392
|
+
await saveSessionState(state, logger)
|
|
393
|
+
return {}
|
|
394
|
+
}),
|
|
395
|
+
})
|
|
396
|
+
logger.info("DCP V2 initialized")
|
|
397
|
+
return () => {
|
|
398
|
+
sessions.clear()
|
|
399
|
+
limits.clear()
|
|
400
|
+
}
|
|
401
|
+
}
|