From 6624803a525041d023a291c65f35c74dc62c1253 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 3 Apr 2026 22:13:02 +0900 Subject: [PATCH] fix(plugin): handle raw /ulw-loop commands appearing after injected messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix parseRawLoopSlashCommand to correctly extract and parse loop commands that appear after injected background task messages (e.g., "[BACKGROUND TASK COMPLETED]"). Previously, the function only checked if the entire message started with a slash command, failing when injected content preceded the command. 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode) --- src/plugin/chat-message.test.ts | 45 +++++++++++++++++++++++++++++++++ src/plugin/chat-message.ts | 13 +++++++--- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts index 91cc869b7..45ce7df99 100644 --- a/src/plugin/chat-message.test.ts +++ b/src/plugin/chat-message.test.ts @@ -136,6 +136,51 @@ describe("createChatMessageHandler - /ulw-loop raw slash fallback", () => { }, ]) }) + + test("starts ultrawork loop when injected messages appear before the raw /ulw-loop command", async () => { + // given + const startLoopCalls: Array<{ + sessionID: string + prompt: string + options: Record + }> = [] + const args = createMockHandlerArgs() + args.hooks.ralphLoop = { + startLoop: (sessionID: string, prompt: string, options?: Record) => { + startLoopCalls.push({ sessionID, prompt, options: options ?? {} }) + return true + }, + cancelLoop: () => true, + } + const handler = createChatMessageHandler(args) + const input = createMockInput("sisyphus") + const output: ChatMessageHandlerOutput = { + message: {}, + parts: [ + { + type: "text", + text: "[BACKGROUND TASK COMPLETED]\nPlan finished.\n\n---\n\n/ulw-loop \"Ship feature\" --strategy=continue", + }, + ], + } + + // when + await handler(input, output) + + // then + expect(startLoopCalls).toEqual([ + { + sessionID: "test-session", + prompt: "Ship feature", + options: { + ultrawork: true, + maxIterations: undefined, + completionPromise: undefined, + strategy: "continue", + }, + }, + ]) + }) }) function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) { diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index 42e120bcd..0d69ce3d4 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -90,17 +90,24 @@ function getStoredMainSessionModel( function parseRawLoopSlashCommand(promptText: string): RawLoopCommand | null { const trimmed = promptText.trim() + const commandText = trimmed.startsWith("/") + ? trimmed + : trimmed + .split("\n") + .map((line) => line.trim()) + .filter((line) => /^\/(?:ralph-loop|ulw-loop|cancel-ralph)\b/i.test(line)) + .at(-1) - if (!trimmed.startsWith("/")) { + if (!commandText) { return null } - const cancelMatch = trimmed.match(/^\/cancel-ralph(?:\s+.*)?$/i) + const cancelMatch = commandText.match(/^\/cancel-ralph(?:\s+.*)?$/i) if (cancelMatch) { return { command: "cancel-ralph", args: "" } } - const loopMatch = trimmed.match(/^\/(ralph-loop|ulw-loop)\s*([\s\S]*)$/i) + const loopMatch = commandText.match(/^\/(ralph-loop|ulw-loop)\s*([\s\S]*)$/i) if (!loopMatch) { return null }