diff --git a/core/core.ts b/core/core.ts index 2357d8687..8aa6a55ae 100644 --- a/core/core.ts +++ b/core/core.ts @@ -29,6 +29,7 @@ import { editConfigFile, migrateV1DevDataFiles } from "./util/paths"; import { Telemetry } from "./util/posthog"; import { isProcessBackgrounded, + killTerminalProcess, markProcessAsBackgrounded, } from "./util/processTerminalStates"; import { getSymbolsForManyFiles } from "./util/treeSitter"; @@ -1057,9 +1058,6 @@ export class Core { ); on("process/killTerminalProcess", async ({ data: { toolCallId } }) => { - const { killTerminalProcess } = await import( - "./util/processTerminalStates.js" - ); await killTerminalProcess(toolCallId); }); diff --git a/core/util/messageContent.ts b/core/util/messageContent.ts index 31c447389..5a0bc95bf 100644 --- a/core/util/messageContent.ts +++ b/core/util/messageContent.ts @@ -35,6 +35,21 @@ export function renderContextItems(contextItems: ContextItem[]): string { return contextItems.map((item) => item.content).join("\n\n"); } +export function renderContextItemsWithStatus(contextItems: any[]): string { + return contextItems + .map((item) => { + let result = item.content; + + // If this item has a status, append it directly after the content + if (item.status) { + result += `\n[Status: ${item.status}]`; + } + + return result; + }) + .join("\n\n"); +} + export function normalizeToMessageParts(message: ChatMessage): MessagePart[] { switch (message.role) { case "user": diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx index 66d35f3cb..d67187161 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/StreamingToolbar.tsx @@ -12,15 +12,11 @@ export function StreamingToolbar({ }: StreamingToolbarProps) { const jetbrains = isJetBrains(); - const handleStop = () => { - onStop(); - }; - return (
{displayText} diff --git a/gui/src/redux/util/constructMessages.test.ts b/gui/src/redux/util/constructMessages.test.ts index d4c5e572e..b79d8da14 100644 --- a/gui/src/redux/util/constructMessages.test.ts +++ b/gui/src/redux/util/constructMessages.test.ts @@ -8,6 +8,7 @@ import { ToolResultChatMessage, UserChatMessage, } from "core"; +import { BuiltInToolNames } from "core/tools/builtIn"; import { CANCELLED_TOOL_CALL_MESSAGE, NO_TOOL_CALL_OUTPUT_MESSAGE, @@ -712,4 +713,346 @@ describe("constructMessages", () => { expect(messages[0].content).toContain("Base System Message"); expect(messages[0].content).not.toContain(LAST_MESSAGE_RULE.rule); }); + + // Tests for the specific block in lines 135-142 handling toolCallState.output + describe("toolCallState.output handling (lines 135-142)", () => { + test("should use CANCELLED_TOOL_CALL_MESSAGE for cancelled tool calls", () => { + const assistantWithToolCall: AssistantChatMessage = { + role: "assistant", + content: "I'll run a command", + toolCalls: [ + { + id: "tool-call-1", + type: "function", + function: { + name: "some_tool", + arguments: '{"command": "ls"}', + }, + }, + ], + }; + + mockHistory = [ + { + message: assistantWithToolCall, + contextItems: [], + toolCallStates: [ + { + toolCallId: "tool-call-1", + toolCall: { + id: "tool-call-1", + type: "function", + function: { + name: "some_tool", + arguments: '{"command": "ls"}', + }, + }, + status: "canceled", + parsedArgs: { command: "ls" }, + output: [ + createContextItem( + "result", + "This should be ignored due to cancelled status", + ), + ], + }, + ], + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.content).toBe(CANCELLED_TOOL_CALL_MESSAGE); + expect(toolMessage.content).not.toContain("This should be ignored"); + }); + + test("should use renderContextItemsWithStatus for RunTerminalCommand with output", () => { + const assistantWithTerminalCall: AssistantChatMessage = { + role: "assistant", + content: "I'll run a terminal command", + toolCalls: [ + { + id: "terminal-call-1", + type: "function", + function: { + name: BuiltInToolNames.RunTerminalCommand, + arguments: '{"command": "ls -la"}', + }, + }, + ], + }; + + const terminalOutputWithStatus = [ + { + ...createContextItem("terminal-output", "file1.txt\nfile2.txt"), + status: "completed", + }, + { + ...createContextItem("terminal-error", "Warning: deprecated flag"), + status: "warning", + }, + ]; + + mockHistory = [ + { + message: assistantWithTerminalCall, + contextItems: [], + toolCallStates: [ + { + toolCallId: "terminal-call-1", + toolCall: { + id: "terminal-call-1", + type: "function", + function: { + name: BuiltInToolNames.RunTerminalCommand, + arguments: '{"command": "ls -la"}', + }, + }, + status: "done", + parsedArgs: { command: "ls -la" }, + output: terminalOutputWithStatus, + }, + ], + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.toolCallId).toBe("terminal-call-1"); + // Should contain content with status appended + expect(toolMessage.content).toContain("file1.txt\nfile2.txt"); + expect(toolMessage.content).toContain("[Status: completed]"); + expect(toolMessage.content).toContain("Warning: deprecated flag"); + expect(toolMessage.content).toContain("[Status: warning]"); + // Should use renderContextItemsWithStatus format with double newlines between items + expect(toolMessage.content).toMatch( + /file1\.txt[\s\S]*\[Status: completed\][\s\S]*\n\n[\s\S]*Warning.*\[Status: warning\]/, + ); + }); + + test("should use renderContextItems for non-RunTerminalCommand tools with output", () => { + const assistantWithSearchCall: AssistantChatMessage = { + role: "assistant", + content: "I'll search for that", + toolCalls: [ + { + id: "search-call-1", + type: "function", + function: { + name: "search", // Not RunTerminalCommand + arguments: '{"query": "test"}', + }, + }, + ], + }; + + const searchOutput = [ + createContextItem("result1", "First search result"), + createContextItem("result2", "Second search result"), + ]; + + mockHistory = [ + { + message: assistantWithSearchCall, + contextItems: [], + toolCallStates: [ + { + toolCallId: "search-call-1", + toolCall: { + id: "search-call-1", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + status: "done", + parsedArgs: { query: "test" }, + output: searchOutput, + }, + ], + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.toolCallId).toBe("search-call-1"); + // Should use renderContextItems format (no status, double newlines between items) + expect(toolMessage.content).toBe( + "First search result\n\nSecond search result", + ); + expect(toolMessage.content).not.toContain("[Status:"); + }); + + test("should use NO_TOOL_CALL_OUTPUT_MESSAGE when no output exists", () => { + const assistantWithToolCall: AssistantChatMessage = { + role: "assistant", + content: "I'll search for that", + toolCalls: [ + { + id: "search-call-1", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + ], + }; + + mockHistory = [ + { + message: assistantWithToolCall, + contextItems: [], + toolCallStates: [ + { + toolCallId: "search-call-1", + toolCall: { + id: "search-call-1", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + status: "generating", + parsedArgs: { query: "test" }, + // No output field + }, + ], + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.toolCallId).toBe("search-call-1"); + expect(toolMessage.content).toBe(NO_TOOL_CALL_OUTPUT_MESSAGE); + }); + + test("should use NO_TOOL_CALL_OUTPUT_MESSAGE when toolCallState is undefined", () => { + const assistantWithToolCall: AssistantChatMessage = { + role: "assistant", + content: "I'll search for that", + toolCalls: [ + { + id: "search-call-1", + type: "function", + function: { + name: "search", + arguments: '{"query": "test"}', + }, + }, + ], + }; + + mockHistory = [ + { + message: assistantWithToolCall, + contextItems: [], + // No toolCallStates array + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.toolCallId).toBe("search-call-1"); + expect(toolMessage.content).toBe(NO_TOOL_CALL_OUTPUT_MESSAGE); + }); + + test("should prioritize cancelled status over output content", () => { + const assistantWithTerminalCall: AssistantChatMessage = { + role: "assistant", + content: "I'll run a command", + toolCalls: [ + { + id: "terminal-call-1", + type: "function", + function: { + name: BuiltInToolNames.RunTerminalCommand, + arguments: '{"command": "ls"}', + }, + }, + ], + }; + + mockHistory = [ + { + message: assistantWithTerminalCall, + contextItems: [], + toolCallStates: [ + { + toolCallId: "terminal-call-1", + toolCall: { + id: "terminal-call-1", + type: "function", + function: { + name: BuiltInToolNames.RunTerminalCommand, + arguments: '{"command": "ls"}', + }, + }, + status: "canceled", // Cancelled status + parsedArgs: { command: "ls" }, + output: [ + createContextItem( + "terminal-output", + "This output should be ignored", + ), + ], // Has output but is cancelled + }, + ], + }, + ]; + + const { messages } = constructMessages( + mockHistory, + "Base System Message", + mockRules, + {}, + ); + + const toolMessage = messages[2] as ToolResultChatMessage; + expect(toolMessage.role).toBe("tool"); + expect(toolMessage.toolCallId).toBe("terminal-call-1"); + // Should use cancelled message, not the output content + expect(toolMessage.content).toBe(CANCELLED_TOOL_CALL_MESSAGE); + expect(toolMessage.content).not.toContain( + "This output should be ignored", + ); + }); + }); }); diff --git a/gui/src/redux/util/constructMessages.ts b/gui/src/redux/util/constructMessages.ts index 666a1bfd9..9b3593082 100644 --- a/gui/src/redux/util/constructMessages.ts +++ b/gui/src/redux/util/constructMessages.ts @@ -10,6 +10,7 @@ import { import { chatMessageIsEmpty } from "core/llm/messages"; import { getSystemMessageWithRules } from "core/llm/rules/getSystemMessageWithRules"; import { RulePolicies } from "core/llm/rules/types"; +import { BuiltInToolNames } from "core/tools/builtIn"; import { CANCELLED_TOOL_CALL_MESSAGE, NO_TOOL_CALL_OUTPUT_MESSAGE, @@ -17,25 +18,16 @@ import { import { convertToolCallStatesToSystemCallsAndOutput } from "core/tools/systemMessageTools/convertSystemTools"; import { SystemMessageToolsFramework } from "core/tools/systemMessageTools/types"; import { findLast, findLastIndex } from "core/util/findLast"; -import { normalizeToMessageParts } from "core/util/messageContent"; +import { + normalizeToMessageParts, + renderContextItems, + renderContextItemsWithStatus, +} from "core/util/messageContent"; import { toolCallStateToContextItems } from "../../pages/gui/ToolCallDiv/utils"; // Helper function to render context items and append status information // Helper function to render context items and append status information -function renderContextItemsWithStatus(contextItems: any[]): string { - return contextItems - .map((item) => { - let result = item.content; - // If this item has a status, append it directly after the content - if (item.status) { - result += `\n[Status: ${item.status}]`; - } - - return result; - }) - .join("\n\n"); -} interface MessageWithContextItems { ctxItems: ContextItemWithId[]; message: ChatMessage; @@ -146,8 +138,14 @@ export function constructMessages( if (toolCallState?.status === "canceled") { content = CANCELLED_TOOL_CALL_MESSAGE; - } else if (toolCallState?.output) { + } else if ( + toolCallState?.output && + toolCall.function?.name == BuiltInToolNames.RunTerminalCommand + ) { + // Add status for tools containing detailed status outcomes per context item content = renderContextItemsWithStatus(toolCallState.output); + } else if (toolCallState?.output) { + content = renderContextItems(toolCallState?.output); } msgs.push({