@@ -8,13 +8,14 @@ import (
9 "github.com/charmbracelet/bubbles/v2/spinner"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11− "github.com/charmbracelet/crush/internal/llm/models"
12 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
13+ "github.com/charmbracelet/crush/internal/llm/models"
14 "github.com/charmbracelet/crush/internal/message"
14 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/tui/components/anim"
15 "github.com/charmbracelet/crush/internal/tui/components/anim"
16 "github.com/charmbracelet/crush/internal/tui/components/core"
16 "github.com/charmbracelet/crush/internal/tui/components/core"
17 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
17 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
18+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
18 "github.com/charmbracelet/crush/internal/tui/styles"
19 "github.com/charmbracelet/crush/internal/tui/styles"
19 "github.com/charmbracelet/crush/internal/tui/util"
20 "github.com/charmbracelet/crush/internal/tui/util"
@@ -37,32 +38,17 @@ type messageCmp struct {
37 focused bool // Focus state for border styling
38 focused bool // Focus state for border styling
39 // Core message data and state
40 // Core message data and state
40− message message.Message // The underlying message content
41+ message message.Message // The underlying message content
41− spinning bool // Whether to show loading animation
42+ spinning bool // Whether to show loading animation
42− anim util.Model // Animation component for loading states
43+ anim util.Model // Animation component for loading states
43− lastUserMessageTime time.Time // Used for calculating response duration
46−// MessageOption provides functional options for configuring message components
47−type MessageOption func(*messageCmp)
49−// WithLastUserMessageTime sets the timestamp of the last user message
50−// for calculating assistant response duration
51−func WithLastUserMessageTime(t time.Time) MessageOption {
52− return func(m *messageCmp) {
53− m.lastUserMessageTime = t
57// NewMessageCmp creates a new message component with the given message and options
46// NewMessageCmp creates a new message component with the given message and options
58−func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
47+func NewMessageCmp(msg message.Message) MessageCmp {
59 m := &messageCmp{
48 m := &messageCmp{
60 message: msg,
49 message: msg,
61 anim: anim.New(15, ""),
50 anim: anim.New(15, ""),
63− for _, opt := range opts {
@@ -145,32 +131,10 @@ func (msg *messageCmp) style() lipgloss.Style {
145// renderAssistantMessage renders assistant messages with optional footer information.
131// renderAssistantMessage renders assistant messages with optional footer information.
146// Shows model name, response time, and finish reason when the message is complete.
132// Shows model name, response time, and finish reason when the message is complete.
147func (m *messageCmp) renderAssistantMessage() string {
133func (m *messageCmp) renderAssistantMessage() string {
148− t := styles.CurrentTheme()
149 parts := []string{
134 parts := []string{
150 m.markdownContent(),
135 m.markdownContent(),
153− finished := m.message.IsFinished()
154− finishData := m.message.FinishPart()
155− // Only show the footer if the message is not a tool call
156− if finished && finishData.Reason != message.FinishReasonToolUse {
158− switch finishData.Reason {
159− case message.FinishReasonEndTurn:
160− finishTime := time.Unix(finishData.Time, 0)
161− duration := finishTime.Sub(m.lastUserMessageTime)
162− infoMsg = duration.String()
163− case message.FinishReasonCanceled:
164− infoMsg = "canceled"
165− case message.FinishReasonError:
167− case message.FinishReasonPermissionDenied:
168− infoMsg = "permission denied"
170− assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg))
171− parts = append(parts, core.Section(assistant, m.textWidth()))
174 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
138 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
175 return m.style().Render(joined)
139 return m.style().Render(joined)
@@ -225,7 +189,7 @@ func (m *messageCmp) markdownContent() string {
225 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
189 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
226 // Sometimes the LLMs respond with no content when they think the previous tool result
190 // Sometimes the LLMs respond with no content when they think the previous tool result
227 // provides the requested question
191 // provides the requested question
228− content = "*Finished without output*"
192+ content = ""
229 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
193 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
230 content = "*Canceled*"
194 content = "*Canceled*"
@@ -287,3 +251,59 @@ func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
287func (m *messageCmp) Spinning() bool {
251func (m *messageCmp) Spinning() bool {
288 return m.spinning
252 return m.spinning
255+type AssistantSection interface {
260+type assistantSectionModel struct {
262+ message message.Message
263+ lastUserMessageTime time.Time
266+func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection {
267+ return &assistantSectionModel{
270+ lastUserMessageTime: lastUserMessageTime,
274+func (m *assistantSectionModel) Init() tea.Cmd {
278+func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
282+func (m *assistantSectionModel) View() tea.View {
283+ t := styles.CurrentTheme()
284+ finishData := m.message.FinishPart()
285+ finishTime := time.Unix(finishData.Time, 0)
286+ duration := finishTime.Sub(m.lastUserMessageTime)
287+ infoMsg := t.S().Subtle.Render(duration.String())
288+ icon := t.S().Subtle.Render(styles.ModelIcon)
289+ model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
290+ assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
292+ t.S().Base.PaddingLeft(2).Render(
293+ core.Section(assistant, m.width-1),
298+func (m *assistantSectionModel) GetSize() (int, int) {
302+func (m *assistantSectionModel) SetSize(width int, height int) tea.Cmd {
307+func (m *assistantSectionModel) IsSectionHeader() bool {