Skip to content

Commit 654db05

Browse files
committed
feat: move thinking indicator into message list
1 parent 0988aa7 commit 654db05

3 files changed

Lines changed: 101 additions & 38 deletions

File tree

‎internal/tui/app/chat/messages/model.go‎

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package messages
33
import (
44
"strings"
55

6-
"github.com/usetero/cli/internal/log"
7-
86
tea "charm.land/bubbletea/v2"
7+
"github.com/usetero/cli/internal/log"
98
"github.com/usetero/cli/internal/styles"
9+
"github.com/usetero/cli/internal/tui/components/thinking"
1010
)
1111

1212
// Model is a scrollable, focusable list of message items.
@@ -41,6 +41,10 @@ type Model struct {
4141
mouseDragIdx int // Current item index during drag
4242
mouseDragX int // Current column during drag
4343
mouseDragY int // Current line within item during drag
44+
45+
// Thinking indicator
46+
thinking thinking.Model
47+
showThinking bool
4448
}
4549

4650
// New creates a new message list model.
@@ -60,17 +64,26 @@ func (m Model) Init() tea.Cmd {
6064

6165
// Update handles messages.
6266
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
67+
var cmds []tea.Cmd
68+
69+
// Forward to thinking indicator if active
70+
if m.showThinking {
71+
var cmd tea.Cmd
72+
m.thinking, cmd = m.thinking.Update(msg)
73+
cmds = append(cmds, cmd)
74+
}
75+
6376
switch msg := msg.(type) {
6477
case tea.MouseClickMsg:
6578
m, _ = m.handleMouseDown(msg.X, msg.Y)
66-
return m, nil
6779

6880
case tea.MouseMotionMsg:
6981
m = m.handleMouseDrag(msg.X, msg.Y)
70-
return m, nil
7182

7283
case tea.MouseReleaseMsg:
73-
return m.handleMouseUp(msg.X, msg.Y)
84+
var cmd tea.Cmd
85+
m, cmd = m.handleMouseUp(msg.X, msg.Y)
86+
cmds = append(cmds, cmd)
7487

7588
case tea.MouseWheelMsg:
7689
switch msg.Button {
@@ -79,11 +92,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
7992
case tea.MouseWheelDown:
8093
m = m.scrollDown(3)
8194
}
82-
return m, nil
8395

8496
case tea.KeyPressMsg:
8597
if !m.focused {
86-
return m, nil
98+
return m, tea.Batch(cmds...)
8799
}
88100

89101
switch msg.String() {
@@ -115,35 +127,41 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
115127
if m.HasHighlight() {
116128
content := m.HighlightContent()
117129
m = m.ClearHighlight()
118-
return m, tea.SetClipboard(content)
130+
cmds = append(cmds, tea.SetClipboard(content))
131+
return m, tea.Batch(cmds...)
119132
}
120133
if m.selectedIdx >= 0 && m.selectedIdx < len(m.items) {
121134
if cp, ok := m.items[m.selectedIdx].(Copyable); ok {
122-
return m, tea.SetClipboard(cp.CopyableContent())
135+
cmds = append(cmds, tea.SetClipboard(cp.CopyableContent()))
136+
return m, tea.Batch(cmds...)
123137
}
124138
}
125139
case "esc":
126140
// Clear highlight on escape
127141
if m.HasHighlight() {
128142
m = m.ClearHighlight()
129-
return m, nil
130143
}
131144
}
132145

133146
// Forward to selected item
134147
if m.selectedIdx >= 0 && m.selectedIdx < len(m.items) {
135148
var cmd tea.Cmd
136149
m.items[m.selectedIdx], cmd = m.items[m.selectedIdx].Update(msg)
137-
return m, cmd
150+
cmds = append(cmds, cmd)
138151
}
139152
}
140153

141-
return m, nil
154+
return m, tea.Batch(cmds...)
142155
}
143156

144157
// View renders the visible portion of the message list.
145158
func (m Model) View() string {
146-
if m.width == 0 || m.height == 0 || len(m.items) == 0 {
159+
if m.width == 0 || m.height == 0 {
160+
return ""
161+
}
162+
163+
// Nothing to show
164+
if len(m.items) == 0 && !m.showThinking {
147165
return ""
148166
}
149167

@@ -188,6 +206,27 @@ func (m Model) View() string {
188206
currentIdx++
189207
}
190208

209+
// Render thinking indicator after items
210+
if m.showThinking && remainingHeight > 0 {
211+
// Add gap before thinking if we have items
212+
if len(m.items) > 0 && m.gap > 0 {
213+
for i := 0; i < m.gap && remainingHeight > 0; i++ {
214+
lines = append(lines, "")
215+
remainingHeight--
216+
}
217+
}
218+
219+
thinkingView := m.renderThinking()
220+
thinkingLines := strings.Split(thinkingView, "\n")
221+
for _, line := range thinkingLines {
222+
if remainingHeight <= 0 {
223+
break
224+
}
225+
lines = append(lines, line)
226+
remainingHeight--
227+
}
228+
}
229+
191230
// Pad to fill height
192231
for len(lines) < m.height {
193232
lines = append(lines, "")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package messages
2+
3+
import (
4+
tea "charm.land/bubbletea/v2"
5+
"charm.land/lipgloss/v2"
6+
"github.com/usetero/cli/internal/tui/components/thinking"
7+
)
8+
9+
// StartThinking shows the thinking indicator.
10+
func (m Model) StartThinking() (Model, tea.Cmd) {
11+
m.thinking = thinking.New(m.theme, "Thinking")
12+
m.showThinking = true
13+
return m, m.thinking.Init()
14+
}
15+
16+
// StopThinking hides the thinking indicator.
17+
func (m Model) StopThinking() Model {
18+
m.showThinking = false
19+
return m
20+
}
21+
22+
// IsThinking returns true if the thinking indicator is shown.
23+
func (m Model) IsThinking() bool {
24+
return m.showThinking
25+
}
26+
27+
// renderThinking renders the thinking indicator with label.
28+
func (m Model) renderThinking() string {
29+
colors := m.theme.Colors
30+
label := lipgloss.NewStyle().
31+
Foreground(colors.Brand.GradientEnd).
32+
Bold(true).
33+
PaddingLeft(1).
34+
Render("Tero")
35+
content := lipgloss.NewStyle().PaddingLeft(1).Render(m.thinking.View())
36+
return lipgloss.JoinVertical(lipgloss.Left, label, content)
37+
}

‎internal/tui/app/chat/model.go‎

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,20 @@ func (m Model) handleSubmit(text string) (Model, tea.Cmd) {
259259
m = m.updateLayout()
260260
}
261261

262-
// Start streaming - Turn owns the thinking indicator
262+
// Start streaming with thinking indicator
263263
m.state = StateStreaming
264+
var thinkingCmd tea.Cmd
265+
m.list, thinkingCmd = m.list.StartThinking()
264266
m.logger.Info("message sent")
265267

266-
return m, m.turn.Send(ctx, convID.String(), m.rawMessages, m.tools.Definitions())
268+
return m, tea.Batch(thinkingCmd, m.turn.Send(ctx, convID.String(), m.rawMessages, m.tools.Definitions()))
267269
}
268270

269271
// handleStreamDone processes when streaming completes.
270272
func (m Model) handleStreamDone(msg turn.StreamDoneMsg) (Model, tea.Cmd) {
273+
// Stop thinking indicator
274+
m.list = m.list.StopThinking()
275+
271276
if msg.Err != nil {
272277
m.logger.Error("stream error", "error", msg.Err)
273278
m.state = StateIdle
@@ -378,13 +383,15 @@ func (m Model) continueAfterTools() (Model, tea.Cmd) {
378383
}
379384
m.rawMessages = append(m.rawMessages, toolResultMsg)
380385

381-
// Continue streaming - Turn owns the thinking indicator
386+
// Continue streaming with thinking indicator
382387
m.state = StateStreaming
383388
m.currentAssistant = nil
389+
var thinkingCmd tea.Cmd
390+
m.list, thinkingCmd = m.list.StartThinking()
384391

385392
m.logger.Info("continuing with tool results", "count", len(toolResults))
386393

387-
return m, m.turn.Send(m.ctx, m.conversationID.String(), m.rawMessages, m.tools.Definitions())
394+
return m, tea.Batch(thinkingCmd, m.turn.Send(m.ctx, m.conversationID.String(), m.rawMessages, m.tools.Definitions()))
388395
}
389396

390397
// rebuildList rebuilds the message list from raw messages.
@@ -453,35 +460,15 @@ func (m Model) View() string {
453460
}
454461

455462
// Has messages
456-
var messagesView string
457-
if m.turn.IsActive() {
458-
// Show Turn's view (thinking indicator) while streaming
459-
listView := m.list.View()
460-
turnView := m.renderTurnView()
461-
messagesView = lipgloss.JoinVertical(lipgloss.Left, listView, turnView)
462-
} else {
463-
messagesView = m.list.View()
464-
}
465-
466463
commandBarView := m.commandBar.View()
467-
mainContent := lipgloss.JoinVertical(lipgloss.Left, messagesView, commandBarView)
464+
mainContent := lipgloss.JoinVertical(lipgloss.Left, m.list.View(), commandBarView)
468465

469466
sidebarView := m.sidebar.View()
470467
composedContent := lipgloss.JoinHorizontal(lipgloss.Top, mainContent, " ", sidebarView)
471468

472469
return m.baseLayout.Render(composedContent)
473470
}
474471

475-
// renderTurnView renders the Turn's view with label.
476-
func (m Model) renderTurnView() string {
477-
colors := m.theme.Colors
478-
label := lipgloss.NewStyle().
479-
Foreground(colors.Brand.GradientEnd).
480-
Bold(true).
481-
Render("Tero")
482-
return lipgloss.JoinVertical(lipgloss.Left, label, m.turn.View())
483-
}
484-
485472
// SetSize returns a new Model with the given dimensions.
486473
func (m Model) SetSize(width, height int) Model {
487474
m.width = width

0 commit comments

Comments
 (0)