Detailed changes
@@ -272,7 +272,7 @@ func (m *messageListCmp) findAssistantMessageAndToolCalls(items []util.Model, me
assistantIndex = i
}
} else if tc, ok := item.(messages.ToolCallCmp); ok {
- if tc.ParentMessageId() == messageID {
+ if tc.ParentMessageID() == messageID {
toolCalls[i] = tc
}
}
@@ -295,9 +295,17 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
assistantIndex,
messages.NewMessageCmp(
msg,
- messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
),
)
+
+ if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
+ m.listCmp.AppendItem(
+ messages.NewAssistantSection(
+ msg,
+ time.Unix(m.lastUserMessageTime, 0),
+ ),
+ )
+ }
} else if hasToolCallsOnly {
m.listCmp.DeleteItem(assistantIndex)
}
@@ -347,7 +355,6 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
cmd := m.listCmp.AppendItem(
messages.NewMessageCmp(
msg,
- messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
),
)
cmds = append(cmds, cmd)
@@ -412,6 +419,9 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message,
uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
case message.Assistant:
uiMessages = append(uiMessages, m.convertAssistantMessage(msg, toolResultMap)...)
+ if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
+ uiMessages = append(uiMessages, messages.NewAssistantSection(msg, time.Unix(m.lastUserMessageTime, 0)))
+ }
}
}
@@ -428,7 +438,6 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
uiMessages,
messages.NewMessageCmp(
msg,
- messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
),
)
}
@@ -8,13 +8,14 @@ import (
"github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/llm/models"
"github.com/charmbracelet/lipgloss/v2"
+ "github.com/charmbracelet/crush/internal/llm/models"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/tui/components/anim"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/core/layout"
+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
"github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/crush/internal/tui/util"
)
@@ -37,32 +38,17 @@ type messageCmp struct {
focused bool // Focus state for border styling
// Core message data and state
- message message.Message // The underlying message content
- spinning bool // Whether to show loading animation
- anim util.Model // Animation component for loading states
- lastUserMessageTime time.Time // Used for calculating response duration
-}
-
-// MessageOption provides functional options for configuring message components
-type MessageOption func(*messageCmp)
-
-// WithLastUserMessageTime sets the timestamp of the last user message
-// for calculating assistant response duration
-func WithLastUserMessageTime(t time.Time) MessageOption {
- return func(m *messageCmp) {
- m.lastUserMessageTime = t
- }
+ message message.Message // The underlying message content
+ spinning bool // Whether to show loading animation
+ anim util.Model // Animation component for loading states
}
// NewMessageCmp creates a new message component with the given message and options
-func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
+func NewMessageCmp(msg message.Message) MessageCmp {
m := &messageCmp{
message: msg,
anim: anim.New(15, ""),
}
- for _, opt := range opts {
- opt(m)
- }
return m
}
@@ -145,32 +131,10 @@ func (msg *messageCmp) style() lipgloss.Style {
// renderAssistantMessage renders assistant messages with optional footer information.
// Shows model name, response time, and finish reason when the message is complete.
func (m *messageCmp) renderAssistantMessage() string {
- t := styles.CurrentTheme()
parts := []string{
m.markdownContent(),
}
- finished := m.message.IsFinished()
- finishData := m.message.FinishPart()
- // Only show the footer if the message is not a tool call
- if finished && finishData.Reason != message.FinishReasonToolUse {
- infoMsg := ""
- switch finishData.Reason {
- case message.FinishReasonEndTurn:
- finishTime := time.Unix(finishData.Time, 0)
- duration := finishTime.Sub(m.lastUserMessageTime)
- infoMsg = duration.String()
- case message.FinishReasonCanceled:
- infoMsg = "canceled"
- case message.FinishReasonError:
- infoMsg = "error"
- case message.FinishReasonPermissionDenied:
- infoMsg = "permission denied"
- }
- assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg))
- parts = append(parts, core.Section(assistant, m.textWidth()))
- }
-
joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
return m.style().Render(joined)
}
@@ -225,7 +189,7 @@ func (m *messageCmp) markdownContent() string {
} else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
// Sometimes the LLMs respond with no content when they think the previous tool result
// provides the requested question
- content = "*Finished without output*"
+ content = ""
} else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
content = "*Canceled*"
}
@@ -287,3 +251,59 @@ func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
func (m *messageCmp) Spinning() bool {
return m.spinning
}
+
+type AssistantSection interface {
+ util.Model
+ layout.Sizeable
+ list.SectionHeader
+}
+type assistantSectionModel struct {
+ width int
+ message message.Message
+ lastUserMessageTime time.Time
+}
+
+func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection {
+ return &assistantSectionModel{
+ width: 0,
+ message: message,
+ lastUserMessageTime: lastUserMessageTime,
+ }
+}
+
+func (m *assistantSectionModel) Init() tea.Cmd {
+ return nil
+}
+
+func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
+ return m, nil
+}
+
+func (m *assistantSectionModel) View() tea.View {
+ t := styles.CurrentTheme()
+ finishData := m.message.FinishPart()
+ finishTime := time.Unix(finishData.Time, 0)
+ duration := finishTime.Sub(m.lastUserMessageTime)
+ infoMsg := t.S().Subtle.Render(duration.String())
+ icon := t.S().Subtle.Render(styles.ModelIcon)
+ model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
+ assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
+ return tea.NewView(
+ t.S().Base.PaddingLeft(2).Render(
+ core.Section(assistant, m.width-1),
+ ),
+ )
+}
+
+func (m *assistantSectionModel) GetSize() (int, int) {
+ return m.width, 1
+}
+
+func (m *assistantSectionModel) SetSize(width int, height int) tea.Cmd {
+ m.width = width
+ return nil
+}
+
+func (m *assistantSectionModel) IsSectionHeader() bool {
+ return true
+}
@@ -25,7 +25,7 @@ type ToolCallCmp interface {
SetToolResult(message.ToolResult) // Update tool result
SetToolCall(message.ToolCall) // Update tool call
SetCancelled() // Mark as cancelled
- ParentMessageId() string // Get parent message ID
+ ParentMessageID() string // Get parent message ID
Spinning() bool // Animation state for pending tools
GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls
@@ -137,9 +137,6 @@ func (m *toolCallCmp) View() tea.View {
box := m.style()
if !m.call.Finished && !m.cancelled {
- if m.isNested {
- return tea.NewView(box.Render(m.renderPending()))
- }
return tea.NewView(box.Render(m.renderPending()))
}
@@ -166,8 +163,8 @@ func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
}
}
-// ParentMessageId returns the ID of the message that initiated this tool call
-func (m *toolCallCmp) ParentMessageId() string {
+// ParentMessageID returns the ID of the message that initiated this tool call
+func (m *toolCallCmp) ParentMessageID() string {
return m.parentMessageID
}
@@ -212,7 +209,7 @@ func (m *toolCallCmp) renderPending() string {
t := styles.CurrentTheme()
icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
- return fmt.Sprintf("%s %s: %s", icon, tool, m.anim.View())
+ return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
}
// style returns the lipgloss style for the tool call component.
@@ -20,7 +20,16 @@
- [ ] Parallel tool calls and permissions
- [ ] Run the tools in parallel and add results in parallel
- [ ] Show multiple permissions dialogs
+- [ ] Add another space around buttons
+- [ ] Completions
+ - [ ] Should change the help to show the completions stuff
+ - [ ] Should make it wider
+ - [ ] Tab and ctrl+y should accept
+ - [ ] Words should line up
+ - [ ] If there are no completions and cick tab/ctrl+y/enter it should close it
- [ ] Investigate messages issues
+ - [ ] Make the agent separator look like the
+ - [ ] Cleanup tool calls (watch all states)
- [ ] Weird behavior sometimes the message does not update
- [ ] Message length (I saw the message go beyond the correct length when there are errors)
- [ ] Address UX issues