1package chat
2
3import (
4 "fmt"
5 "strings"
6
7 "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/crush/internal/session"
9 "github.com/charmbracelet/crush/internal/tui/components/chat/todos"
10 "github.com/charmbracelet/crush/internal/tui/styles"
11)
12
13func hasIncompleteTodos(todos []session.Todo) bool {
14 for _, todo := range todos {
15 if todo.Status != session.TodoStatusCompleted {
16 return true
17 }
18 }
19 return false
20}
21
22const (
23 pillHeightWithBorder = 3
24 maxTaskDisplayLength = 40
25 maxQueueDisplayLength = 60
26)
27
28func queuePill(queue int, focused, pillsPanelFocused bool, t *styles.Theme) string {
29 if queue <= 0 {
30 return ""
31 }
32 triangles := styles.ForegroundGrad("▶▶▶▶▶▶▶▶▶", false, t.RedDark, t.Accent)
33 if queue < 10 {
34 triangles = triangles[:queue]
35 }
36
37 content := fmt.Sprintf("%s %d Queued", strings.Join(triangles, ""), queue)
38
39 style := t.S().Base.PaddingLeft(1).PaddingRight(1)
40 if !pillsPanelFocused || focused {
41 style = style.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(t.BgOverlay)
42 } else {
43 style = style.BorderStyle(lipgloss.HiddenBorder())
44 }
45 return style.Render(content)
46}
47
48func todoPill(todos []session.Todo, spinnerView string, focused, pillsPanelFocused bool, t *styles.Theme) string {
49 if !hasIncompleteTodos(todos) {
50 return ""
51 }
52
53 completed := 0
54 var currentTodo *session.Todo
55 for i := range todos {
56 switch todos[i].Status {
57 case session.TodoStatusCompleted:
58 completed++
59 case session.TodoStatusInProgress:
60 if currentTodo == nil {
61 currentTodo = &todos[i]
62 }
63 }
64 }
65
66 total := len(todos)
67
68 label := "To-Do"
69 progress := t.S().Base.Foreground(t.FgMuted).Render(fmt.Sprintf("%d/%d", completed, total))
70
71 var content string
72 if pillsPanelFocused {
73 content = fmt.Sprintf("%s %s", label, progress)
74 } else if currentTodo != nil {
75 taskText := currentTodo.Content
76 if currentTodo.ActiveForm != "" {
77 taskText = currentTodo.ActiveForm
78 }
79 if len(taskText) > maxTaskDisplayLength {
80 taskText = taskText[:maxTaskDisplayLength-1] + "…"
81 }
82 task := t.S().Base.Foreground(t.FgSubtle).Render(taskText)
83 content = fmt.Sprintf("%s %s %s %s", spinnerView, label, progress, task)
84 } else {
85 content = fmt.Sprintf("%s %s", label, progress)
86 }
87
88 style := t.S().Base.PaddingLeft(1).PaddingRight(1)
89 if !pillsPanelFocused || focused {
90 style = style.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(t.BgOverlay)
91 } else {
92 style = style.BorderStyle(lipgloss.HiddenBorder())
93 }
94 return style.Render(content)
95}
96
97func todoList(sessionTodos []session.Todo, spinnerView string, t *styles.Theme, width int) string {
98 return todos.FormatTodosList(sessionTodos, spinnerView, t, width)
99}
100
101func queueList(queueItems []string, t *styles.Theme) string {
102 if len(queueItems) == 0 {
103 return ""
104 }
105
106 var lines []string
107 for _, item := range queueItems {
108 text := item
109 if len(text) > maxQueueDisplayLength {
110 text = text[:maxQueueDisplayLength-1] + "…"
111 }
112 prefix := t.S().Base.Foreground(t.FgMuted).Render(" •") + " "
113 lines = append(lines, prefix+t.S().Base.Foreground(t.FgMuted).Render(text))
114 }
115
116 return strings.Join(lines, "\n")
117}
118
119func sectionLine(availableWidth int, t *styles.Theme) string {
120 if availableWidth <= 0 {
121 return ""
122 }
123 line := strings.Repeat("─", availableWidth)
124 return t.S().Base.Foreground(t.Border).Render(line)
125}