diff --git a/lib/msgfmt/format_tool_call.go b/lib/msgfmt/format_tool_call.go index 5befafa5..b5ead1f7 100644 --- a/lib/msgfmt/format_tool_call.go +++ b/lib/msgfmt/format_tool_call.go @@ -125,6 +125,68 @@ func removeCodexReportTaskToolCall(msg string) (string, []string) { return strings.TrimRight(strings.Join(lines, "\n"), "\n"), toolCallMessages } +func removeCopilotReportTaskToolCall(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_report_task:` + lines := strings.Split(msg, "\n") + + toolCallStartIdx := -1 + newLineAfterToolCall := -1 + + // Store all tool call start and end indices [[start, end], ...] + var toolCallIdxs []toolCallRange + + for i := 0; i < len(lines); i++ { + line := strings.Trim(strings.TrimSpace(lines[i]), "\n") + + if strings.Contains(line, "● coder_report_task:") { + toolCallStartIdx = i + } else if toolCallStartIdx != -1 { + if strings.Contains(line, "{\"message\":\"Thanks for reporting!\"}") { + // Store [start, end] pair + toolCallIdxs = append(toolCallIdxs, toolCallRange{toolCallStartIdx, min(len(lines), i+2), false}) + + // Reset to find the next tool call + toolCallStartIdx = -1 + newLineAfterToolCall = -1 + } else if len(line) == 0 { + newLineAfterToolCall = i + 1 + } + } + } + + // Handle the malformed/partially rendered tool_calls + // Note: This case has not yet been observed in Copilot + if toolCallStartIdx != -1 { + if newLineAfterToolCall != -1 { + toolCallIdxs = append(toolCallIdxs, toolCallRange{toolCallStartIdx, newLineAfterToolCall, true}) + } else { + toolCallIdxs = append(toolCallIdxs, toolCallRange{toolCallStartIdx, len(lines), true}) + } + } + + // 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-- { + start, end := toolCallIdxs[i].start, toolCallIdxs[i].end + + // If the toolCall is malformed, we don't want to log it + if !toolCallIdxs[i].malformed { + toolCallMessages = append(toolCallMessages, strings.Join(lines[start:end], "\n")) + } + + lines = append(lines[:start], lines[end:]...) + } + return strings.Trim(strings.Join(lines, "\n"), "\n"), toolCallMessages +} + func FormatToolCall(agentType AgentType, message string) (string, []string) { switch agentType { case AgentTypeClaude: @@ -138,7 +200,7 @@ func FormatToolCall(agentType AgentType, message string) (string, []string) { case AgentTypeGemini: return message, []string{} case AgentTypeCopilot: - return message, []string{} + return removeCopilotReportTaskToolCall(message) case AgentTypeAmp: return message, []string{} case AgentTypeCursor: diff --git a/lib/msgfmt/message_box.go b/lib/msgfmt/message_box.go index 1ac75c9e..5c1dc41f 100644 --- a/lib/msgfmt/message_box.go +++ b/lib/msgfmt/message_box.go @@ -1,9 +1,12 @@ package msgfmt import ( + "regexp" "strings" ) +var genericSlimMessageBoxPattern = regexp.MustCompile(`(?m)^.*─{15,}.*\n.*[|│❯].*\n(?:.*\n)?.*─{15,}.*`) + // Usually something like // ─────────────── // > @@ -26,14 +29,25 @@ func findGreaterThanMessageBox(lines []string) int { // | // ─────────────── func findGenericSlimMessageBox(lines []string) int { - for i := len(lines) - 3; i >= max(len(lines)-9, 0); i-- { - if strings.Contains(lines[i], "───────────────") && - (strings.Contains(lines[i+1], "|") || strings.Contains(lines[i+1], "│") || strings.Contains(lines[i+1], "❯")) && - strings.Contains(lines[i+2], "───────────────") { - return i - } + // genericSlimMessageBoxPattern matches a message box pattern like: + // ─────────────── + // | or │ or ❯ + // + // (optional line(s)) + // ─────────────── + + // Search within the last ~9 lines for the message box pattern + startIdx := max(len(lines)-9, 0) + searchText := strings.Join(lines[startIdx:], "\n") + + loc := genericSlimMessageBoxPattern.FindStringIndex(searchText) + if loc == nil { + return -1 } - return -1 + + // Count newlines before the match to find the line number + linesBeforeMatch := strings.Count(searchText[:loc[0]], "\n") + return startIdx + linesBeforeMatch } func removeMessageBox(msg string) string { @@ -99,3 +113,19 @@ func removeAmpMessageBox(msg string) string { } return formattedMsg } + +func removeCopilotFooter(msg string) string { + lines := strings.Split(msg, "\n") + + firstEmptyLine := 0 + + // From the end strip until we discover an empty line + for i := len(lines) - 1; i >= 0; i-- { + if len(strings.TrimSpace(lines[i])) == 0 { + firstEmptyLine = i + break + } + } + + return strings.Join(lines[:firstEmptyLine], "\n") +} diff --git a/lib/msgfmt/msgfmt.go b/lib/msgfmt/msgfmt.go index 5695f821..68c3b625 100644 --- a/lib/msgfmt/msgfmt.go +++ b/lib/msgfmt/msgfmt.go @@ -254,6 +254,14 @@ func formatGenericMessage(message string, userInput string, agentType AgentType) return message } +func formatCopilotMessage(message string, userInput string) string { + message = RemoveUserInput(message, userInput, AgentTypeCopilot) + message = removeMessageBox(message) + message = removeCopilotFooter(message) + message = trimEmptyLines(message) + return message +} + func formatCodexMessage(message string, userInput string) string { message = RemoveUserInput(message, userInput, AgentTypeCodex) message = removeCodexMessageBox(message) @@ -288,7 +296,7 @@ func FormatAgentMessage(agentType AgentType, message string, userInput string) s case AgentTypeGemini: return formatGenericMessage(message, userInput, agentType) case AgentTypeCopilot: - return formatGenericMessage(message, userInput, agentType) + return formatCopilotMessage(message, userInput) case AgentTypeAmp: return formatAmpMessage(message, userInput) case AgentTypeCursor: diff --git a/lib/msgfmt/testdata/format/copilot/first_message/expected.txt b/lib/msgfmt/testdata/format/copilot/first_message/expected.txt index db1217c5..b59dd6bd 100644 --- a/lib/msgfmt/testdata/format/copilot/first_message/expected.txt +++ b/lib/msgfmt/testdata/format/copilot/first_message/expected.txt @@ -6,6 +6,4 @@ ● Logged in with gh as user: 35C4n0r - ● Connected to GitHub MCP Server - - ~/Documents/work/agentapi [⎇ feat-github-cli*] \ No newline at end of file + ● Connected to GitHub MCP Server \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/first_message/msg.txt b/lib/msgfmt/testdata/format/copilot/first_message/msg.txt index dee0c4da..580d1d9f 100644 --- a/lib/msgfmt/testdata/format/copilot/first_message/msg.txt +++ b/lib/msgfmt/testdata/format/copilot/first_message/msg.txt @@ -8,8 +8,9 @@ ● Connected to GitHub MCP Server - ~/Documents/work/agentapi [⎇ feat-github-cli*] - ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ > Enter @ to mention files or / for commands │ - ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - Ctrl+c Exit · Ctrl+r Expand all \ No newline at end of file + ~/Documents/work/.../agentapi [⎇ main*] claude-haiku-4.5 (1x) +─────────────────────────────────────────────────────────────────── +❯ Type @ to mention files, # for issues/PRs, / for commands, or ? + for shortcuts +─────────────────────────────────────────────────────────────────── + shift+tab switch mode \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt b/lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt index 28865b17..3d5892c8 100644 --- a/lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt +++ b/lib/msgfmt/testdata/format/copilot/multi-line-input/expected.txt @@ -104,6 +104,4 @@ uses Server-Sent Events (SSE) for real-time communication instead of polling, as seen in the chat/src/components/chat-provider.tsx file where an EventSource connection is established to listen for message updates and status changes from - the server. - - ~/Documents/work/agentapi [⎇ feat-github-cli*] \ No newline at end of file + the server. \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected.txt b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected.txt new file mode 100644 index 00000000..5cb92377 --- /dev/null +++ b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected.txt @@ -0,0 +1,110 @@ +◐ The user wants me to build a snake game in Python. This is a + complete project that I should plan out first. Let me: + + 1. Create a plan for building the snake game + 2. Set up the structure + 3. Implement the game + 4. Test it + + I should start by reporting task status and creating a plan. + Since this is a new task that requires planning, I should: + 1. Report the task as "working" + 2. Create a plan.md file + 3. Set up todos in SQL + 4. Then implement + + Let me start by reporting and exploring the current directory + structure, then creating a plan. + +● List directory . + └ 1 file found + +● Let me check if snake_game already exists: + +● List directory snake_game + └ 6 files found + +● I see there's already a snake_game folder. Let me check the + existing files: + +● Read snake_game/README.md + └ 97 lines read + +● Read snake_game/main.py + └ 229 lines read + +● Great! A snake game already exists. Let me check the test file + and game summary: + +● Read snake_game/test_game.py + └ 113 lines read + +● Read snake_game/GAME_SUMMARY.txt + └ 89 lines read + +● Perfect! The snake game is already built and complete. Let me + verify it's working by running the tests: + +● Run snake game unit tests + $ cd /home/coder/projects/snake_game && python3 test_game.py + └ 7 lines... + +● The virtual environment needs pygame installed. Let me set it + up: + +● Install pygame and run tests + $ cd /home/coder/projects/snake_game && source venv/bin/activate + && pip install pygame -q && python3 test_game.py + └ 16 lines... + +● Excellent! All tests pass. Now let me verify the game can run: + +● Verify game starts successfully + $ cd /home/coder/projects/snake_game && source venv/bin/activate + && timeout 3 python3 main.py 2>&1 || true + └ 3 lines... + +● Perfect! The game initializes successfully. Let me update you + on what's been completed: + +● ✅ Snake Game Complete! + + Your Python snake game is fully built and tested. Here's what + you have: + + 🎮 Game Features + + - Classic snake gameplay with grid-based movement + - Collision detection (walls & self) + - Scoring system (10 points per food) + - Game over screen with restart option + - Keyboard controls (arrow keys) + - 10 FPS consistent gameplay speed + + 📂 Project Structure + + snake_game/ + ├── main.py - Full game implementation + ├── test_game.py - 8 passing unit tests + ├── README.md - Complete documentation + └── venv/ - Virtual environment with pygame + + 🚀 Quick Start + + cd /home/coder/projects/snake_game + source venv/bin/activate + python3 main.py + + ⌨️ Controls + + - Arrow Keys - Move snake + - R - Restart (when game over) + - ESC - Quit + + ✅ Verification Status + + - ✓ All 8 unit tests passing + - ✓ Game initializes and runs successfully + - ✓ Pygame installed and configured + + Ready to play! 🐍 \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected_tool_calls.txt b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected_tool_calls.txt new file mode 100644 index 00000000..5d7db789 --- /dev/null +++ b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/expected_tool_calls.txt @@ -0,0 +1,15 @@ +● coder_report_task: Last Call + └ {"message":"Thanks for reporting!"} +--- +● coder_report_task: Snake game built and verified - all tests +passing, game runs successfully + └ {"message":"Thanks for reporting!"} + +--- +● coder_report_task: Starting snake game project planning and +implementation + └ {"message":"Thanks for reporting!"} + +--- +● coder_report_task: First Call + └ {"message":"Thanks for reporting!"} diff --git a/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/msg.txt b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/msg.txt new file mode 100644 index 00000000..89b24c48 --- /dev/null +++ b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/msg.txt @@ -0,0 +1,126 @@ +● coder_report_task: First Call + └ {"message":"Thanks for reporting!"} + +◐ The user wants me to build a snake game in Python. This is a + complete project that I should plan out first. Let me: + + 1. Create a plan for building the snake game + 2. Set up the structure + 3. Implement the game + 4. Test it + + I should start by reporting task status and creating a plan. + Since this is a new task that requires planning, I should: + 1. Report the task as "working" + 2. Create a plan.md file + 3. Set up todos in SQL + 4. Then implement + + Let me start by reporting and exploring the current directory + structure, then creating a plan. + +● coder_report_task: Starting snake game project planning and +implementation + └ {"message":"Thanks for reporting!"} + +● List directory . + └ 1 file found + +● Let me check if snake_game already exists: + +● List directory snake_game + └ 6 files found + +● I see there's already a snake_game folder. Let me check the + existing files: + +● Read snake_game/README.md + └ 97 lines read + +● Read snake_game/main.py + └ 229 lines read + +● Great! A snake game already exists. Let me check the test file + and game summary: + +● Read snake_game/test_game.py + └ 113 lines read + +● Read snake_game/GAME_SUMMARY.txt + └ 89 lines read + +● Perfect! The snake game is already built and complete. Let me + verify it's working by running the tests: + +● Run snake game unit tests + $ cd /home/coder/projects/snake_game && python3 test_game.py + └ 7 lines... + +● The virtual environment needs pygame installed. Let me set it + up: + +● Install pygame and run tests + $ cd /home/coder/projects/snake_game && source venv/bin/activate + && pip install pygame -q && python3 test_game.py + └ 16 lines... + +● Excellent! All tests pass. Now let me verify the game can run: + +● Verify game starts successfully + $ cd /home/coder/projects/snake_game && source venv/bin/activate + && timeout 3 python3 main.py 2>&1 || true + └ 3 lines... + +● Perfect! The game initializes successfully. Let me update you + on what's been completed: + +● coder_report_task: Snake game built and verified - all tests +passing, game runs successfully + └ {"message":"Thanks for reporting!"} + +● ✅ Snake Game Complete! + + Your Python snake game is fully built and tested. Here's what + you have: + + 🎮 Game Features + + - Classic snake gameplay with grid-based movement + - Collision detection (walls & self) + - Scoring system (10 points per food) + - Game over screen with restart option + - Keyboard controls (arrow keys) + - 10 FPS consistent gameplay speed + + 📂 Project Structure + + snake_game/ + ├── main.py - Full game implementation + ├── test_game.py - 8 passing unit tests + ├── README.md - Complete documentation + └── venv/ - Virtual environment with pygame + + 🚀 Quick Start + + cd /home/coder/projects/snake_game + source venv/bin/activate + python3 main.py + + ⌨️ Controls + + - Arrow Keys - Move snake + - R - Restart (when game over) + - ESC - Quit + + ✅ Verification Status + + - ✓ All 8 unit tests passing + - ✓ Game initializes and runs successfully + - ✓ Pygame installed and configured + + Ready to play! 🐍 + +● coder_report_task: Last Call + └ {"message":"Thanks for reporting!"} + + ~/projects claude-haiku-4.5 (1x) \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/user.txt b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/user.txt new file mode 100644 index 00000000..abeefff7 --- /dev/null +++ b/lib/msgfmt/testdata/format/copilot/remove-task-tool-call/user.txt @@ -0,0 +1 @@ +Build a snake game \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/thinking/expected.txt b/lib/msgfmt/testdata/format/copilot/thinking/expected.txt index 05930b50..fc1af5c8 100644 --- a/lib/msgfmt/testdata/format/copilot/thinking/expected.txt +++ b/lib/msgfmt/testdata/format/copilot/thinking/expected.txt @@ -4,6 +4,4 @@ $ find . -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" | xargs grep -l "useEffect.*checkServerStatus\|Set up polling for messages and server status" 2>/dev/null ↪ 1 line... - ◉ Thinking (Esc to cancel) - - ~/Documents/work/agentapi [⎇ feat-github-cli*] \ No newline at end of file + ◉ Thinking (Esc to cancel) \ No newline at end of file diff --git a/lib/msgfmt/testdata/format/copilot/thinking/msg.txt b/lib/msgfmt/testdata/format/copilot/thinking/msg.txt index 3a878269..bf10760b 100644 --- a/lib/msgfmt/testdata/format/copilot/thinking/msg.txt +++ b/lib/msgfmt/testdata/format/copilot/thinking/msg.txt @@ -28,8 +28,9 @@ ◉ Thinking (Esc to cancel) - ~/Documents/work/agentapi [⎇ feat-github-cli*] - ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ > Enter @ to mention files or / for commands │ - ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - Ctrl+c Exit · Ctrl+r Expand all Remaining requests: 0% + ~/Documents/work/.../agentapi [⎇ main*] claude-haiku-4.5 (1x) +─────────────────────────────────────────────────────────────────── +❯ Type @ to mention files, # for issues/PRs, / for commands, or ? + for shortcuts +─────────────────────────────────────────────────────────────────── + shift+tab switch mode Remaining requests: 0%