Skip to content

Commit f665d0e

Browse files
committed
Add chat.message fallback for /dcp-compress when host skips command registration
1 parent 11f6517 commit f665d0e

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ DCP provides a TUI panel and one prompt-producing slash command:
192192
- `/dcp` — Opens the DCP panel with context, stats, and manual-mode controls.
193193
- `/dcp-compress [focus]` — Asks the model to run one compression pass. Optional focus text directs what content to compress, following the active `compress.mode`.
194194

195+
> [!NOTE]
196+
> On some OpenCode versions the slash-command list is snapshotted before plugin `config` hooks run, so `/dcp-compress` may not appear in the autocomplete list even though DCP loaded correctly. Typing `/dcp-compress [focus]` as a plain message still works — DCP intercepts it via its `chat.message` hook and triggers compression identically. To also restore autocomplete, declare the command in `opencode.json`:
197+
>
198+
> ```jsonc
199+
> {
200+
> "command": {
201+
> "dcp-compress": {
202+
> "template": "",
203+
> "description": "Trigger DCP manual compression with: /dcp-compress [focus]"
204+
> }
205+
> }
206+
> }
207+
> ```
208+
195209
### Prompt Overrides
196210
197211
DCP exposes six editable prompts:

index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Logger } from "./lib/logger"
1010
import { createSessionState } from "./lib/state"
1111
import { PromptStore } from "./lib/prompts/store"
1212
import {
13+
createChatMessageHandler,
1314
createChatMessageTransformHandler,
1415
createCommandExecuteHandler,
1516
createEventHandler,
@@ -77,6 +78,13 @@ const server: Plugin = (async (ctx) => {
7778
ctx.directory,
7879
hostPermissions,
7980
),
81+
"chat.message": createChatMessageHandler(
82+
ctx.client,
83+
state,
84+
logger,
85+
config,
86+
hostPermissions,
87+
),
8088
event: createEventHandler(state, logger),
8189
tool: {
8290
...(config.compress.permission !== "deny" && {

lib/hooks.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,99 @@ export function createCommandExecuteHandler(
289289
}
290290
}
291291

292+
export function createChatMessageHandler(
293+
client: any,
294+
state: SessionState,
295+
logger: Logger,
296+
config: PluginConfig,
297+
hostPermissions: HostPermissionSnapshot,
298+
) {
299+
return async (
300+
input: { sessionID: string },
301+
output: { parts: any[] },
302+
) => {
303+
if (!config.commands.enabled) {
304+
return
305+
}
306+
307+
// Fallback for hosts that never registered the dcp-compress command
308+
// (e.g. OpenCode builds its slash-command snapshot before plugin
309+
// config hooks run). A plain-text "/dcp-compress [focus]" message is
310+
// intercepted here and routed through the same manual-trigger path as
311+
// the slash command.
312+
const textPart = output.parts.find(
313+
(part) =>
314+
part?.type === "text" &&
315+
typeof part.text === "string" &&
316+
!part.ignored &&
317+
!part.synthetic,
318+
)
319+
if (!textPart) {
320+
return
321+
}
322+
323+
const trimmed = textPart.text.trim()
324+
if (!trimmed.startsWith("/dcp-compress")) {
325+
return
326+
}
327+
328+
// The slash-command path already set a pending trigger for this turn;
329+
// don't double-process it.
330+
if (state.pendingManualTrigger) {
331+
return
332+
}
333+
334+
const messagesResponse = await client.session.messages({
335+
path: { id: input.sessionID },
336+
})
337+
const messages = filterMessages(messagesResponse.data || messagesResponse)
338+
339+
await ensureSessionInitialized(
340+
client,
341+
state,
342+
input.sessionID,
343+
logger,
344+
messages,
345+
config.manualMode.enabled,
346+
)
347+
348+
syncCompressPermissionState(state, config, hostPermissions, messages)
349+
350+
if (compressPermission(state, config) === "deny") {
351+
return
352+
}
353+
354+
const focus = trimmed.slice("/dcp-compress".length).trim()
355+
const prompt = await handleManualTriggerCommand(
356+
{
357+
client,
358+
state,
359+
config,
360+
logger,
361+
sessionId: input.sessionID,
362+
messages,
363+
},
364+
"compress",
365+
focus,
366+
)
367+
if (!prompt) {
368+
return
369+
}
370+
371+
state.manualMode = "compress-pending"
372+
state.pendingManualTrigger = {
373+
sessionId: input.sessionID,
374+
prompt,
375+
}
376+
// Normalize the stored message to the canonical marker so downstream
377+
// pruning/summary logic sees exactly what the slash-command path emits.
378+
textPart.text = focus ? `/dcp-compress ${focus}` : "/dcp-compress"
379+
logger.info("Intercepted plain-text /dcp-compress message", {
380+
sessionId: input.sessionID,
381+
})
382+
}
383+
}
384+
292385
export function createTextCompleteHandler() {
293386
return async (
294387
_input: { sessionID: string; messageID: string; partID: string },

0 commit comments

Comments
 (0)