diff --git a/lib/roast/cogs/agent/providers/claude/tool_use.rb b/lib/roast/cogs/agent/providers/claude/tool_use.rb index e9528862..3b1d909a 100644 --- a/lib/roast/cogs/agent/providers/claude/tool_use.rb +++ b/lib/roast/cogs/agent/providers/claude/tool_use.rb @@ -214,6 +214,31 @@ def format_edit "EDIT #{file_path} (#{details})" end + # Formats a TodoWrite tool-use line. + # + # Input fields: + # :todos (Array) – the full todo list [required] + # + # Output: "TODOWRITE todos" (singular "todo" when n is 1). When the + # list is non-empty, " ()" is appended — each distinct :status + # with its count (" "), joined with " · " in the order the + # statuses first appear. + # + # Examples: + # TODOWRITE 3 todos (2 completed · 1 in_progress) + # TODOWRITE 1 todo (1 in_progress) + # TODOWRITE 0 todos + # + #: () -> String + def format_todowrite + todos = input[:todos] || [] + counts = todos.group_by { |todo| todo[:status] }.transform_values(&:length) + summary = counts.map { |status, count| "#{count} #{status}" }.join(" · ") + label = todos.length == 1 ? "todo" : "todos" + base = "TODOWRITE #{todos.length} #{label}" + summary.empty? ? base : "#{base} (#{summary})" + end + #: () -> String def format_unknown "UNKNOWN [#{name}] #{input.inspect}" diff --git a/test/roast/cogs/agent/providers/claude/tool_use_test.rb b/test/roast/cogs/agent/providers/claude/tool_use_test.rb index 586349f0..1804a0a3 100644 --- a/test/roast/cogs/agent/providers/claude/tool_use_test.rb +++ b/test/roast/cogs/agent/providers/claude/tool_use_test.rb @@ -247,6 +247,42 @@ class Claude::ToolUseTest < ActiveSupport::TestCase assert_equal "EDIT /a.rb (-0 +0 lines)", output end + # format_todowrite + + test "format_todowrite shows a zero count and no breakdown for an empty list" do + tool_use = Claude::ToolUse.new(name: :todowrite, input: {}) + + output = tool_use.format + + assert_equal "TODOWRITE 0 todos", output + end + + test "format_todowrite uses the singular label for a single todo" do + tool_use = Claude::ToolUse.new(name: :todowrite, input: { todos: [{ status: "pending" }] }) + + output = tool_use.format + + assert_equal "TODOWRITE 1 todo (1 pending)", output + end + + test "format_todowrite tallies each status with its count" do + todos = [{ status: "completed" }, { status: "pending" }, { status: "completed" }] + tool_use = Claude::ToolUse.new(name: :todowrite, input: { todos: todos }) + + output = tool_use.format + + assert_equal "TODOWRITE 3 todos (2 completed · 1 pending)", output + end + + test "format_todowrite orders the breakdown by where each status first appears" do + todos = [{ status: "pending" }, { status: "completed" }, { status: "completed" }] + tool_use = Claude::ToolUse.new(name: :todowrite, input: { todos: todos }) + + output = tool_use.format + + assert_equal "TODOWRITE 3 todos (1 pending · 2 completed)", output + end + test "format calls format_unknown for unknown tool" do tool_use = Claude::ToolUse.new(name: :unknown_tool, input: { arg: "value" })