From 87134d3390bf333dae1b94e0de8996325083f180 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 5 Jan 2026 02:37:01 +0900 Subject: [PATCH] refactor(keyword): unify keyword injection into UserPromptSubmit pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move keyword detection from experimental.chat.messages.transform to claude-code-hooks chat.message handler. Uses proven injectHookMessage file system approach for reliable context injection. 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- src/features/context-injector/injector.ts | 43 +++++------------------ src/hooks/claude-code-hooks/index.ts | 19 ++++++++-- src/hooks/keyword-detector/index.ts | 23 +++--------- 3 files changed, 29 insertions(+), 56 deletions(-) diff --git a/src/features/context-injector/injector.ts b/src/features/context-injector/injector.ts index 1a3fb2a..2a8ccbd 100644 --- a/src/features/context-injector/injector.ts +++ b/src/features/context-injector/injector.ts @@ -1,7 +1,6 @@ import type { ContextCollector } from "./collector" import type { Message, Part } from "@opencode-ai/sdk" import { log } from "../../shared" -import { detectKeywordsWithType, extractPromptText } from "../../hooks/keyword-detector" interface OutputPart { type: string @@ -80,7 +79,6 @@ export function createContextInjectorMessagesTransformHook( "experimental.chat.messages.transform": async (_input, output) => { const { messages } = output if (messages.length === 0) { - log("[context-injector] messages.transform: no messages") return } @@ -93,47 +91,23 @@ export function createContextInjectorMessagesTransformHook( } if (lastUserMessageIndex === -1) { - log("[context-injector] messages.transform: no user message found") return } const lastUserMessage = messages[lastUserMessageIndex] const sessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID if (!sessionID) { - log("[context-injector] messages.transform: no sessionID on last user message") return } - const userParts = lastUserMessage.parts as Array<{ type: string; text?: string }> - const promptText = extractPromptText(userParts) - const detectedKeywords = detectKeywordsWithType(promptText) - const hasPendingFromCollector = collector.hasPending(sessionID) - - log("[context-injector] messages.transform check", { - sessionID, - detectedKeywords: detectedKeywords.map((k) => k.type), - hasPendingFromCollector, - messageCount: messages.length, - }) - - const contextParts: string[] = [] - - for (const keyword of detectedKeywords) { - contextParts.push(keyword.message) - } - - if (hasPendingFromCollector) { - const pending = collector.consume(sessionID) - if (pending.hasContent) { - contextParts.push(pending.merged) - } - } - - if (contextParts.length === 0) { + if (!collector.hasPending(sessionID)) { return } - const mergedContext = contextParts.join("\n\n") + const pending = collector.consume(sessionID) + if (!pending.hasContent) { + return + } const refInfo = lastUserMessage.info as unknown as { sessionID?: string @@ -162,7 +136,7 @@ export function createContextInjectorMessagesTransformHook( sessionID: sessionID, messageID: syntheticMessageId, type: "text", - text: mergedContext, + text: pending.merged, synthetic: true, time: { start: now, end: now }, } as Part, @@ -171,11 +145,10 @@ export function createContextInjectorMessagesTransformHook( messages.splice(lastUserMessageIndex, 0, syntheticMessage) - log("[context-injector] Injected synthetic message", { + log("[context-injector] Injected synthetic message from collector", { sessionID, insertIndex: lastUserMessageIndex, - contextLength: mergedContext.length, - keywordTypes: detectedKeywords.map((k) => k.type), + contextLength: pending.merged.length, newMessageCount: messages.length, }) }, diff --git a/src/hooks/claude-code-hooks/index.ts b/src/hooks/claude-code-hooks/index.ts index 6c77cf5..9f84639 100644 --- a/src/hooks/claude-code-hooks/index.ts +++ b/src/hooks/claude-code-hooks/index.ts @@ -28,6 +28,7 @@ import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } import type { PluginConfig } from "./types" import { log, isHookDisabled } from "../../shared" import { injectHookMessage } from "../../features/hook-message-injector" +import { detectKeywordsWithType, removeCodeBlocks } from "../keyword-detector" const sessionFirstMessageProcessed = new Set() const sessionErrorState = new Map() @@ -137,9 +138,21 @@ export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig return } - if (result.messages.length > 0) { - const hookContent = result.messages.join("\n\n") - log(`[claude-code-hooks] Injecting ${result.messages.length} hook messages`, { sessionID: input.sessionID, contentLength: hookContent.length, isFirstMessage }) + const detectedKeywords = detectKeywordsWithType(removeCodeBlocks(prompt)) + const keywordMessages = detectedKeywords.map((k) => k.message) + + if (keywordMessages.length > 0) { + log("[claude-code-hooks] Detected keywords", { + sessionID: input.sessionID, + types: detectedKeywords.map((k) => k.type), + }) + } + + const allMessages = [...keywordMessages, ...result.messages] + + if (allMessages.length > 0) { + const hookContent = allMessages.join("\n\n") + log(`[claude-code-hooks] Injecting ${allMessages.length} messages (${keywordMessages.length} keyword + ${result.messages.length} hook)`, { sessionID: input.sessionID, contentLength: hookContent.length, isFirstMessage }) if (isFirstMessage) { const idx = output.parts.findIndex((p) => p.type === "text" && p.text) diff --git a/src/hooks/keyword-detector/index.ts b/src/hooks/keyword-detector/index.ts index e7a7886..7b30560 100644 --- a/src/hooks/keyword-detector/index.ts +++ b/src/hooks/keyword-detector/index.ts @@ -1,7 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { detectKeywordsWithType, extractPromptText } from "./detector" +import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector" import { log } from "../../shared" -import { contextCollector } from "../../features/context-injector" export * from "./detector" export * from "./constants" @@ -24,10 +23,9 @@ export function createKeywordDetectorHook(ctx: PluginInput) { } ): Promise => { const promptText = extractPromptText(output.parts) - const detectedKeywords = detectKeywordsWithType(promptText) - const messages = detectedKeywords.map((k) => k.message) + const detectedKeywords = detectKeywordsWithType(removeCodeBlocks(promptText)) - if (messages.length === 0) { + if (detectedKeywords.length === 0) { return } @@ -50,20 +48,9 @@ export function createKeywordDetectorHook(ctx: PluginInput) { ) } - const context = messages.join("\n") - - for (const keyword of detectedKeywords) { - contextCollector.register(input.sessionID, { - id: `keyword-${keyword.type}`, - source: "keyword-detector", - content: keyword.message, - priority: keyword.type === "ultrawork" ? "critical" : "high", - }) - } - - log(`[keyword-detector] Registered ${messages.length} keyword contexts`, { + log(`[keyword-detector] Detected ${detectedKeywords.length} keywords`, { sessionID: input.sessionID, - contextLength: context.length, + types: detectedKeywords.map((k) => k.type), }) }, }