From f665d0ea8aedacc637ce98cd7aa271d915f13ea2 Mon Sep 17 00:00:00 2001 From: prts101 <1kUnD0G@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:05:11 +0800 Subject: [PATCH] Add chat.message fallback for /dcp-compress when host skips command registration --- README.md | 14 ++++++++ index.ts | 8 +++++ lib/hooks.ts | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/README.md b/README.md index 771fce67..f3b92059 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,20 @@ DCP provides a TUI panel and one prompt-producing slash command: - `/dcp` — Opens the DCP panel with context, stats, and manual-mode controls. - `/dcp-compress [focus]` — Asks the model to run one compression pass. Optional focus text directs what content to compress, following the active `compress.mode`. +> [!NOTE] +> 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`: +> +> ```jsonc +> { +> "command": { +> "dcp-compress": { +> "template": "", +> "description": "Trigger DCP manual compression with: /dcp-compress [focus]" +> } +> } +> } +> ``` + ### Prompt Overrides DCP exposes six editable prompts: diff --git a/index.ts b/index.ts index 3f11e392..650d7a06 100644 --- a/index.ts +++ b/index.ts @@ -10,6 +10,7 @@ import { Logger } from "./lib/logger" import { createSessionState } from "./lib/state" import { PromptStore } from "./lib/prompts/store" import { + createChatMessageHandler, createChatMessageTransformHandler, createCommandExecuteHandler, createEventHandler, @@ -77,6 +78,13 @@ const server: Plugin = (async (ctx) => { ctx.directory, hostPermissions, ), + "chat.message": createChatMessageHandler( + ctx.client, + state, + logger, + config, + hostPermissions, + ), event: createEventHandler(state, logger), tool: { ...(config.compress.permission !== "deny" && { diff --git a/lib/hooks.ts b/lib/hooks.ts index 6d6f3862..fe7e493d 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -289,6 +289,99 @@ export function createCommandExecuteHandler( } } +export function createChatMessageHandler( + client: any, + state: SessionState, + logger: Logger, + config: PluginConfig, + hostPermissions: HostPermissionSnapshot, +) { + return async ( + input: { sessionID: string }, + output: { parts: any[] }, + ) => { + if (!config.commands.enabled) { + return + } + + // Fallback for hosts that never registered the dcp-compress command + // (e.g. OpenCode builds its slash-command snapshot before plugin + // config hooks run). A plain-text "/dcp-compress [focus]" message is + // intercepted here and routed through the same manual-trigger path as + // the slash command. + const textPart = output.parts.find( + (part) => + part?.type === "text" && + typeof part.text === "string" && + !part.ignored && + !part.synthetic, + ) + if (!textPart) { + return + } + + const trimmed = textPart.text.trim() + if (!trimmed.startsWith("/dcp-compress")) { + return + } + + // The slash-command path already set a pending trigger for this turn; + // don't double-process it. + if (state.pendingManualTrigger) { + return + } + + const messagesResponse = await client.session.messages({ + path: { id: input.sessionID }, + }) + const messages = filterMessages(messagesResponse.data || messagesResponse) + + await ensureSessionInitialized( + client, + state, + input.sessionID, + logger, + messages, + config.manualMode.enabled, + ) + + syncCompressPermissionState(state, config, hostPermissions, messages) + + if (compressPermission(state, config) === "deny") { + return + } + + const focus = trimmed.slice("/dcp-compress".length).trim() + const prompt = await handleManualTriggerCommand( + { + client, + state, + config, + logger, + sessionId: input.sessionID, + messages, + }, + "compress", + focus, + ) + if (!prompt) { + return + } + + state.manualMode = "compress-pending" + state.pendingManualTrigger = { + sessionId: input.sessionID, + prompt, + } + // Normalize the stored message to the canonical marker so downstream + // pruning/summary logic sees exactly what the slash-command path emits. + textPart.text = focus ? `/dcp-compress ${focus}` : "/dcp-compress" + logger.info("Intercepted plain-text /dcp-compress message", { + sessionId: input.sessionID, + }) + } +} + export function createTextCompleteHandler() { return async ( _input: { sessionID: string; messageID: string; partID: string },