refactor(keyword): unify keyword injection into UserPromptSubmit pipeline
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)
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import type { ContextCollector } from "./collector"
|
import type { ContextCollector } from "./collector"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk"
|
import type { Message, Part } from "@opencode-ai/sdk"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
import { detectKeywordsWithType, extractPromptText } from "../../hooks/keyword-detector"
|
|
||||||
|
|
||||||
interface OutputPart {
|
interface OutputPart {
|
||||||
type: string
|
type: string
|
||||||
@@ -80,7 +79,6 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
"experimental.chat.messages.transform": async (_input, output) => {
|
"experimental.chat.messages.transform": async (_input, output) => {
|
||||||
const { messages } = output
|
const { messages } = output
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
log("[context-injector] messages.transform: no messages")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,47 +91,23 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastUserMessageIndex === -1) {
|
if (lastUserMessageIndex === -1) {
|
||||||
log("[context-injector] messages.transform: no user message found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUserMessage = messages[lastUserMessageIndex]
|
const lastUserMessage = messages[lastUserMessageIndex]
|
||||||
const sessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID
|
const sessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
log("[context-injector] messages.transform: no sessionID on last user message")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const userParts = lastUserMessage.parts as Array<{ type: string; text?: string }>
|
if (!collector.hasPending(sessionID)) {
|
||||||
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) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedContext = contextParts.join("\n\n")
|
const pending = collector.consume(sessionID)
|
||||||
|
if (!pending.hasContent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const refInfo = lastUserMessage.info as unknown as {
|
const refInfo = lastUserMessage.info as unknown as {
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
@@ -162,7 +136,7 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
sessionID: sessionID,
|
sessionID: sessionID,
|
||||||
messageID: syntheticMessageId,
|
messageID: syntheticMessageId,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: mergedContext,
|
text: pending.merged,
|
||||||
synthetic: true,
|
synthetic: true,
|
||||||
time: { start: now, end: now },
|
time: { start: now, end: now },
|
||||||
} as Part,
|
} as Part,
|
||||||
@@ -171,11 +145,10 @@ export function createContextInjectorMessagesTransformHook(
|
|||||||
|
|
||||||
messages.splice(lastUserMessageIndex, 0, syntheticMessage)
|
messages.splice(lastUserMessageIndex, 0, syntheticMessage)
|
||||||
|
|
||||||
log("[context-injector] Injected synthetic message", {
|
log("[context-injector] Injected synthetic message from collector", {
|
||||||
sessionID,
|
sessionID,
|
||||||
insertIndex: lastUserMessageIndex,
|
insertIndex: lastUserMessageIndex,
|
||||||
contextLength: mergedContext.length,
|
contextLength: pending.merged.length,
|
||||||
keywordTypes: detectedKeywords.map((k) => k.type),
|
|
||||||
newMessageCount: messages.length,
|
newMessageCount: messages.length,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage }
|
|||||||
import type { PluginConfig } from "./types"
|
import type { PluginConfig } from "./types"
|
||||||
import { log, isHookDisabled } from "../../shared"
|
import { log, isHookDisabled } from "../../shared"
|
||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||||
|
import { detectKeywordsWithType, removeCodeBlocks } from "../keyword-detector"
|
||||||
|
|
||||||
const sessionFirstMessageProcessed = new Set<string>()
|
const sessionFirstMessageProcessed = new Set<string>()
|
||||||
const sessionErrorState = new Map<string, { hasError: boolean; errorMessage?: string }>()
|
const sessionErrorState = new Map<string, { hasError: boolean; errorMessage?: string }>()
|
||||||
@@ -137,9 +138,21 @@ export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.messages.length > 0) {
|
const detectedKeywords = detectKeywordsWithType(removeCodeBlocks(prompt))
|
||||||
const hookContent = result.messages.join("\n\n")
|
const keywordMessages = detectedKeywords.map((k) => k.message)
|
||||||
log(`[claude-code-hooks] Injecting ${result.messages.length} hook messages`, { sessionID: input.sessionID, contentLength: hookContent.length, isFirstMessage })
|
|
||||||
|
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) {
|
if (isFirstMessage) {
|
||||||
const idx = output.parts.findIndex((p) => p.type === "text" && p.text)
|
const idx = output.parts.findIndex((p) => p.type === "text" && p.text)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { detectKeywordsWithType, extractPromptText } from "./detector"
|
import { detectKeywordsWithType, extractPromptText, removeCodeBlocks } from "./detector"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
import { contextCollector } from "../../features/context-injector"
|
|
||||||
|
|
||||||
export * from "./detector"
|
export * from "./detector"
|
||||||
export * from "./constants"
|
export * from "./constants"
|
||||||
@@ -24,10 +23,9 @@ export function createKeywordDetectorHook(ctx: PluginInput) {
|
|||||||
}
|
}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const promptText = extractPromptText(output.parts)
|
const promptText = extractPromptText(output.parts)
|
||||||
const detectedKeywords = detectKeywordsWithType(promptText)
|
const detectedKeywords = detectKeywordsWithType(removeCodeBlocks(promptText))
|
||||||
const messages = detectedKeywords.map((k) => k.message)
|
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (detectedKeywords.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,20 +48,9 @@ export function createKeywordDetectorHook(ctx: PluginInput) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = messages.join("\n")
|
log(`[keyword-detector] Detected ${detectedKeywords.length} keywords`, {
|
||||||
|
|
||||||
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`, {
|
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
contextLength: context.length,
|
types: detectedKeywords.map((k) => k.type),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user