import type { OhMyOpenCodeConfig } from "../config" import type { PluginContext } from "./types" import { hasConnectedProvidersCache } from "../shared" import { setSessionModel } from "../shared/session-model-state" import { setSessionAgent } from "../features/claude-code-session-state" import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override" import type { CreatedHooks } from "../create-hooks" type FirstMessageVariantGate = { shouldOverride: (sessionID: string) => boolean markApplied: (sessionID: string) => void } type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } export type ChatMessageHandlerOutput = { message: Record; parts: ChatMessagePart[] } export type ChatMessageInput = { sessionID: string agent?: string model?: { providerID: string; modelID: string } } type StartWorkHookOutput = { parts: Array<{ type: string; text?: string }> } function isStartWorkHookOutput(value: unknown): value is StartWorkHookOutput { if (typeof value !== "object" || value === null) return false const record = value as Record const partsValue = record["parts"] if (!Array.isArray(partsValue)) return false return partsValue.every((part) => { if (typeof part !== "object" || part === null) return false const partRecord = part as Record return typeof partRecord["type"] === "string" }) } export function createChatMessageHandler(args: { ctx: PluginContext pluginConfig: OhMyOpenCodeConfig firstMessageVariantGate: FirstMessageVariantGate hooks: CreatedHooks }): ( input: ChatMessageInput, output: ChatMessageHandlerOutput ) => Promise { const { ctx, pluginConfig, firstMessageVariantGate, hooks } = args return async ( input: ChatMessageInput, output: ChatMessageHandlerOutput ): Promise => { if (input.agent) { setSessionAgent(input.sessionID, input.agent) } const message = output.message if (firstMessageVariantGate.shouldOverride(input.sessionID)) { firstMessageVariantGate.markApplied(input.sessionID) } await hooks.modelFallback?.["chat.message"]?.(input, output) const modelOverride = output.message["model"] if ( modelOverride && typeof modelOverride === "object" && "providerID" in modelOverride && "modelID" in modelOverride ) { const providerID = (modelOverride as { providerID?: string }).providerID const modelID = (modelOverride as { modelID?: string }).modelID if (typeof providerID === "string" && typeof modelID === "string") { setSessionModel(input.sessionID, { providerID, modelID }) } } else if (input.model) { setSessionModel(input.sessionID, input.model) } await hooks.stopContinuationGuard?.["chat.message"]?.(input) await hooks.runtimeFallback?.["chat.message"]?.(input, output) await hooks.keywordDetector?.["chat.message"]?.(input, output) await hooks.claudeCodeHooks?.["chat.message"]?.(input, output) await hooks.autoSlashCommand?.["chat.message"]?.(input, output) await hooks.noSisyphusGpt?.["chat.message"]?.(input, output) await hooks.noHephaestusNonGpt?.["chat.message"]?.(input, output) if (hooks.startWork && isStartWorkHookOutput(output)) { await hooks.startWork["chat.message"]?.(input, output) } if (!hasConnectedProvidersCache()) { ctx.client.tui .showToast({ body: { title: "⚠️ Provider Cache Missing", message: "Model filtering disabled. RESTART OpenCode to enable full functionality.", variant: "warning" as const, duration: 6000, }, }) .catch(() => {}) } if (hooks.ralphLoop) { const parts = output.parts const promptText = parts ?.filter((p) => p.type === "text" && p.text) .map((p) => p.text) .join("\n") .trim() || "" const isRalphLoopTemplate = promptText.includes("You are starting a Ralph Loop") && promptText.includes("") const isCancelRalphTemplate = promptText.includes( "Cancel the currently active Ralph Loop", ) if (isRalphLoopTemplate) { const taskMatch = promptText.match(/\s*([\s\S]*?)\s*<\/user-task>/i) const rawTask = taskMatch?.[1]?.trim() || "" const quotedMatch = rawTask.match(/^["'](.+?)["']/) const prompt = quotedMatch?.[1] || rawTask.split(/\s+--/)[0]?.trim() || "Complete the task as instructed" const maxIterMatch = rawTask.match(/--max-iterations=(\d+)/i) const promiseMatch = rawTask.match( /--completion-promise=["']?([^"'\s]+)["']?/i, ) hooks.ralphLoop.startLoop(input.sessionID, prompt, { maxIterations: maxIterMatch ? parseInt(maxIterMatch[1], 10) : undefined, completionPromise: promiseMatch?.[1], }) } else if (isCancelRalphTemplate) { hooks.ralphLoop.cancelLoop(input.sessionID) } } applyUltraworkModelOverrideOnMessage(pluginConfig, input.agent, output, ctx.client.tui, input.sessionID) } }