feat(chat): expandable thinking for assistant

Kujtim Hoxha created

Change summary

internal/ui/chat/assistant.go | 19 +++++++++++++++++++
internal/ui/chat/messages.go  |  6 ++++++
internal/ui/model/chat.go     |  7 +++++++
internal/ui/model/keys.go     |  6 +++++-
internal/ui/model/ui.go       |  2 ++
5 files changed, 39 insertions(+), 1 deletion(-)

Detailed changes

internal/ui/chat/assistant.go 🔗

@@ -230,3 +230,22 @@ func (a *AssistantMessageItem) SetMessage(message *message.Message) tea.Cmd {
 	}
 	return nil
 }
+
+// ToggleExpanded toggles the expanded state of the thinking box.
+func (a *AssistantMessageItem) ToggleExpanded() {
+	a.thinkingExpanded = !a.thinkingExpanded
+	a.clearCache()
+}
+
+// HandleMouseClick implements MouseClickable.
+func (a *AssistantMessageItem) HandleMouseClick(btn ansi.MouseButton, x, y int) bool {
+	if btn != ansi.MouseLeft {
+		return false
+	}
+	// check if the click is within the thinking box
+	if a.thinkingBoxHeight > 0 && y < a.thinkingBoxHeight {
+		a.ToggleExpanded()
+		return true
+	}
+	return false
+}

internal/ui/chat/messages.go 🔗

@@ -26,11 +26,17 @@ type Identifiable interface {
 	ID() string
 }
 
+// Animatable is an interface for items that support animation.
 type Animatable interface {
 	StartAnimation() tea.Cmd
 	Animate(msg anim.StepMsg) tea.Cmd
 }
 
+// Expandable is an interface for items that can be expanded or collapsed.
+type Expandable interface {
+	ToggleExpanded()
+}
+
 // MessageItem represents a [message.Message] item that can be displayed in the
 // UI and be part of a [list.List] identifiable by a unique ID.
 type MessageItem interface {

internal/ui/model/chat.go 🔗

@@ -250,6 +250,13 @@ func (m *Chat) GetMessageItem(id string) chat.MessageItem {
 	return item
 }
 
+// ToggleExpandedSelectedItem expands the selected message item if it is expandable.
+func (m *Chat) ToggleExpandedSelectedItem() {
+	if expandable, ok := m.list.SelectedItem().(chat.Expandable); ok {
+		expandable.ToggleExpanded()
+	}
+}
+
 // HandleMouseDown handles mouse down events for the chat component.
 func (m *Chat) HandleMouseDown(x, y int) bool {
 	if m.list.Len() == 0 {

internal/ui/model/keys.go 🔗

@@ -37,6 +37,7 @@ type KeyMap struct {
 		End            key.Binding
 		Copy           key.Binding
 		ClearHighlight key.Binding
+		Expand         key.Binding
 	}
 
 	Initialize struct {
@@ -205,7 +206,10 @@ func DefaultKeyMap() KeyMap {
 		key.WithKeys("esc", "alt+esc"),
 		key.WithHelp("esc", "clear selection"),
 	)
-
+	km.Chat.Expand = key.NewBinding(
+		key.WithKeys("space"),
+		key.WithHelp("space", "expand/collapse"),
+	)
 	km.Initialize.Yes = key.NewBinding(
 		key.WithKeys("y", "Y"),
 		key.WithHelp("y", "yes"),

internal/ui/model/ui.go 🔗

@@ -577,6 +577,8 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
 				m.focus = uiFocusEditor
 				cmds = append(cmds, m.textarea.Focus())
 				m.chat.Blur()
+			case key.Matches(msg, m.keyMap.Chat.Expand):
+				m.chat.ToggleExpandedSelectedItem()
 			case key.Matches(msg, m.keyMap.Chat.Up):
 				if cmd := m.chat.ScrollBy(-1); cmd != nil {
 					cmds = append(cmds, cmd)