@@ -13,7 +13,7 @@ import (
13 "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/session"
14 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
14 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
15 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
15 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
16− "github.com/charmbracelet/crush/internal/tui/components/core/list"
16+ "github.com/charmbracelet/crush/internal/tui/exp/list"
17 "github.com/charmbracelet/crush/internal/tui/styles"
17 "github.com/charmbracelet/crush/internal/tui/styles"
18 "github.com/charmbracelet/crush/internal/tui/util"
18 "github.com/charmbracelet/crush/internal/tui/util"
@@ -49,8 +49,8 @@ type messageListCmp struct {
49 app *app.App
49 app *app.App
50 width, height int
50 width, height int
51 session session.Session
51 session session.Session
52− listCmp list.ListModel
52+ listCmp list.List[list.Item]
53− previousSelected int // Last selected item index for restoring focus
53+ previousSelected string // Last selected item index for restoring focus
55 lastUserMessageTime int64
55 lastUserMessageTime int64
56 defaultListKeyMap list.KeyMap
56 defaultListKeyMap list.KeyMap
@@ -61,14 +61,15 @@ type messageListCmp struct {
61func New(app *app.App) MessageListCmp {
61func New(app *app.App) MessageListCmp {
62 defaultListKeyMap := list.DefaultKeyMap()
62 defaultListKeyMap := list.DefaultKeyMap()
63 listCmp := list.New(
63 listCmp := list.New(
64− list.WithGapSize(1),
64+ []list.Item{},
65− list.WithReverse(true),
65+ list.WithGap(1),
66+ list.WithDirection(list.Backward),
66 list.WithKeyMap(defaultListKeyMap),
67 list.WithKeyMap(defaultListKeyMap),
68 return &messageListCmp{
69 return &messageListCmp{
70 listCmp: listCmp,
71 listCmp: listCmp,
71− previousSelected: list.NoSelection,
72+ previousSelected: "",
72 defaultListKeyMap: defaultListKeyMap,
73 defaultListKeyMap: defaultListKeyMap,
@@ -89,7 +90,7 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
89 return m, nil
90 return m, nil
90 case SessionClearedMsg:
91 case SessionClearedMsg:
91 m.session = session.Session{}
92 m.session = session.Session{}
92− return m, m.listCmp.SetItems([]util.Model{})
93+ return m, m.listCmp.SetItems([]list.Item{})
94 case pubsub.Event[message.Message]:
95 case pubsub.Event[message.Message]:
95 cmd := m.handleMessageEvent(msg)
96 cmd := m.handleMessageEvent(msg)
@@ -97,7 +98,7 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98 var cmds []tea.Cmd
99 var cmds []tea.Cmd
99 u, cmd := m.listCmp.Update(msg)
100 u, cmd := m.listCmp.Update(msg)
100− m.listCmp = u.(list.ListModel)
101+ m.listCmp = u.(list.List[list.Item])
101 cmds = append(cmds, cmd)
102 cmds = append(cmds, cmd)
102 return m, tea.Batch(cmds...)
103 return m, tea.Batch(cmds...)
@@ -169,7 +170,7 @@ func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message])
170 toolCall.SetNestedToolCalls(nestedToolCalls)
171 toolCall.SetNestedToolCalls(nestedToolCalls)
171 m.listCmp.UpdateItem(
172 m.listCmp.UpdateItem(
172− toolCallInx,
173+ toolCall.ID(),
173 toolCall,
174 toolCall,
175 return tea.Batch(cmds...)
176 return tea.Batch(cmds...)
@@ -233,7 +234,7 @@ func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
233 if toolCallIndex := m.findToolCallByID(items, tr.ToolCallID); toolCallIndex != NotFound {
234 if toolCallIndex := m.findToolCallByID(items, tr.ToolCallID); toolCallIndex != NotFound {
234 toolCall := items[toolCallIndex].(messages.ToolCallCmp)
235 toolCall := items[toolCallIndex].(messages.ToolCallCmp)
235 toolCall.SetToolResult(tr)
236 toolCall.SetToolResult(tr)
236− m.listCmp.UpdateItem(toolCallIndex, toolCall)
237+ m.listCmp.UpdateItem(toolCall.ID(), toolCall)
239 return nil
240 return nil
@@ -241,7 +242,7 @@ func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
242// findToolCallByID searches for a tool call with the specified ID.
243// findToolCallByID searches for a tool call with the specified ID.
243// Returns the index if found, NotFound otherwise.
244// Returns the index if found, NotFound otherwise.
244−func (m *messageListCmp) findToolCallByID(items []util.Model, toolCallID string) int {
245+func (m *messageListCmp) findToolCallByID(items []list.Item, toolCallID string) int {
245 // Search backwards as tool calls are more likely to be recent
246 // Search backwards as tool calls are more likely to be recent
246 for i := len(items) - 1; i >= 0; i-- {
247 for i := len(items) - 1; i >= 0; i-- {
247 if toolCall, ok := items[i].(messages.ToolCallCmp); ok && toolCall.GetToolCall().ID == toolCallID {
248 if toolCall, ok := items[i].(messages.ToolCallCmp); ok && toolCall.GetToolCall().ID == toolCallID {
@@ -274,7 +275,7 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
276// findAssistantMessageAndToolCalls locates the assistant message and its tool calls.
277// findAssistantMessageAndToolCalls locates the assistant message and its tool calls.
277−func (m *messageListCmp) findAssistantMessageAndToolCalls(items []util.Model, messageID string) (int, map[int]messages.ToolCallCmp) {
278+func (m *messageListCmp) findAssistantMessageAndToolCalls(items []list.Item, messageID string) (int, map[int]messages.ToolCallCmp) {
278 assistantIndex := NotFound
279 assistantIndex := NotFound
279 toolCalls := make(map[int]messages.ToolCallCmp)
280 toolCalls := make(map[int]messages.ToolCallCmp)
@@ -310,7 +311,7 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
310 uiMsg := items[assistantIndex].(messages.MessageCmp)
311 uiMsg := items[assistantIndex].(messages.MessageCmp)
311 uiMsg.SetMessage(msg)
312 uiMsg.SetMessage(msg)
312 m.listCmp.UpdateItem(
313 m.listCmp.UpdateItem(
313− assistantIndex,
314+ items[assistantIndex].ID(),
316 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
317 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
@@ -322,7 +323,8 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
324 } else if hasToolCallsOnly {
325 } else if hasToolCallsOnly {
326+ items := m.listCmp.Items()
325− m.listCmp.DeleteItem(assistantIndex)
327+ m.listCmp.DeleteItem(items[assistantIndex].ID())
328 return cmd
330 return cmd
@@ -349,13 +351,13 @@ func (m *messageListCmp) updateToolCalls(msg message.Message, existingToolCalls
349// updateOrAddToolCall updates an existing tool call or adds a new one.
351// updateOrAddToolCall updates an existing tool call or adds a new one.
350func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
352func (m *messageListCmp) updateOrAddToolCall(msg message.Message, tc message.ToolCall, existingToolCalls map[int]messages.ToolCallCmp) tea.Cmd {
351 // Try to find existing tool call
353 // Try to find existing tool call
352− for index, existingTC := range existingToolCalls {
354+ for _, existingTC := range existingToolCalls {
353 if tc.ID == existingTC.GetToolCall().ID {
355 if tc.ID == existingTC.GetToolCall().ID {
354 existingTC.SetToolCall(tc)
356 existingTC.SetToolCall(tc)
355 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
357 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonCanceled {
356 existingTC.SetCancelled()
358 existingTC.SetCancelled()
358− m.listCmp.UpdateItem(index, existingTC)
360+ m.listCmp.UpdateItem(tc.ID, existingTC)
359 return nil
361 return nil
@@ -400,7 +402,7 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
402 if len(sessionMessages) == 0 {
404 if len(sessionMessages) == 0 {
403− return m.listCmp.SetItems([]util.Model{})
405+ return m.listCmp.SetItems([]list.Item{})
406 // Initialize with first message timestamp
408 // Initialize with first message timestamp
@@ -427,8 +429,8 @@ func (m *messageListCmp) buildToolResultMap(messages []message.Message) map[stri
429// convertMessagesToUI converts database messages to UI components.
431// convertMessagesToUI converts database messages to UI components.
430−func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, toolResultMap map[string]message.ToolResult) []util.Model {
432+func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
431− uiMessages := make([]util.Model, 0)
433+ uiMessages := make([]list.Item, 0)
433 for _, msg := range sessionMessages {
435 for _, msg := range sessionMessages {
434 switch msg.Role {
436 switch msg.Role {
@@ -447,8 +449,8 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message,
449// convertAssistantMessage converts an assistant message and its tool calls to UI components.
451// convertAssistantMessage converts an assistant message and its tool calls to UI components.
450−func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResultMap map[string]message.ToolResult) []util.Model {
452+func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResultMap map[string]message.ToolResult) []list.Item {
451− var uiMessages []util.Model
453+ var uiMessages []list.Item
453 // Add assistant message if it should be displayed
455 // Add assistant message if it should be displayed
454 if m.shouldShowAssistantMessage(msg) {
456 if m.shouldShowAssistantMessage(msg) {