Skip to content

Commit 09df074

Browse files
authored
Merge pull request #284 from Opencode-DCP/feat/commands-config
feat: add commands config for enabling/disabling slash commands
2 parents 3312e9a + 9d2309a commit 09df074

File tree

6 files changed

+139
-4
lines changed

6 files changed

+139
-4
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ DCP uses its own config file:
7171
"debug": false,
7272
// Notification display: "off", "minimal", or "detailed"
7373
"pruneNotification": "detailed",
74+
// Enable or disable slash commands
75+
"commands": {
76+
"context": {
77+
"enabled": true,
78+
},
79+
"stats": {
80+
"enabled": true,
81+
},
82+
},
7483
// Protect from pruning for <turns> message turns
7584
"turnProtection": {
7685
"enabled": false,
@@ -126,6 +135,13 @@ DCP uses its own config file:
126135

127136
</details>
128137

138+
### Commands
139+
140+
DCP provides two slash commands for visibility into context usage:
141+
142+
- `/dcp-context` — Shows a breakdown of your current session's token usage by category (system, user, assistant, tools, etc.) and how much has been saved through pruning.
143+
- `/dcp-stats` — Shows cumulative pruning statistics across all sessions.
144+
129145
### Turn Protection
130146

131147
When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `discard` and `extract` tools, as well as automatic strategies.

dcp.schema.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,37 @@
2626
"default": "detailed",
2727
"description": "Level of notification shown when pruning occurs"
2828
},
29+
"commands": {
30+
"type": "object",
31+
"description": "Enable or disable slash commands",
32+
"additionalProperties": false,
33+
"properties": {
34+
"context": {
35+
"type": "object",
36+
"description": "Configuration for /dcp-context command",
37+
"additionalProperties": false,
38+
"properties": {
39+
"enabled": {
40+
"type": "boolean",
41+
"default": true,
42+
"description": "Enable the /dcp-context command"
43+
}
44+
}
45+
},
46+
"stats": {
47+
"type": "object",
48+
"description": "Configuration for /dcp-stats command",
49+
"additionalProperties": false,
50+
"properties": {
51+
"enabled": {
52+
"type": "boolean",
53+
"default": true,
54+
"description": "Enable the /dcp-stats command"
55+
}
56+
}
57+
}
58+
}
59+
},
2960
"turnProtection": {
3061
"type": "object",
3162
"description": "Protect recent tool outputs from being pruned",

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const plugin: Plugin = (async (ctx) => {
9494
)
9595
}
9696
},
97-
"command.execute.before": createCommandExecuteHandler(ctx.client, state, logger),
97+
"command.execute.before": createCommandExecuteHandler(ctx.client, state, logger, config),
9898
}
9999
}) satisfies Plugin
100100

lib/commands/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,12 @@ function formatContextMessage(breakdown: TokenBreakdown): string {
197197
let labelWithPct: string
198198
let valueStr: string
199199
if ("isSaved" in cat && cat.isSaved) {
200-
labelWithPct = cat.label.padEnd(16)
200+
labelWithPct = cat.label.padEnd(17)
201201
valueStr = `${formatTokenCount(cat.value).replace(" tokens", "").padStart(6)} saved`
202202
} else {
203203
const percentage =
204204
breakdown.total > 0 ? ((cat.value / breakdown.total) * 100).toFixed(1) : "0.0"
205-
labelWithPct = `${cat.label.padEnd(9)} ${percentage.padStart(5)}%`
205+
labelWithPct = `${cat.label.padEnd(9)} ${percentage.padStart(5)}% `
206206
valueStr = formatTokenCount(cat.value).padStart(13)
207207
}
208208

lib/config.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,20 @@ export interface TurnProtection {
4545
turns: number
4646
}
4747

48+
export interface CommandConfig {
49+
enabled: boolean
50+
}
51+
52+
export interface Commands {
53+
context: CommandConfig
54+
stats: CommandConfig
55+
}
56+
4857
export interface PluginConfig {
4958
enabled: boolean
5059
debug: boolean
5160
pruneNotification: "off" | "minimal" | "detailed"
61+
commands: Commands
5262
turnProtection: TurnProtection
5363
protectedFilePatterns: string[]
5464
tools: Tools
@@ -84,6 +94,11 @@ export const VALID_CONFIG_KEYS = new Set([
8494
"turnProtection.enabled",
8595
"turnProtection.turns",
8696
"protectedFilePatterns",
97+
"commands",
98+
"commands.context",
99+
"commands.context.enabled",
100+
"commands.stats",
101+
"commands.stats.enabled",
87102
"tools",
88103
"tools.settings",
89104
"tools.settings.nudgeEnabled",
@@ -196,6 +211,28 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
196211
}
197212
}
198213

214+
// Commands validators
215+
const commands = config.commands
216+
if (commands) {
217+
if (
218+
commands.context?.enabled !== undefined &&
219+
typeof commands.context.enabled !== "boolean"
220+
) {
221+
errors.push({
222+
key: "commands.context.enabled",
223+
expected: "boolean",
224+
actual: typeof commands.context.enabled,
225+
})
226+
}
227+
if (commands.stats?.enabled !== undefined && typeof commands.stats.enabled !== "boolean") {
228+
errors.push({
229+
key: "commands.stats.enabled",
230+
expected: "boolean",
231+
actual: typeof commands.stats.enabled,
232+
})
233+
}
234+
}
235+
199236
// Tools validators
200237
const tools = config.tools
201238
if (tools) {
@@ -388,6 +425,14 @@ const defaultConfig: PluginConfig = {
388425
enabled: true,
389426
debug: false,
390427
pruneNotification: "detailed",
428+
commands: {
429+
context: {
430+
enabled: true,
431+
},
432+
stats: {
433+
enabled: true,
434+
},
435+
},
391436
turnProtection: {
392437
enabled: false,
393438
turns: 4,
@@ -498,6 +543,15 @@ function createDefaultConfig(): void {
498543
"debug": false,
499544
// Notification display: "off", "minimal", or "detailed"
500545
"pruneNotification": "detailed",
546+
// Enable or disable slash commands
547+
"commands": {
548+
"context": {
549+
"enabled": true
550+
},
551+
"stats": {
552+
"enabled": true
553+
}
554+
},
501555
// Protect from pruning for <turns> message turns
502556
"turnProtection": {
503557
"enabled": false,
@@ -637,9 +691,29 @@ function mergeTools(
637691
}
638692
}
639693

694+
function mergeCommands(
695+
base: PluginConfig["commands"],
696+
override?: Partial<PluginConfig["commands"]>,
697+
): PluginConfig["commands"] {
698+
if (!override) return base
699+
700+
return {
701+
context: {
702+
enabled: override.context?.enabled ?? base.context.enabled,
703+
},
704+
stats: {
705+
enabled: override.stats?.enabled ?? base.stats.enabled,
706+
},
707+
}
708+
}
709+
640710
function deepCloneConfig(config: PluginConfig): PluginConfig {
641711
return {
642712
...config,
713+
commands: {
714+
context: { ...config.commands.context },
715+
stats: { ...config.commands.stats },
716+
},
643717
turnProtection: { ...config.turnProtection },
644718
protectedFilePatterns: [...config.protectedFilePatterns],
645719
tools: {
@@ -693,6 +767,7 @@ export function getConfig(ctx: PluginInput): PluginConfig {
693767
enabled: result.data.enabled ?? config.enabled,
694768
debug: result.data.debug ?? config.debug,
695769
pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
770+
commands: mergeCommands(config.commands, result.data.commands as any),
696771
turnProtection: {
697772
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
698773
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
@@ -735,6 +810,7 @@ export function getConfig(ctx: PluginInput): PluginConfig {
735810
enabled: result.data.enabled ?? config.enabled,
736811
debug: result.data.debug ?? config.debug,
737812
pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
813+
commands: mergeCommands(config.commands, result.data.commands as any),
738814
turnProtection: {
739815
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
740816
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
@@ -774,6 +850,7 @@ export function getConfig(ctx: PluginInput): PluginConfig {
774850
enabled: result.data.enabled ?? config.enabled,
775851
debug: result.data.debug ?? config.debug,
776852
pruneNotification: result.data.pruneNotification ?? config.pruneNotification,
853+
commands: mergeCommands(config.commands, result.data.commands as any),
777854
turnProtection: {
778855
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
779856
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,

lib/hooks.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,20 @@ export function createChatMessageTransformHandler(
7979
}
8080
}
8181

82-
export function createCommandExecuteHandler(client: any, state: SessionState, logger: Logger) {
82+
export function createCommandExecuteHandler(
83+
client: any,
84+
state: SessionState,
85+
logger: Logger,
86+
config: PluginConfig,
87+
) {
8388
return async (
8489
input: { command: string; sessionID: string; arguments: string },
8590
_output: { parts: any[] },
8691
) => {
8792
if (input.command === "dcp-stats") {
93+
if (!config.commands.stats.enabled) {
94+
return
95+
}
8896
const messagesResponse = await client.session.messages({
8997
path: { id: input.sessionID },
9098
})
@@ -99,6 +107,9 @@ export function createCommandExecuteHandler(client: any, state: SessionState, lo
99107
throw new Error("__DCP_STATS_HANDLED__")
100108
}
101109
if (input.command === "dcp-context") {
110+
if (!config.commands.context.enabled) {
111+
return
112+
}
102113
const messagesResponse = await client.session.messages({
103114
path: { id: input.sessionID },
104115
})

0 commit comments

Comments
 (0)