Skip to content

Commit c1becdb

Browse files
committed
Add configurable pruning summary modes and fix input minimization for state-modifying tools
1 parent b635b3c commit c1becdb

File tree

5 files changed

+69
-10
lines changed

5 files changed

+69
-10
lines changed

index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const plugin: Plugin = (async (ctx) => {
5656
const stateManager = new StateManager()
5757
const toolParametersCache = new Map<string, any>() // callID -> parameters
5858
const modelCache = new Map<string, { providerID: string; modelID: string }>() // sessionID -> model info
59-
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode)
59+
const janitor = new Janitor(ctx.client, stateManager, logger, toolParametersCache, config.protectedTools, modelCache, config.model, config.showModelErrorToasts, config.pruningMode, config.pruning_summary)
6060

6161
const cacheToolParameters = (messages: any[], component: string) => {
6262
for (const message of messages) {
@@ -244,6 +244,7 @@ const plugin: Plugin = (async (ctx) => {
244244
protectedTools: config.protectedTools,
245245
model: config.model,
246246
pruningMode: config.pruningMode,
247+
pruning_summary: config.pruning_summary,
247248
globalConfigFile: join(homedir(), ".config", "opencode", "dcp.jsonc"),
248249
projectConfigFile: ctx.directory ? join(ctx.directory, ".opencode", "dcp.jsonc") : "N/A",
249250
logDirectory: join(homedir(), ".config", "opencode", "logs", "dcp"),

lib/config.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ export interface PluginConfig {
1313
model?: string // Format: "provider/model" (e.g., "anthropic/claude-haiku-4-5")
1414
showModelErrorToasts?: boolean // Show toast notifications when model selection fails
1515
pruningMode: "auto" | "smart" // Pruning strategy: auto (deduplication only) or smart (deduplication + LLM analysis)
16+
pruning_summary: "off" | "minimal" | "detailed" // UI summary display mode
1617
}
1718

1819
const defaultConfig: PluginConfig = {
1920
enabled: true, // Plugin is enabled by default
2021
debug: false, // Disable debug logging by default
2122
protectedTools: ['task', 'todowrite', 'todoread'], // Tools that should never be pruned (including stateful tools)
2223
showModelErrorToasts: true, // Show model error toasts by default
23-
pruningMode: 'smart' // Default to smart mode (deduplication + LLM analysis)
24+
pruningMode: 'smart', // Default to smart mode (deduplication + LLM analysis)
25+
pruning_summary: 'detailed' // Default to detailed summary
2426
}
2527

2628
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode')
@@ -109,6 +111,12 @@ function createDefaultConfig(): void {
109111
// "smart": Deduplication + AI analysis for intelligent pruning (recommended)
110112
"pruningMode": "smart",
111113
114+
// Pruning summary display mode:
115+
// "off": No UI summary (silent pruning)
116+
// "minimal": Show tokens saved and count (e.g., "Saved ~2.5K tokens (6 tools pruned)")
117+
// "detailed": Show full breakdown by tool type and pruning method (default)
118+
"pruning_summary": "detailed",
119+
112120
// List of tools that should never be pruned from context
113121
// "task": Each subagent invocation is intentional
114122
// "todowrite"/"todoread": Stateful tools where each call matters
@@ -161,7 +169,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig {
161169
protectedTools: globalConfig.protectedTools ?? config.protectedTools,
162170
model: globalConfig.model ?? config.model,
163171
showModelErrorToasts: globalConfig.showModelErrorToasts ?? config.showModelErrorToasts,
164-
pruningMode: globalConfig.pruningMode ?? config.pruningMode
172+
pruningMode: globalConfig.pruningMode ?? config.pruningMode,
173+
pruning_summary: globalConfig.pruning_summary ?? config.pruning_summary
165174
}
166175
logger.info('config', 'Loaded global config', { path: configPaths.global })
167176
}
@@ -181,7 +190,8 @@ export function getConfig(ctx?: PluginInput): PluginConfig {
181190
protectedTools: projectConfig.protectedTools ?? config.protectedTools,
182191
model: projectConfig.model ?? config.model,
183192
showModelErrorToasts: projectConfig.showModelErrorToasts ?? config.showModelErrorToasts,
184-
pruningMode: projectConfig.pruningMode ?? config.pruningMode
193+
pruningMode: projectConfig.pruningMode ?? config.pruningMode,
194+
pruning_summary: projectConfig.pruning_summary ?? config.pruning_summary
185195
}
186196
logger.info('config', 'Loaded project config (overrides global)', { path: configPaths.project })
187197
}

lib/janitor.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export class Janitor {
1616
private modelCache: Map<string, { providerID: string; modelID: string }>,
1717
private configModel?: string, // Format: "provider/model"
1818
private showModelErrorToasts: boolean = true, // Whether to show toast for model errors
19-
private pruningMode: "auto" | "smart" = "smart" // Pruning strategy
19+
private pruningMode: "auto" | "smart" = "smart", // Pruning strategy
20+
private pruningSummary: "off" | "minimal" | "detailed" = "detailed" // UI summary display mode
2021
) { }
2122

2223
/**
@@ -583,6 +584,26 @@ export class Janitor {
583584
return toolsSummary
584585
}
585586

587+
/**
588+
* Send minimal summary notification (just tokens saved and count)
589+
*/
590+
private async sendMinimalNotification(
591+
sessionID: string,
592+
totalPruned: number,
593+
toolOutputs: Map<string, string>,
594+
prunedIds: string[]
595+
) {
596+
if (totalPruned === 0) return
597+
598+
const tokensSaved = await this.calculateTokensSaved(prunedIds, toolOutputs)
599+
const tokensFormatted = formatTokenCount(tokensSaved)
600+
const toolText = totalPruned === 1 ? 'tool' : 'tools'
601+
602+
const message = `🧹 DCP: Saved ~${tokensFormatted} tokens (${totalPruned} ${toolText} pruned)`
603+
604+
await this.sendIgnoredMessage(sessionID, message)
605+
}
606+
586607
/**
587608
* Auto mode notification - shows only deduplication results
588609
*/
@@ -594,6 +615,16 @@ export class Janitor {
594615
) {
595616
if (deduplicatedIds.length === 0) return
596617

618+
// Check if notifications are disabled
619+
if (this.pruningSummary === 'off') return
620+
621+
// Send minimal notification if configured
622+
if (this.pruningSummary === 'minimal') {
623+
await this.sendMinimalNotification(sessionID, deduplicatedIds.length, toolOutputs, deduplicatedIds)
624+
return
625+
}
626+
627+
// Otherwise send detailed notification
597628
// Calculate token savings
598629
const tokensSaved = await this.calculateTokensSaved(deduplicatedIds, toolOutputs)
599630
const tokensFormatted = formatTokenCount(tokensSaved)
@@ -647,6 +678,17 @@ export class Janitor {
647678
const totalPruned = deduplicatedIds.length + llmPrunedIds.length
648679
if (totalPruned === 0) return
649680

681+
// Check if notifications are disabled
682+
if (this.pruningSummary === 'off') return
683+
684+
// Send minimal notification if configured
685+
if (this.pruningSummary === 'minimal') {
686+
const allPrunedIds = [...deduplicatedIds, ...llmPrunedIds]
687+
await this.sendMinimalNotification(sessionID, totalPruned, toolOutputs, allPrunedIds)
688+
return
689+
}
690+
691+
// Otherwise send detailed notification
650692
// Calculate token savings
651693
const allPrunedIds = [...deduplicatedIds, ...llmPrunedIds]
652694
const tokensSaved = await this.calculateTokensSaved(allPrunedIds, toolOutputs)

lib/logger.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ export class Logger {
8989
? idsMatch[1].split(',').map(id => id.trim())
9090
: []
9191

92-
// Extract session history (between "Session history:\n" and "\n\nYou MUST respond")
92+
// Extract session history (between "Session history" and "\n\nYou MUST respond")
9393
// The captured text has literal newlines, so we need to escape them back to \n for valid JSON
94-
const historyMatch = prompt.match(/Session history:\s*\n([\s\S]*?)\n\nYou MUST respond/)
94+
const historyMatch = prompt.match(/Session history[^\n]*:\s*\n([\s\S]*?)\n\nYou MUST respond/)
9595
let sessionHistory: any[] = []
9696

9797
if (historyMatch) {
@@ -113,7 +113,8 @@ export class Logger {
113113

114114
// Extract response schema (after "You MUST respond with valid JSON matching this exact schema:")
115115
// Note: The schema contains "..." placeholders which aren't valid JSON, so we save it as a string
116-
const schemaMatch = prompt.match(/matching this exact schema:\s*\n(\{[\s\S]*?\})\s*\n\nReturn ONLY/)
116+
// Now matches until end of prompt since we removed the "Return ONLY..." line
117+
const schemaMatch = prompt.match(/matching this exact schema:\s*\n(\{[\s\S]*?\})\s*$/)
117118
const responseSchema = schemaMatch
118119
? schemaMatch[1] // Keep as string since it has "..." placeholders
119120
: null

lib/prompt.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,13 @@ function minimizeMessages(messages: any[], alreadyPrunedIds?: string[], protecte
6565
if (part.state?.input) {
6666
const input = part.state.input
6767

68-
// For file operations, just keep the file path
69-
if (input.filePath) {
68+
// For write/edit tools, keep file path AND content (what was changed matters)
69+
// These tools: write, edit, multiedit, patch
70+
if (input.filePath && (part.tool === 'write' || part.tool === 'edit' || part.tool === 'multiedit' || part.tool === 'patch')) {
71+
toolPart.input = input // Keep full input (content, oldString, newString, etc.)
72+
}
73+
// For read-only file operations, just keep the file path
74+
else if (input.filePath) {
7075
toolPart.input = { filePath: input.filePath }
7176
}
7277
// For batch operations, summarize instead of full array

0 commit comments

Comments
 (0)