Skip to content

Commit b89906e

Browse files
committed
feat(apply-patch): add support for apply_patch tool (types, formatter, tests)
1 parent f36bdc5 commit b89906e

4 files changed

Lines changed: 45 additions & 1 deletion

File tree

lua/opencode/types.lua

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,33 @@
214214
---@field quick_chat OpencodeQuickChatConfig
215215

216216
---@class MessagePartState
217-
---@field input TaskToolInput|BashToolInput|FileToolInput|TodoToolInput|GlobToolInput|GrepToolInput|WebFetchToolInput|ListToolInput|QuestionToolInput Input data for the tool
217+
---@field input TaskToolInput|BashToolInput|FileToolInput|TodoToolInput|GlobToolInput|GrepToolInput|WebFetchToolInput|ListToolInput|QuestionToolInput|ApplyPatchToolInput Input data for the tool
218218
---@field metadata TaskToolMetadata|ToolMetadataBase|WebFetchToolMetadata|BashToolMetadata|FileToolMetadata|GlobToolMetadata|GrepToolMetadata|ListToolMetadata|QuestionToolMetadata Metadata about the tool execution
219219
---@field time { start: number, end: number } Timestamps for tool use
220220
---@field status string Status of the tool use (e.g., 'running', 'completed', 'failed')
221221
---@field title string Title of the tool use
222222
---@field output string Output of the tool use, if applicable
223223
---@field error? string Error message if the part failed
224224

225+
---@class ApplyPatchToolInput
226+
---@field patchText string The patch content in unified diff format
227+
228+
---@class ApplyPatchFileResult
229+
---@field filePath string Absolute path to the file
230+
---@field relativePath string Relative path to the file
231+
---@field before string File contents before the patch
232+
---@field after string File contents after the patch
233+
---@field additions number Number of lines added
234+
---@field deletions number Number of lines deleted
235+
---@field type 'add'|'edit'|'delete' Type of file operation
236+
---@field diff string Unified diff for this file
237+
238+
---@class ApplyPatchToolMetadata: ToolMetadataBase
239+
---@field truncated boolean Whether the output was truncated
240+
---@field diagnostics table<string, any> Diagnostic information keyed by file path
241+
---@field files ApplyPatchFileResult[] Per-file results
242+
---@field diff string Combined unified diff for all files
243+
225244
---@class ToolMetadataBase
226245
---@field error boolean|nil Whether the tool execution resulted in an error
227246
---@field message string|nil Optional status or error message

lua/opencode/ui/formatter.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,18 @@ function M._format_file_tool(output, tool_type, input, metadata, duration_text)
532532
end
533533
end
534534

535+
---@param output Output Output object to write to
536+
---@param metadata ApplyPatchToolMetadata Metadata for the tool use
537+
---@param duration_text? string
538+
function M._format_apply_patch_tool(output, metadata, duration_text)
539+
for _, file in ipairs(metadata.files or {}) do
540+
M.format_action(output, icons.get('edit') .. ' apply patch', file.relativePath or file.filePath, duration_text)
541+
if config.ui.output.tools.show_output and file.diff then
542+
M.format_diff(output, file.diff, '')
543+
end
544+
end
545+
end
546+
535547
---@param output Output Output object to write to
536548
---@param title string
537549
---@param input TodoToolInput
@@ -685,6 +697,8 @@ function M._format_tool(output, part)
685697
M._format_todo_tool(output, part.state.title, input --[[@as TodoToolInput]], duration_text)
686698
elseif tool == 'glob' then
687699
M._format_glob_tool(output, input --[[@as GlobToolInput]], metadata --[[@as GlobToolMetadata]], duration_text)
700+
elseif tool == 'apply_patch' then
701+
M._format_apply_patch_tool(output, metadata --[[@as ApplyPatchToolMetadata]], duration_text)
688702
elseif tool == 'list' then
689703
M._format_list_tool(
690704
output,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"timestamp":1772481560,"lines":["----","","","----","","","** apply patch** `src/app/features/auth/__tests__/LoginForm.test.tsx` 4s","","`````"," import React from 'react'"," // minimal diff for testing","","`````","",""],"actions":[],"extmarks":[[1,1,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false,"priority":10,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" [msg_user001]","OpencodeHint"]]}],[2,4,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":false,"priority":10,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-3,"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],["","OpencodeHint"],[" [msg_asst001]","OpencodeHint"]]}],[3,6,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[4,7,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[5,8,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[6,9,0,{"virt_text_pos":"overlay","right_gravity":true,"priority":5000,"ns_id":3,"end_row":10,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"end_col":0,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffAdd","virt_text":[["+","OpencodeDiffAdd"]]}],[7,9,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[8,10,0,{"virt_text_pos":"overlay","right_gravity":true,"priority":5000,"ns_id":3,"end_row":11,"hl_eol":true,"end_right_gravity":false,"virt_text_hide":false,"end_col":0,"virt_text_repeat_linebreak":false,"hl_group":"OpencodeDiffAdd","virt_text":[["+","OpencodeDiffAdd"]]}],[9,10,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[10,11,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}],[11,12,0,{"virt_text_hide":false,"right_gravity":true,"virt_text_repeat_linebreak":true,"priority":4096,"ns_id":3,"virt_text_pos":"win_col","virt_text_win_col":-1,"virt_text":[["▌","OpencodeToolBorder"]]}]]}

tests/data/apply-patch.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{"properties":{},"type":"server.connected"},
3+
{"properties":{"info":{"id":"ses_abc123","title":"Create user authentication tests","version":"1.2.10","directory":"/home/user/Projects/my-app"}},"type":"session.created"},
4+
{"properties":{"info":{"id":"msg_user001","sessionID":"ses_abc123","role":"user","agent":"build"}},"type":"message.updated"},
5+
{"properties":{"info":{"id":"msg_asst001","sessionID":"ses_abc123","role":"assistant","parentID":"msg_user001","agent":"build"}},"type":"message.updated"},
6+
{"properties":{"part":{"id":"prt_tool001","callID":"call_abc123","state":{"status":"running","input":{"patchText":"*** Begin Patch\n*** Add File: src/app/features/auth/__tests__/LoginForm.test.tsx\n+import React from 'react'\n+// ... test content\n*** End Patch"},"time":{"start":1772468525525}},"sessionID":"ses_abc123","type":"tool","messageID":"msg_asst001","tool":"apply_patch"}},"type":"message.part.updated"},
7+
{"properties":{"part":{"id":"prt_tool001","callID":"call_abc123","state":{"status":"completed","output":"Success. Updated the following files:\nA src/app/features/auth/__tests__/LoginForm.test.tsx","metadata":{"truncated":false,"diagnostics":{},"files":[{"filePath":"/home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx","relativePath":"src/app/features/auth/__tests__/LoginForm.test.tsx","before":"","after":"import React from 'react'\n","additions":42,"deletions":0,"type":"add","diff":"Index: /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n===================================================================\n--- /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n+++ /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n@@ -0,0 +1,3 @@\n+import React from 'react'\n+// minimal diff for testing\n"}],"diff":"Index: /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n===================================================================\n--- /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n+++ /home/user/Projects/my-app/src/app/features/auth/__tests__/LoginForm.test.tsx\n@@ -0,0 +1,3 @@\n+import React from 'react'\n+// minimal diff for testing\n"},"input":{"patchText":"*** Begin Patch\n*** Add File: src/app/features/auth/__tests__/LoginForm.test.tsx\n+import React from 'react'\n*** End Patch"},"time":{"start":1772468525525,"end":1772468529137},"title":"Success. Updated the following files:\nA src/app/features/auth/__tests__/LoginForm.test.tsx"},"sessionID":"ses_abc123","type":"tool","messageID":"msg_asst001","tool":"apply_patch"}},"type":"message.part.updated"},
8+
{"properties":{"sessionID":"ses_abc123","status":{"type":"idle"}},"type":"session.status"},
9+
{"properties":{"sessionID":"ses_abc123"},"type":"session.idle"}
10+
]

0 commit comments

Comments
 (0)