diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 18f2bf40..59497873 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -233,6 +233,10 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { return mf.IsAgentReadyForInitialPrompt(config.AgentType, message) } + formatToolCall := func(message string) (string, []string) { + return mf.FormatToolCall(config.AgentType, message) + } + conversation := st.NewConversation(ctx, st.ConversationConfig{ AgentType: config.AgentType, AgentIO: config.Process, @@ -243,6 +247,8 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { ScreenStabilityLength: 2 * time.Second, FormatMessage: formatMessage, ReadyForInitialPrompt: isAgentReadyForInitialPrompt, + FormatToolCall: formatToolCall, + Logger: logger, }, config.InitialPrompt) emitter := NewEventEmitter(1024) diff --git a/lib/msgfmt/format_tool_call.go b/lib/msgfmt/format_tool_call.go new file mode 100644 index 00000000..8af1fdca --- /dev/null +++ b/lib/msgfmt/format_tool_call.go @@ -0,0 +1,82 @@ +package msgfmt + +import ( + "strings" +) + +func removeClaudeReportTaskToolCall(msg string) (string, []string) { + msg = "\n" + msg // This handles the case where the message starts with a tool call + + // Remove all tool calls that start with `● coder - coder_report_task (MCP)` + lines := strings.Split(msg, "\n") + + toolCallStartIdx := -1 + + // Store all tool call start and end indices [[start, end], ...] + var toolCallIdxs [][]int + + for i := 1; i < len(lines)-1; i++ { + prevLine := strings.TrimSpace(lines[i-1]) + line := strings.TrimSpace(lines[i]) + nextLine := strings.TrimSpace(lines[i+1]) + + if strings.Contains(line, "coder - coder_report_task (MCP)") { + toolCallStartIdx = i + } else if toolCallStartIdx != -1 && line == "\"message\": \"Thanks for reporting!\"" && nextLine == "}" && strings.HasSuffix(prevLine, "{") { + // Store [start, end] pair + toolCallIdxs = append(toolCallIdxs, []int{toolCallStartIdx, min(len(lines), i+2)}) + + // Reset to find the next tool call + toolCallStartIdx = -1 + } + } + + // If no tool calls found, return original message + if len(toolCallIdxs) == 0 { + return strings.TrimLeft(msg, "\n"), []string{} + } + + toolCallMessages := make([]string, 0) + + // Remove tool calls from the message + for i := len(toolCallIdxs) - 1; i >= 0; i-- { + idxPair := toolCallIdxs[i] + start, end := idxPair[0], idxPair[1] + + toolCallMessages = append(toolCallMessages, strings.Join(lines[start:end], "\n")) + + lines = append(lines[:start], lines[end:]...) + } + return strings.TrimLeft(strings.Join(lines, "\n"), "\n"), toolCallMessages +} + +func FormatToolCall(agentType AgentType, message string) (string, []string) { + switch agentType { + case AgentTypeClaude: + return removeClaudeReportTaskToolCall(message) + case AgentTypeGoose: + return message, []string{} + case AgentTypeAider: + return message, []string{} + case AgentTypeCodex: + return message, []string{} + case AgentTypeGemini: + return message, []string{} + case AgentTypeCopilot: + return message, []string{} + case AgentTypeAmp: + return message, []string{} + case AgentTypeCursor: + return message, []string{} + case AgentTypeAuggie: + return message, []string{} + case AgentTypeAmazonQ: + return message, []string{} + case AgentTypeOpencode: + return message, []string{} + case AgentTypeCustom: + return message, []string{} + default: + return message, []string{} + } +} diff --git a/lib/msgfmt/message_box.go b/lib/msgfmt/message_box.go index d7370c60..13efaf11 100644 --- a/lib/msgfmt/message_box.go +++ b/lib/msgfmt/message_box.go @@ -100,53 +100,3 @@ func removeAmpMessageBox(msg string) string { } return formattedMsg } - -func removeClaudeReportTaskToolCall(msg string) string { - // Remove all tool calls that start with `● coder - coder_report_task (MCP)` till we encounter the next line starting with ● - lines := strings.Split(msg, "\n") - - toolCallStartIdx := -1 - newLineAfterToolCallIdx := -1 - - // Store all tool call start and end indices [[start, end], ...] - var toolCallIdxs [][]int - - for i := 0; i < len(lines); i++ { - line := strings.TrimSpace(lines[i]) - - if strings.HasPrefix(line, "● coder - coder_report_task (MCP)") { - toolCallStartIdx = i - } else if toolCallStartIdx != -1 && strings.HasPrefix(line, "●") { - // Store [start, end] pair - toolCallIdxs = append(toolCallIdxs, []int{toolCallStartIdx, i}) - - // Reset to find the next tool call - toolCallStartIdx = -1 - newLineAfterToolCallIdx = -1 - } - if len(line) == 0 && toolCallStartIdx != -1 && newLineAfterToolCallIdx == -1 { - newLineAfterToolCallIdx = i - } - } - - // Handle the case where the last tool call goes till the end of the message - // And a failsafe when the next message is not prefixed with ● - if toolCallStartIdx != -1 && newLineAfterToolCallIdx != -1 { - toolCallIdxs = append(toolCallIdxs, []int{toolCallStartIdx, newLineAfterToolCallIdx}) - } - - // If no tool calls found, return original message - if len(toolCallIdxs) == 0 { - return msg - } - - // Remove tool calls from the message - for i := len(toolCallIdxs) - 1; i >= 0; i-- { - idxPair := toolCallIdxs[i] - start, end := idxPair[0], idxPair[1] - - lines = append(lines[:start], lines[end:]...) - } - - return strings.Join(lines, "\n") -} diff --git a/lib/msgfmt/msgfmt.go b/lib/msgfmt/msgfmt.go index 975dc7f6..3fccc2c3 100644 --- a/lib/msgfmt/msgfmt.go +++ b/lib/msgfmt/msgfmt.go @@ -254,14 +254,6 @@ func formatGenericMessage(message string, userInput string, agentType AgentType) return message } -func formatClaudeMessage(message string, userInput string) string { - message = RemoveUserInput(message, userInput, AgentTypeClaude) - message = removeMessageBox(message) - message = removeClaudeReportTaskToolCall(message) - message = trimEmptyLines(message) - return message -} - func formatCodexMessage(message string, userInput string) string { message = RemoveUserInput(message, userInput, AgentTypeCodex) message = removeCodexInputBox(message) @@ -286,7 +278,7 @@ func formatAmpMessage(message string, userInput string) string { func FormatAgentMessage(agentType AgentType, message string, userInput string) string { switch agentType { case AgentTypeClaude: - return formatClaudeMessage(message, userInput) + return formatGenericMessage(message, userInput, agentType) case AgentTypeGoose: return formatGenericMessage(message, userInput, agentType) case AgentTypeAider: diff --git a/lib/msgfmt/msgfmt_test.go b/lib/msgfmt/msgfmt_test.go index 780a3954..37c3d6a2 100644 --- a/lib/msgfmt/msgfmt_test.go +++ b/lib/msgfmt/msgfmt_test.go @@ -233,7 +233,8 @@ func TestFormatAgentMessage(t *testing.T) { assert.NoError(t, err) expected, err := testdataDir.ReadFile(path.Join(dir, string(agentType), c.Name(), "expected.txt")) assert.NoError(t, err) - assert.Equal(t, string(expected), FormatAgentMessage(agentType, string(msg), string(userInput))) + output, _ := FormatToolCall(agentType, FormatAgentMessage(agentType, string(msg), string(userInput))) + assert.Equal(t, string(expected), output) }) } }) diff --git a/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt index f06f58f6..caef0663 100644 --- a/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt +++ b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/expected.txt @@ -1,6 +1,7 @@ ● I'll build a snake game for you. Let me start by reporting my progress and creating a task list. + ● Now I'll create a complete snake game with HTML, CSS, and JavaScript: @@ -19,6 +20,8 @@ padding: 0; … +334 lines (ctrl+o to expand) + + ● I've built a complete snake game for you! The game is saved at /home/coder/snake-game.html. @@ -36,4 +39,4 @@ How to play: Open the HTML file in your web browser and use the arrow keys to move the snake. Collect the red food to grow and increase - your score! \ No newline at end of file + your score! diff --git a/lib/msgfmt/testdata/format/claude/remove-task-tool-call/msg.txt b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/msg.txt index 384e1094..6c8a0acd 100644 --- a/lib/msgfmt/testdata/format/claude/remove-task-tool-call/msg.txt +++ b/lib/msgfmt/testdata/format/claude/remove-task-tool-call/msg.txt @@ -1,5 +1,13 @@ > Build a snake game +● coder - coder_report_task (MCP)(summary: "Snake game created + successfully at snake-game.html", + link: "file:///home/coder/snake-ga + me.html", state: "working") + ⎿ { + "message": "Thanks for reporting!" + } + ● I'll build a snake game for you. Let me start by reporting my progress and creating a task list. diff --git a/lib/screentracker/conversation.go b/lib/screentracker/conversation.go index 043f4409..97a74722 100644 --- a/lib/screentracker/conversation.go +++ b/lib/screentracker/conversation.go @@ -3,6 +3,7 @@ package screentracker import ( "context" "fmt" + "log/slog" "strings" "sync" "time" @@ -43,6 +44,9 @@ type ConversationConfig struct { SkipSendMessageStatusCheck bool // ReadyForInitialPrompt detects whether the agent has initialized and is ready to accept the initial prompt ReadyForInitialPrompt func(message string) bool + // FormatToolCall removes the coder report_task tool call from the agent message and also returns the array of removed tool calls + FormatToolCall func(message string) (string, []string) + Logger *slog.Logger } type ConversationRole string @@ -82,6 +86,8 @@ type Conversation struct { InitialPromptSent bool // ReadyForInitialPrompt keeps track if the agent is ready to accept the initial prompt ReadyForInitialPrompt bool + // toolCallMessageSet keeps track of the tool calls that have been detected & logged in the current agent message + toolCallMessageSet map[string]bool } type ConversationStatus string @@ -115,8 +121,9 @@ func NewConversation(ctx context.Context, cfg ConversationConfig, initialPrompt Time: cfg.GetTime(), }, }, - InitialPrompt: initialPrompt, - InitialPromptSent: len(initialPrompt) == 0, + InitialPrompt: initialPrompt, + InitialPromptSent: len(initialPrompt) == 0, + toolCallMessageSet: make(map[string]bool), } return c } @@ -205,9 +212,19 @@ func (c *Conversation) lastMessage(role ConversationRole) ConversationMessage { func (c *Conversation) updateLastAgentMessage(screen string, timestamp time.Time) { agentMessage := FindNewMessage(c.screenBeforeLastUserMessage, screen, c.cfg.AgentType) lastUserMessage := c.lastMessage(ConversationRoleUser) + var toolCalls []string if c.cfg.FormatMessage != nil { agentMessage = c.cfg.FormatMessage(agentMessage, lastUserMessage.Message) } + if c.cfg.FormatToolCall != nil { + agentMessage, toolCalls = c.cfg.FormatToolCall(agentMessage) + } + for _, toolCall := range toolCalls { + if c.toolCallMessageSet[toolCall] == false { + c.toolCallMessageSet[toolCall] = true + c.cfg.Logger.Info("Tool call detected", "toolCall", toolCall) + } + } shouldCreateNewMessage := len(c.messages) == 0 || c.messages[len(c.messages)-1].Role == ConversationRoleUser lastAgentMessage := c.lastMessage(ConversationRoleAgent) if lastAgentMessage.Message == agentMessage { @@ -220,6 +237,10 @@ func (c *Conversation) updateLastAgentMessage(screen string, timestamp time.Time } if shouldCreateNewMessage { c.messages = append(c.messages, conversationMessage) + + // Cleanup + c.toolCallMessageSet = make(map[string]bool) + } else { c.messages[len(c.messages)-1] = conversationMessage }