diff --git a/src/features/context-injector/injector.test.ts b/src/features/context-injector/injector.test.ts index ae0924d..565bd86 100644 --- a/src/features/context-injector/injector.test.ts +++ b/src/features/context-injector/injector.test.ts @@ -234,11 +234,11 @@ describe("createContextInjectorMessagesTransformHook", () => { expect(output.messages[3].parts[0].text).toBe("Second message") }) - it("does nothing when no pending context", async () => { + it("does nothing when no pending context and no keywords", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) const sessionID = "ses_transform2" - const messages = [createMockMessage("user", "Message", sessionID)] + const messages = [createMockMessage("user", "Hello world", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any const output = { messages } as any @@ -249,6 +249,63 @@ describe("createContextInjectorMessagesTransformHook", () => { expect(output.messages.length).toBe(1) }) + it("injects synthetic message when user message contains ulw keyword", async () => { + // #given + const hook = createContextInjectorMessagesTransformHook(collector) + const sessionID = "ses_transform_keyword" + const messages = [createMockMessage("user", "ulw do this task", sessionID)] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const output = { messages } as any + + // #when + await hook["experimental.chat.messages.transform"]!({}, output) + + // #then + expect(output.messages.length).toBe(2) + expect(output.messages[0].parts[0].synthetic).toBe(true) + expect(output.messages[0].parts[0].text).toContain("ultrawork") + expect(output.messages[1].parts[0].text).toBe("ulw do this task") + }) + + it("injects synthetic message when user message contains ultrawork keyword", async () => { + // #given + const hook = createContextInjectorMessagesTransformHook(collector) + const sessionID = "ses_transform_ultrawork" + const messages = [createMockMessage("user", "ultrawork please", sessionID)] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const output = { messages } as any + + // #when + await hook["experimental.chat.messages.transform"]!({}, output) + + // #then + expect(output.messages.length).toBe(2) + expect(output.messages[0].parts[0].synthetic).toBe(true) + }) + + it("combines keyword context with collector context", async () => { + // #given + const hook = createContextInjectorMessagesTransformHook(collector) + const sessionID = "ses_transform_combined" + collector.register(sessionID, { + id: "extra", + source: "custom", + content: "Extra context from collector", + }) + const messages = [createMockMessage("user", "ulw combined test", sessionID)] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const output = { messages } as any + + // #when + await hook["experimental.chat.messages.transform"]!({}, output) + + // #then + expect(output.messages.length).toBe(2) + const syntheticText = output.messages[0].parts[0].text + expect(syntheticText).toContain("ultrawork") + expect(syntheticText).toContain("Extra context from collector") + }) + it("does nothing when no user messages", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) diff --git a/src/features/context-injector/injector.ts b/src/features/context-injector/injector.ts index 4ed1ebe..1a3fb2a 100644 --- a/src/features/context-injector/injector.ts +++ b/src/features/context-injector/injector.ts @@ -1,6 +1,7 @@ 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 @@ -83,22 +84,6 @@ export function createContextInjectorMessagesTransformHook( return } - const lastMessage = messages[messages.length - 1] - const sessionID = (lastMessage.info as unknown as { sessionID?: string }).sessionID - if (!sessionID) { - log("[context-injector] messages.transform: no sessionID on last message") - return - } - - const hasPending = collector.hasPending(sessionID) - log("[context-injector] messages.transform check", { - sessionID, - hasPending, - messageCount: messages.length, - }) - - if (!hasPending) return - let lastUserMessageIndex = -1 for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].info.role === "user") { @@ -112,14 +97,45 @@ export function createContextInjectorMessagesTransformHook( return } - const pending = collector.consume(sessionID) - if (!pending.hasContent) { - log("[context-injector] messages.transform: pending was empty") + 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 refMessage = messages[lastUserMessageIndex] - const refInfo = refMessage.info as unknown as { + 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) { + return + } + + const mergedContext = contextParts.join("\n\n") + + const refInfo = lastUserMessage.info as unknown as { sessionID?: string agent?: string model?: { providerID?: string; modelID?: string } @@ -146,7 +162,7 @@ export function createContextInjectorMessagesTransformHook( sessionID: sessionID, messageID: syntheticMessageId, type: "text", - text: pending.merged, + text: mergedContext, synthetic: true, time: { start: now, end: now }, } as Part, @@ -158,7 +174,8 @@ export function createContextInjectorMessagesTransformHook( log("[context-injector] Injected synthetic message", { sessionID, insertIndex: lastUserMessageIndex, - contextLength: pending.merged.length, + contextLength: mergedContext.length, + keywordTypes: detectedKeywords.map((k) => k.type), newMessageCount: messages.length, }) },