Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/tools/builtin/todo.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (h *todoHandler) updateTodo(_ context.Context, toolCall tools.ToolCall) (*t
h.todos.Store(params.ID, todo)

return &tools.ToolCallResult{
Output: fmt.Sprintf("Updated todo [%s] to status: [%s]", params.ID, params.Status),
Output: fmt.Sprintf("Updated todo %q to status: [%s]", todo.Description, params.Status),
}, nil
}

Expand Down
10 changes: 3 additions & 7 deletions pkg/tools/builtin/todo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,7 @@ func TestTodoTool_CreateTodos(t *testing.T) {
result, err := createTodosHandler(t.Context(), toolCall)

require.NoError(t, err)
assert.Contains(t, result.Output, "Created 3 todos:")
assert.Contains(t, result.Output, "todo_1")
assert.Contains(t, result.Output, "todo_2")
assert.Contains(t, result.Output, "todo_3")
assert.Equal(t, "Created 3 todos: [todo_1], [todo_2], [todo_3]", result.Output)

assert.Equal(t, 3, tool.handler.todos.Length())

Expand All @@ -203,8 +200,7 @@ func TestTodoTool_CreateTodos(t *testing.T) {
result, err = createTodosHandler(t.Context(), toolCall)

require.NoError(t, err)
assert.Contains(t, result.Output, "Created 1 todos:")
assert.Contains(t, result.Output, "todo_4")
assert.Equal(t, "Created 1 todos: [todo_4]", result.Output)
assert.Equal(t, 4, tool.handler.todos.Length())
}

Expand Down Expand Up @@ -253,7 +249,7 @@ func TestTodoTool_UpdateTodo(t *testing.T) {
result, err := updateHandler(t.Context(), updateToolCall)

require.NoError(t, err)
assert.Contains(t, result.Output, "Updated todo [todo_1] to status: [completed]")
assert.Contains(t, result.Output, "Updated todo \"Test todo item\" to status: [completed]")

todo, exists := tool.handler.todos.Load("todo_1")
assert.True(t, exists)
Expand Down
99 changes: 55 additions & 44 deletions pkg/tui/components/tool/todotool/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,28 @@ import (
// Component represents a unified todo tool component that handles all todo operations.
// It determines which operation to display based on the tool call name.
type Component struct {
message *types.Message
renderer *glamour.TermRenderer
spinner spinner.Spinner
width int
height int
message *types.Message
renderer *glamour.TermRenderer
sessionState *service.SessionState
spinner spinner.Spinner
width int
height int
}

// New creates a new unified todo component.
// This component handles create, create_multiple, list, and update operations.
func New(
msg *types.Message,
renderer *glamour.TermRenderer,
_ *service.SessionState,
sessionState *service.SessionState,
) layout.Model {
return &Component{
message: msg,
renderer: renderer,
spinner: spinner.New(spinner.ModeSpinnerOnly),
width: 80,
height: 1,
message: msg,
renderer: renderer,
sessionState: sessionState,
spinner: spinner.New(spinner.ModeSpinnerOnly),
width: 80,
height: 1,
}
}

Expand Down Expand Up @@ -136,12 +138,7 @@ func (c *Component) renderCreateMultiple() string {
}
}

var resultContent string
if (msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError) && msg.Content != "" {
resultContent = "\n" + styles.MutedStyle.Render(msg.Content)
}

return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content + resultContent)
return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content)
}

func (c *Component) renderList() string {
Expand All @@ -154,28 +151,22 @@ func (c *Component) renderList() string {
}

if (msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError) && msg.Content != "" {
lines := strings.Split(msg.Content, "\n")
var styledLines []string
for _, line := range lines {
if strings.HasPrefix(line, "- [") {
switch {
case strings.Contains(line, "(Status: pending)"):
icon, style := renderTodoIcon("pending")
styledLines = append(styledLines, style.Render(icon)+" "+style.Render(strings.TrimSuffix(strings.TrimSpace(line[2:]), " (Status: pending)")))
case strings.Contains(line, "(Status: in-progress)"):
icon, style := renderTodoIcon("in-progress")
styledLines = append(styledLines, style.Render(icon)+" "+style.Render(strings.TrimSuffix(strings.TrimSpace(line[2:]), " (Status: in-progress)")))
case strings.Contains(line, "(Status: completed)"):
icon, style := renderTodoIcon("completed")
styledLines = append(styledLines, style.Render(icon)+" "+style.Render(strings.TrimSuffix(strings.TrimSpace(line[2:]), " (Status: completed)")))
default:
styledLines = append(styledLines, line)
}
} else {
styledLines = append(styledLines, line)
// Use robust parsing with fallback to string-based parsing
parser := NewTodoOutputParser()
todos, err := parser.ParseTodoListWithFallback(msg.Content)

if err != nil || len(todos) == 0 {
// Final fallback to showing raw content if all parsing fails
content += "\n" + styles.MutedStyle.Render(msg.Content)
} else {
var styledLines []string
for _, todo := range todos {
styledLines = append(styledLines, RenderParsedTodo(todo))
}
if len(styledLines) > 0 {
content += "\n" + strings.Join(styledLines, "\n")
}
}
content += "\n" + strings.Join(styledLines, "\n")
}

return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content)
Expand All @@ -195,21 +186,28 @@ func (c *Component) renderUpdate() string {
if err == nil {
if updateParams, ok := params.(builtin.UpdateTodoArgs); ok {
icon, style := renderTodoIcon(updateParams.Status)

var displayText string
if c.sessionState != nil {
if todo := c.sessionState.TodoManager.GetTodoByID(updateParams.ID); todo != nil {
displayText = todo.Description
} else {
displayText = extractTaskNumber(updateParams.ID)
}
} else {
displayText = extractTaskNumber(updateParams.ID)
}

todoLine := fmt.Sprintf("\n%s %s → %s",
style.Render(icon),
style.Render(updateParams.ID),
style.Render(displayText),
style.Render(updateParams.Status))
content += todoLine
}
}
}

var resultContent string
if (msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError) && msg.Content != "" {
resultContent = "\n" + styles.MutedStyle.Render(msg.Content)
}

return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content + resultContent)
return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content)
}

func (c *Component) renderDefault() string {
Expand All @@ -228,3 +226,16 @@ func (c *Component) renderDefault() string {

return styles.BaseStyle.PaddingLeft(2).PaddingTop(1).Render(content + resultContent)
}

// extractTaskNumber extracts a task number from todo ID and formats it as "Task1", "Task2", etc.
func extractTaskNumber(id string) string {
// Handle common formats: "todo_1", "todo_2", etc.
if strings.HasPrefix(id, "todo_") {
if num := strings.TrimPrefix(id, "todo_"); num != "" {
return fmt.Sprintf("Task%s", num)
}
}

// For UUIDs or other formats, just return "Task"
return "Task"
}
Loading