Detailed changes
@@ -211,7 +211,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.send()
}
}
-
}
m.textarea, cmd = m.textarea.Update(msg)
return m, cmd
@@ -233,7 +232,8 @@ func (m *editorCmp) View() string {
return lipgloss.JoinVertical(lipgloss.Top,
m.attachmentsContent(),
lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
- m.textarea.View()),
+ m.textarea.View(),
+ ),
)
}
@@ -1,44 +1,49 @@
package chat
-import (
- "context"
- "fmt"
- "math"
-
- "github.com/charmbracelet/bubbles/v2/key"
- "github.com/charmbracelet/bubbles/v2/spinner"
- "github.com/charmbracelet/bubbles/v2/viewport"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/opencode-ai/opencode/internal/app"
- "github.com/opencode-ai/opencode/internal/message"
- "github.com/opencode-ai/opencode/internal/pubsub"
- "github.com/opencode-ai/opencode/internal/session"
- "github.com/opencode-ai/opencode/internal/tui/components/dialog"
- "github.com/opencode-ai/opencode/internal/tui/styles"
- "github.com/opencode-ai/opencode/internal/tui/theme"
- "github.com/opencode-ai/opencode/internal/tui/util"
-)
-
-type cacheItem struct {
- width int
- content []uiMessage
-}
-type messagesCmp struct {
- app *app.App
- width, height int
- viewport viewport.Model
- session session.Session
- messages []message.Message
- uiMessages []uiMessage
- currentMsgID string
- cachedContent map[string]cacheItem
- spinner spinner.Model
- rendering bool
- attachments viewport.Model
-}
-type renderFinishedMsg struct{}
-
+import "github.com/charmbracelet/bubbles/v2/key"
+
+// import (
+//
+// "context"
+// "fmt"
+// "math"
+//
+// "github.com/charmbracelet/bubbles/v2/key"
+// "github.com/charmbracelet/bubbles/v2/spinner"
+// "github.com/charmbracelet/bubbles/v2/viewport"
+// tea "github.com/charmbracelet/bubbletea/v2"
+// "github.com/charmbracelet/lipgloss/v2"
+// "github.com/opencode-ai/opencode/internal/app"
+// "github.com/opencode-ai/opencode/internal/message"
+// "github.com/opencode-ai/opencode/internal/pubsub"
+// "github.com/opencode-ai/opencode/internal/session"
+// "github.com/opencode-ai/opencode/internal/tui/components/dialog"
+// "github.com/opencode-ai/opencode/internal/tui/styles"
+// "github.com/opencode-ai/opencode/internal/tui/theme"
+// "github.com/opencode-ai/opencode/internal/tui/util"
+//
+// )
+//
+// type cacheItem struct {
+// width int
+// content []uiMessage
+// }
+//
+// type messagesCmp struct {
+// app *app.App
+// width, height int
+// viewport viewport.Model
+// session session.Session
+// messages []message.Message
+// uiMessages []uiMessage
+// currentMsgID string
+// cachedContent map[string]cacheItem
+// spinner spinner.Model
+// rendering bool
+// attachments viewport.Model
+// }
+//
+// type renderFinishedMsg struct{}
type MessageKeys struct {
PageDown key.Binding
PageUp key.Binding
@@ -65,410 +70,411 @@ var messageKeys = MessageKeys{
),
}
-func (m *messagesCmp) Init() tea.Cmd {
- return tea.Batch(m.viewport.Init(), m.spinner.Tick)
-}
-
-func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case dialog.ThemeChangedMsg:
- m.rerender()
- return m, nil
- case SessionSelectedMsg:
- if msg.ID != m.session.ID {
- cmd := m.SetSession(msg)
- return m, cmd
- }
- return m, nil
- case SessionClearedMsg:
- m.session = session.Session{}
- m.messages = make([]message.Message, 0)
- m.currentMsgID = ""
- m.rendering = false
- return m, nil
-
- case tea.KeyMsg:
- if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
- key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
- u, cmd := m.viewport.Update(msg)
- m.viewport = u
- cmds = append(cmds, cmd)
- }
-
- case renderFinishedMsg:
- m.rendering = false
- m.viewport.GotoBottom()
- case pubsub.Event[session.Session]:
- if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
- m.session = msg.Payload
- if m.session.SummaryMessageID == m.currentMsgID {
- delete(m.cachedContent, m.currentMsgID)
- m.renderView()
- }
- }
- case pubsub.Event[message.Message]:
- needsRerender := false
- if msg.Type == pubsub.CreatedEvent {
- if msg.Payload.SessionID == m.session.ID {
-
- messageExists := false
- for _, v := range m.messages {
- if v.ID == msg.Payload.ID {
- messageExists = true
- break
- }
- }
-
- if !messageExists {
- if len(m.messages) > 0 {
- lastMsgID := m.messages[len(m.messages)-1].ID
- delete(m.cachedContent, lastMsgID)
- }
-
- m.messages = append(m.messages, msg.Payload)
- delete(m.cachedContent, m.currentMsgID)
- m.currentMsgID = msg.Payload.ID
- needsRerender = true
- }
- }
- // There are tool calls from the child task
- for _, v := range m.messages {
- for _, c := range v.ToolCalls() {
- if c.ID == msg.Payload.SessionID {
- delete(m.cachedContent, v.ID)
- needsRerender = true
- }
- }
- }
- } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
- for i, v := range m.messages {
- if v.ID == msg.Payload.ID {
- m.messages[i] = msg.Payload
- delete(m.cachedContent, msg.Payload.ID)
- needsRerender = true
- break
- }
- }
- }
- if needsRerender {
- m.renderView()
- if len(m.messages) > 0 {
- if (msg.Type == pubsub.CreatedEvent) ||
- (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
- m.viewport.GotoBottom()
- }
- }
- }
- }
-
- spinner, cmd := m.spinner.Update(msg)
- m.spinner = spinner
- cmds = append(cmds, cmd)
- return m, tea.Batch(cmds...)
-}
-
-func (m *messagesCmp) IsAgentWorking() bool {
- return m.app.CoderAgent.IsSessionBusy(m.session.ID)
-}
-
-func formatTimeDifference(unixTime1, unixTime2 int64) string {
- diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
-
- if diffSeconds < 60 {
- return fmt.Sprintf("%.1fs", diffSeconds)
- }
-
- minutes := int(diffSeconds / 60)
- seconds := int(diffSeconds) % 60
- return fmt.Sprintf("%dm%ds", minutes, seconds)
-}
-
-func (m *messagesCmp) renderView() {
- m.uiMessages = make([]uiMessage, 0)
- pos := 0
- baseStyle := styles.BaseStyle()
-
- if m.width == 0 {
- return
- }
- for inx, msg := range m.messages {
- switch msg.Role {
- case message.User:
- if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
- m.uiMessages = append(m.uiMessages, cache.content...)
- continue
- }
- userMsg := renderUserMessage(
- msg,
- msg.ID == m.currentMsgID,
- m.width,
- pos,
- )
- m.uiMessages = append(m.uiMessages, userMsg)
- m.cachedContent[msg.ID] = cacheItem{
- width: m.width,
- content: []uiMessage{userMsg},
- }
- pos += userMsg.height + 1 // + 1 for spacing
- case message.Assistant:
- if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
- m.uiMessages = append(m.uiMessages, cache.content...)
- continue
- }
- isSummary := m.session.SummaryMessageID == msg.ID
-
- assistantMessages := renderAssistantMessage(
- msg,
- inx,
- m.messages,
- m.app.Messages,
- m.currentMsgID,
- isSummary,
- m.width,
- pos,
- )
- for _, msg := range assistantMessages {
- m.uiMessages = append(m.uiMessages, msg)
- pos += msg.height + 1 // + 1 for spacing
- }
- m.cachedContent[msg.ID] = cacheItem{
- width: m.width,
- content: assistantMessages,
- }
- }
- }
-
- messages := make([]string, 0)
- for _, v := range m.uiMessages {
- messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
- baseStyle.
- Width(m.width).
- Render(
- "",
- ),
- )
- }
-
- m.viewport.SetContent(
- baseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Top,
- messages...,
- ),
- ),
- )
-}
-
-func (m *messagesCmp) View() string {
- baseStyle := styles.BaseStyle()
-
- if m.rendering {
- return baseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Top,
- "Loading...",
- m.working(),
- m.help(),
- ),
- )
- }
- if len(m.messages) == 0 {
- content := baseStyle.
- Width(m.width).
- Height(m.height - 1).
- Render(
- initialScreen(),
- )
-
- return baseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Top,
- content,
- "",
- m.help(),
- ),
- )
- }
-
- return baseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Top,
- m.viewport.View(),
- m.working(),
- m.help(),
- ),
- )
-}
-
-func hasToolsWithoutResponse(messages []message.Message) bool {
- toolCalls := make([]message.ToolCall, 0)
- toolResults := make([]message.ToolResult, 0)
- for _, m := range messages {
- toolCalls = append(toolCalls, m.ToolCalls()...)
- toolResults = append(toolResults, m.ToolResults()...)
- }
-
- for _, v := range toolCalls {
- found := false
- for _, r := range toolResults {
- if v.ID == r.ToolCallID {
- found = true
- break
- }
- }
- if !found && v.Finished {
- return true
- }
- }
- return false
-}
-
-func hasUnfinishedToolCalls(messages []message.Message) bool {
- toolCalls := make([]message.ToolCall, 0)
- for _, m := range messages {
- toolCalls = append(toolCalls, m.ToolCalls()...)
- }
- for _, v := range toolCalls {
- if !v.Finished {
- return true
- }
- }
- return false
-}
-
-func (m *messagesCmp) working() string {
- text := ""
- if m.IsAgentWorking() && len(m.messages) > 0 {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- task := "Thinking..."
- lastMessage := m.messages[len(m.messages)-1]
- if hasToolsWithoutResponse(m.messages) {
- task = "Waiting for tool response..."
- } else if hasUnfinishedToolCalls(m.messages) {
- task = "Building tool call..."
- } else if !lastMessage.IsFinished() {
- task = "Generating..."
- }
- if task != "" {
- text += baseStyle.
- Width(m.width).
- Foreground(t.Primary()).
- Bold(true).
- Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
- }
- }
- return text
-}
-
-func (m *messagesCmp) help() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- text := ""
-
- if m.app.CoderAgent.IsBusy() {
- text += lipgloss.JoinHorizontal(
- lipgloss.Left,
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
- baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
- )
- } else {
- text += lipgloss.JoinHorizontal(
- lipgloss.Left,
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
- baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
- baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
- baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
- )
- }
- return baseStyle.
- Width(m.width).
- Render(text)
-}
-
-func (m *messagesCmp) rerender() {
- for _, msg := range m.messages {
- delete(m.cachedContent, msg.ID)
- }
- m.renderView()
-}
-
-func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
- if m.width == width && m.height == height {
- return nil
- }
- m.width = width
- m.height = height
- m.viewport.SetWidth(width)
- m.viewport.SetHeight(height - 2)
- m.attachments.SetWidth(width + 40)
- m.attachments.SetHeight(3)
- m.rerender()
- return nil
-}
-
-func (m *messagesCmp) GetSize() (int, int) {
- return m.width, m.height
-}
-
-func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
- if m.session.ID == session.ID {
- return nil
- }
- m.session = session
- messages, err := m.app.Messages.List(context.Background(), session.ID)
- if err != nil {
- return util.ReportError(err)
- }
- m.messages = messages
- if len(m.messages) > 0 {
- m.currentMsgID = m.messages[len(m.messages)-1].ID
- }
- delete(m.cachedContent, m.currentMsgID)
- m.rendering = true
- return func() tea.Msg {
- m.renderView()
- return renderFinishedMsg{}
- }
-}
-
-func (m *messagesCmp) BindingKeys() []key.Binding {
- return []key.Binding{
- m.viewport.KeyMap.PageDown,
- m.viewport.KeyMap.PageUp,
- m.viewport.KeyMap.HalfPageUp,
- m.viewport.KeyMap.HalfPageDown,
- }
-}
-
-func NewMessagesCmp(app *app.App) util.Model {
- s := spinner.New()
- s.Spinner = spinner.Pulse
- vp := viewport.New()
- attachmets := viewport.New()
- vp.KeyMap.PageUp = messageKeys.PageUp
- vp.KeyMap.PageDown = messageKeys.PageDown
- vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
- vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
- return &messagesCmp{
- app: app,
- cachedContent: make(map[string]cacheItem),
- viewport: vp,
- spinner: s,
- attachments: attachmets,
- }
-}
+//
+// func (m *messagesCmp) Init() tea.Cmd {
+// return tea.Batch(m.viewport.Init(), m.spinner.Tick)
+// }
+//
+// func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+// var cmds []tea.Cmd
+// switch msg := msg.(type) {
+// case dialog.ThemeChangedMsg:
+// m.rerender()
+// return m, nil
+// case SessionSelectedMsg:
+// if msg.ID != m.session.ID {
+// cmd := m.SetSession(msg)
+// return m, cmd
+// }
+// return m, nil
+// case SessionClearedMsg:
+// m.session = session.Session{}
+// m.messages = make([]message.Message, 0)
+// m.currentMsgID = ""
+// m.rendering = false
+// return m, nil
+//
+// case tea.KeyMsg:
+// if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+// key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+// u, cmd := m.viewport.Update(msg)
+// m.viewport = u
+// cmds = append(cmds, cmd)
+// }
+//
+// case renderFinishedMsg:
+// m.rendering = false
+// m.viewport.GotoBottom()
+// case pubsub.Event[session.Session]:
+// if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
+// m.session = msg.Payload
+// if m.session.SummaryMessageID == m.currentMsgID {
+// delete(m.cachedContent, m.currentMsgID)
+// m.renderView()
+// }
+// }
+// case pubsub.Event[message.Message]:
+// needsRerender := false
+// if msg.Type == pubsub.CreatedEvent {
+// if msg.Payload.SessionID == m.session.ID {
+//
+// messageExists := false
+// for _, v := range m.messages {
+// if v.ID == msg.Payload.ID {
+// messageExists = true
+// break
+// }
+// }
+//
+// if !messageExists {
+// if len(m.messages) > 0 {
+// lastMsgID := m.messages[len(m.messages)-1].ID
+// delete(m.cachedContent, lastMsgID)
+// }
+//
+// m.messages = append(m.messages, msg.Payload)
+// delete(m.cachedContent, m.currentMsgID)
+// m.currentMsgID = msg.Payload.ID
+// needsRerender = true
+// }
+// }
+// // There are tool calls from the child task
+// for _, v := range m.messages {
+// for _, c := range v.ToolCalls() {
+// if c.ID == msg.Payload.SessionID {
+// delete(m.cachedContent, v.ID)
+// needsRerender = true
+// }
+// }
+// }
+// } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
+// for i, v := range m.messages {
+// if v.ID == msg.Payload.ID {
+// m.messages[i] = msg.Payload
+// delete(m.cachedContent, msg.Payload.ID)
+// needsRerender = true
+// break
+// }
+// }
+// }
+// if needsRerender {
+// m.renderView()
+// if len(m.messages) > 0 {
+// if (msg.Type == pubsub.CreatedEvent) ||
+// (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
+// m.viewport.GotoBottom()
+// }
+// }
+// }
+// }
+//
+// spinner, cmd := m.spinner.Update(msg)
+// m.spinner = spinner
+// cmds = append(cmds, cmd)
+// return m, tea.Batch(cmds...)
+// }
+//
+// func (m *messagesCmp) IsAgentWorking() bool {
+// return m.app.CoderAgent.IsSessionBusy(m.session.ID)
+// }
+//
+// func formatTimeDifference(unixTime1, unixTime2 int64) string {
+// diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
+//
+// if diffSeconds < 60 {
+// return fmt.Sprintf("%.1fs", diffSeconds)
+// }
+//
+// minutes := int(diffSeconds / 60)
+// seconds := int(diffSeconds) % 60
+// return fmt.Sprintf("%dm%ds", minutes, seconds)
+// }
+//
+// func (m *messagesCmp) renderView() {
+// m.uiMessages = make([]uiMessage, 0)
+// pos := 0
+// baseStyle := styles.BaseStyle()
+//
+// if m.width == 0 {
+// return
+// }
+// for inx, msg := range m.messages {
+// switch msg.Role {
+// case message.User:
+// if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+// m.uiMessages = append(m.uiMessages, cache.content...)
+// continue
+// }
+// userMsg := renderUserMessage(
+// msg,
+// msg.ID == m.currentMsgID,
+// m.width,
+// pos,
+// )
+// m.uiMessages = append(m.uiMessages, userMsg)
+// m.cachedContent[msg.ID] = cacheItem{
+// width: m.width,
+// content: []uiMessage{userMsg},
+// }
+// pos += userMsg.height + 1 // + 1 for spacing
+// case message.Assistant:
+// if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+// m.uiMessages = append(m.uiMessages, cache.content...)
+// continue
+// }
+// isSummary := m.session.SummaryMessageID == msg.ID
+//
+// assistantMessages := renderAssistantMessage(
+// msg,
+// inx,
+// m.messages,
+// m.app.Messages,
+// m.currentMsgID,
+// isSummary,
+// m.width,
+// pos,
+// )
+// for _, msg := range assistantMessages {
+// m.uiMessages = append(m.uiMessages, msg)
+// pos += msg.height + 1 // + 1 for spacing
+// }
+// m.cachedContent[msg.ID] = cacheItem{
+// width: m.width,
+// content: assistantMessages,
+// }
+// }
+// }
+//
+// messages := make([]string, 0)
+// for _, v := range m.uiMessages {
+// messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
+// baseStyle.
+// Width(m.width).
+// Render(
+// "",
+// ),
+// )
+// }
+//
+// m.viewport.SetContent(
+// baseStyle.
+// Width(m.width).
+// Render(
+// lipgloss.JoinVertical(
+// lipgloss.Top,
+// messages...,
+// ),
+// ),
+// )
+// }
+//
+// func (m *messagesCmp) View() string {
+// baseStyle := styles.BaseStyle()
+//
+// if m.rendering {
+// return baseStyle.
+// Width(m.width).
+// Render(
+// lipgloss.JoinVertical(
+// lipgloss.Top,
+// "Loading...",
+// m.working(),
+// m.help(),
+// ),
+// )
+// }
+// if len(m.messages) == 0 {
+// content := baseStyle.
+// Width(m.width).
+// Height(m.height - 1).
+// Render(
+// initialScreen(),
+// )
+//
+// return baseStyle.
+// Width(m.width).
+// Render(
+// lipgloss.JoinVertical(
+// lipgloss.Top,
+// content,
+// "",
+// m.help(),
+// ),
+// )
+// }
+//
+// return baseStyle.
+// Width(m.width).
+// Render(
+// lipgloss.JoinVertical(
+// lipgloss.Top,
+// m.viewport.View(),
+// m.working(),
+// m.help(),
+// ),
+// )
+// }
+//
+// func hasToolsWithoutResponse(messages []message.Message) bool {
+// toolCalls := make([]message.ToolCall, 0)
+// toolResults := make([]message.ToolResult, 0)
+// for _, m := range messages {
+// toolCalls = append(toolCalls, m.ToolCalls()...)
+// toolResults = append(toolResults, m.ToolResults()...)
+// }
+//
+// for _, v := range toolCalls {
+// found := false
+// for _, r := range toolResults {
+// if v.ID == r.ToolCallID {
+// found = true
+// break
+// }
+// }
+// if !found && v.Finished {
+// return true
+// }
+// }
+// return false
+// }
+//
+// func hasUnfinishedToolCalls(messages []message.Message) bool {
+// toolCalls := make([]message.ToolCall, 0)
+// for _, m := range messages {
+// toolCalls = append(toolCalls, m.ToolCalls()...)
+// }
+// for _, v := range toolCalls {
+// if !v.Finished {
+// return true
+// }
+// }
+// return false
+// }
+//
+// func (m *messagesCmp) working() string {
+// text := ""
+// if m.IsAgentWorking() && len(m.messages) > 0 {
+// t := theme.CurrentTheme()
+// baseStyle := styles.BaseStyle()
+//
+// task := "Thinking..."
+// lastMessage := m.messages[len(m.messages)-1]
+// if hasToolsWithoutResponse(m.messages) {
+// task = "Waiting for tool response..."
+// } else if hasUnfinishedToolCalls(m.messages) {
+// task = "Building tool call..."
+// } else if !lastMessage.IsFinished() {
+// task = "Generating..."
+// }
+// if task != "" {
+// text += baseStyle.
+// Width(m.width).
+// Foreground(t.Primary()).
+// Bold(true).
+// Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
+// }
+// }
+// return text
+// }
+//
+// func (m *messagesCmp) help() string {
+// t := theme.CurrentTheme()
+// baseStyle := styles.BaseStyle()
+//
+// text := ""
+//
+// if m.app.CoderAgent.IsBusy() {
+// text += lipgloss.JoinHorizontal(
+// lipgloss.Left,
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+// baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
+// )
+// } else {
+// text += lipgloss.JoinHorizontal(
+// lipgloss.Left,
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+// baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
+// baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
+// )
+// }
+// return baseStyle.
+// Width(m.width).
+// Render(text)
+// }
+//
+// func (m *messagesCmp) rerender() {
+// for _, msg := range m.messages {
+// delete(m.cachedContent, msg.ID)
+// }
+// m.renderView()
+// }
+//
+// func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
+// if m.width == width && m.height == height {
+// return nil
+// }
+// m.width = width
+// m.height = height
+// m.viewport.SetWidth(width)
+// m.viewport.SetHeight(height - 2)
+// m.attachments.SetWidth(width + 40)
+// m.attachments.SetHeight(3)
+// m.rerender()
+// return nil
+// }
+//
+// func (m *messagesCmp) GetSize() (int, int) {
+// return m.width, m.height
+// }
+//
+// func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
+// if m.session.ID == session.ID {
+// return nil
+// }
+// m.session = session
+// messages, err := m.app.Messages.List(context.Background(), session.ID)
+// if err != nil {
+// return util.ReportError(err)
+// }
+// m.messages = messages
+// if len(m.messages) > 0 {
+// m.currentMsgID = m.messages[len(m.messages)-1].ID
+// }
+// delete(m.cachedContent, m.currentMsgID)
+// m.rendering = true
+// return func() tea.Msg {
+// m.renderView()
+// return renderFinishedMsg{}
+// }
+// }
+//
+// func (m *messagesCmp) BindingKeys() []key.Binding {
+// return []key.Binding{
+// m.viewport.KeyMap.PageDown,
+// m.viewport.KeyMap.PageUp,
+// m.viewport.KeyMap.HalfPageUp,
+// m.viewport.KeyMap.HalfPageDown,
+// }
+// }
+//
+// func NewMessagesCmp(app *app.App) util.Model {
+// s := spinner.New()
+// s.Spinner = spinner.Pulse
+// vp := viewport.New()
+// attachmets := viewport.New()
+// vp.KeyMap.PageUp = messageKeys.PageUp
+// vp.KeyMap.PageDown = messageKeys.PageDown
+// vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
+// vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
+// return &messagesCmp{
+// app: app,
+// cachedContent: make(map[string]cacheItem),
+// viewport: vp,
+// spinner: s,
+// attachments: attachmets,
+// }
+// }
@@ -106,6 +106,8 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
return m.handleNewUserMessage(event.Payload)
case message.Assistant:
return m.handleNewAssistantMessage(event.Payload)
+ case message.Tool:
+ return m.handleToolMessage(event.Payload)
}
// TODO: handle tools
case pubsub.UpdatedEvent:
@@ -119,6 +121,10 @@ func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
}
+func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
+ return nil
+}
+
func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
// Simple update the content
items := m.listCmp.Items()
@@ -136,6 +142,8 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
),
)
+ } else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
+ m.listCmp.DeleteItem(len(items) - 1)
}
return nil
}
@@ -1,629 +0,0 @@
-package chat
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/x/ansi"
- "github.com/opencode-ai/opencode/internal/config"
- "github.com/opencode-ai/opencode/internal/diff"
- "github.com/opencode-ai/opencode/internal/llm/agent"
- "github.com/opencode-ai/opencode/internal/llm/models"
- "github.com/opencode-ai/opencode/internal/llm/tools"
- "github.com/opencode-ai/opencode/internal/message"
- "github.com/opencode-ai/opencode/internal/tui/styles"
- "github.com/opencode-ai/opencode/internal/tui/theme"
-)
-
-type uiMessageType int
-
-const (
- userMessageType uiMessageType = iota
- assistantMessageType
- toolMessageType
-
- maxResultHeight = 10
-)
-
-type uiMessage struct {
- ID string
- messageType uiMessageType
- position int
- height int
- content string
-}
-
-func toMarkdown(content string, focused bool, width int) string {
- r := styles.GetMarkdownRenderer(width)
- rendered, _ := r.Render(content)
- return rendered
-}
-
-func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
- t := theme.CurrentTheme()
-
- style := styles.BaseStyle().
- Width(width - 1).
- BorderLeft(true).
- Foreground(t.TextMuted()).
- BorderForeground(t.Primary()).
- BorderStyle(lipgloss.ThickBorder())
-
- if isUser {
- style = style.BorderForeground(t.Secondary())
- }
-
- // Apply markdown formatting and handle background color
- parts := []string{
- toMarkdown(msg, isFocused, width),
- }
-
- // Remove newline at the end
- parts[0] = strings.TrimSuffix(parts[0], "\n")
- if len(info) > 0 {
- parts = append(parts, info...)
- }
-
- rendered := style.Render(
- lipgloss.JoinVertical(
- lipgloss.Left,
- parts...,
- ),
- )
-
- return rendered
-}
-
-func renderUserMessage(msg message.Message, isFocused bool, width int, position int) uiMessage {
- var styledAttachments []string
- t := theme.CurrentTheme()
- attachmentStyles := styles.BaseStyle().
- MarginLeft(1).
- Background(t.TextMuted()).
- Foreground(t.Text())
- for _, attachment := range msg.BinaryContent() {
- file := filepath.Base(attachment.Path)
- var filename string
- if len(file) > 10 {
- filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
- } else {
- filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
- }
- styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
- }
- content := ""
- if len(styledAttachments) > 0 {
- attachmentContent := styles.BaseStyle().Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
- content = renderMessage(msg.Content().String(), true, isFocused, width, attachmentContent)
- } else {
- content = renderMessage(msg.Content().String(), true, isFocused, width)
- }
- userMsg := uiMessage{
- ID: msg.ID,
- messageType: userMessageType,
- position: position,
- height: lipgloss.Height(content),
- content: content,
- }
- return userMsg
-}
-
-// Returns multiple uiMessages because of the tool calls
-func renderAssistantMessage(
- msg message.Message,
- msgIndex int,
- allMessages []message.Message, // we need this to get tool results and the user message
- messagesService message.Service, // We need this to get the task tool messages
- focusedUIMessageId string,
- isSummary bool,
- width int,
- position int,
-) []uiMessage {
- messages := []uiMessage{}
- content := msg.Content().String()
- thinking := msg.IsThinking()
- thinkingContent := msg.ReasoningContent().Thinking
- finished := msg.IsFinished()
- finishData := msg.FinishPart()
- info := []string{}
-
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- // Add finish info if available
- if finished {
- switch finishData.Reason {
- case message.FinishReasonEndTurn:
- took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
- )
- case message.FinishReasonCanceled:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
- )
- case message.FinishReasonError:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
- )
- case message.FinishReasonPermissionDenied:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
- )
- }
- }
- if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
- if content == "" {
- content = "*Finished without output*"
- }
- if isSummary {
- info = append(info, baseStyle.Width(width-1).Foreground(t.TextMuted()).Render(" (summary)"))
- }
-
- content = renderMessage(content, false, true, width, info...)
- messages = append(messages, uiMessage{
- ID: msg.ID,
- messageType: assistantMessageType,
- position: position,
- height: lipgloss.Height(content),
- content: content,
- })
- position += messages[0].height
- position++ // for the space
- } else if thinking && thinkingContent != "" {
- // Render the thinking content
- content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width)
- }
-
- for i, toolCall := range msg.ToolCalls() {
- toolCallContent := renderToolMessage(
- toolCall,
- allMessages,
- messagesService,
- focusedUIMessageId,
- false,
- width,
- i+1,
- )
- messages = append(messages, toolCallContent)
- position += toolCallContent.height
- position++ // for the space
- }
- return messages
-}
-
-func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
- for _, msg := range futureMessages {
- for _, result := range msg.ToolResults() {
- if result.ToolCallID == toolCallID {
- return &result
- }
- }
- }
- return nil
-}
-
-func toolName(name string) string {
- switch name {
- case agent.AgentToolName:
- return "Task"
- case tools.BashToolName:
- return "Bash"
- case tools.EditToolName:
- return "Edit"
- case tools.FetchToolName:
- return "Fetch"
- case tools.GlobToolName:
- return "Glob"
- case tools.GrepToolName:
- return "Grep"
- case tools.LSToolName:
- return "List"
- case tools.SourcegraphToolName:
- return "Sourcegraph"
- case tools.ViewToolName:
- return "View"
- case tools.WriteToolName:
- return "Write"
- case tools.PatchToolName:
- return "Patch"
- }
- return name
-}
-
-func getToolAction(name string) string {
- switch name {
- case agent.AgentToolName:
- return "Preparing prompt..."
- case tools.BashToolName:
- return "Building command..."
- case tools.EditToolName:
- return "Preparing edit..."
- case tools.FetchToolName:
- return "Writing fetch..."
- case tools.GlobToolName:
- return "Finding files..."
- case tools.GrepToolName:
- return "Searching content..."
- case tools.LSToolName:
- return "Listing directory..."
- case tools.SourcegraphToolName:
- return "Searching code..."
- case tools.ViewToolName:
- return "Reading file..."
- case tools.WriteToolName:
- return "Preparing write..."
- case tools.PatchToolName:
- return "Preparing patch..."
- }
- return "Working..."
-}
-
-func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
- params := ""
- switch toolCall.Name {
- case agent.AgentToolName:
- var params agent.AgentParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
- return renderParams(paramWidth, prompt)
- case tools.BashToolName:
- var params tools.BashParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- command := strings.ReplaceAll(params.Command, "\n", " ")
- return renderParams(paramWidth, command)
- case tools.EditToolName:
- var params tools.EditParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- filePath := removeWorkingDirPrefix(params.FilePath)
- return renderParams(paramWidth, filePath)
- case tools.FetchToolName:
- var params tools.FetchParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- url := params.URL
- toolParams := []string{
- url,
- }
- if params.Format != "" {
- toolParams = append(toolParams, "format", params.Format)
- }
- if params.Timeout != 0 {
- toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String())
- }
- return renderParams(paramWidth, toolParams...)
- case tools.GlobToolName:
- var params tools.GlobParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- pattern := params.Pattern
- toolParams := []string{
- pattern,
- }
- if params.Path != "" {
- toolParams = append(toolParams, "path", params.Path)
- }
- return renderParams(paramWidth, toolParams...)
- case tools.GrepToolName:
- var params tools.GrepParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- pattern := params.Pattern
- toolParams := []string{
- pattern,
- }
- if params.Path != "" {
- toolParams = append(toolParams, "path", params.Path)
- }
- if params.Include != "" {
- toolParams = append(toolParams, "include", params.Include)
- }
- if params.LiteralText {
- toolParams = append(toolParams, "literal", "true")
- }
- return renderParams(paramWidth, toolParams...)
- case tools.LSToolName:
- var params tools.LSParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- path := params.Path
- if path == "" {
- path = "."
- }
- return renderParams(paramWidth, path)
- case tools.SourcegraphToolName:
- var params tools.SourcegraphParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- return renderParams(paramWidth, params.Query)
- case tools.ViewToolName:
- var params tools.ViewParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- filePath := removeWorkingDirPrefix(params.FilePath)
- toolParams := []string{
- filePath,
- }
- if params.Limit != 0 {
- toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
- }
- if params.Offset != 0 {
- toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
- }
- return renderParams(paramWidth, toolParams...)
- case tools.WriteToolName:
- var params tools.WriteParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- filePath := removeWorkingDirPrefix(params.FilePath)
- return renderParams(paramWidth, filePath)
- default:
- input := strings.ReplaceAll(toolCall.Input, "\n", " ")
- params = renderParams(paramWidth, input)
- }
- return params
-}
-
-func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- if response.IsError {
- errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
- errContent = ansi.Truncate(errContent, width-1, "...")
- return baseStyle.
- Width(width).
- Foreground(t.Error()).
- Render(errContent)
- }
-
- resultContent := truncateHeight(response.Content, maxResultHeight)
- switch toolCall.Name {
- case agent.AgentToolName:
- return toMarkdown(resultContent, false, width)
- case tools.BashToolName:
- resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
- return toMarkdown(resultContent, true, width)
- case tools.EditToolName:
- metadata := tools.EditResponseMetadata{}
- json.Unmarshal([]byte(response.Metadata), &metadata)
- truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
- formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
- return formattedDiff
- case tools.FetchToolName:
- var params tools.FetchParams
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- mdFormat := "markdown"
- switch params.Format {
- case "text":
- mdFormat = "text"
- case "html":
- mdFormat = "html"
- }
- resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
- return toMarkdown(resultContent, true, width)
- case tools.GlobToolName:
- return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
- case tools.GrepToolName:
- return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
- case tools.LSToolName:
- return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
- case tools.SourcegraphToolName:
- return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
- case tools.ViewToolName:
- metadata := tools.ViewResponseMetadata{}
- json.Unmarshal([]byte(response.Metadata), &metadata)
- ext := filepath.Ext(metadata.FilePath)
- if ext == "" {
- ext = ""
- } else {
- ext = strings.ToLower(ext[1:])
- }
- resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
- return toMarkdown(resultContent, true, width)
- case tools.WriteToolName:
- params := tools.WriteParams{}
- json.Unmarshal([]byte(toolCall.Input), ¶ms)
- metadata := tools.WriteResponseMetadata{}
- json.Unmarshal([]byte(response.Metadata), &metadata)
- ext := filepath.Ext(params.FilePath)
- if ext == "" {
- ext = ""
- } else {
- ext = strings.ToLower(ext[1:])
- }
- resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
- return toMarkdown(resultContent, true, width)
- default:
- resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
- return toMarkdown(resultContent, true, width)
- }
-}
-
-func renderToolMessage(
- toolCall message.ToolCall,
- allMessages []message.Message,
- messagesService message.Service,
- focusedUIMessageId string,
- nested bool,
- width int,
- position int,
-) uiMessage {
- if nested {
- width = width - 3
- }
-
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- style := baseStyle.
- Width(width - 1).
- BorderLeft(true).
- BorderStyle(lipgloss.ThickBorder()).
- PaddingLeft(1).
- BorderForeground(t.TextMuted())
-
- response := findToolResponse(toolCall.ID, allMessages)
- toolNameText := baseStyle.Foreground(t.TextMuted()).
- Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
-
- if !toolCall.Finished {
- // Get a brief description of what the tool is doing
- toolAction := getToolAction(toolCall.Name)
-
- progressText := baseStyle.
- Width(width - 2 - lipgloss.Width(toolNameText)).
- Foreground(t.TextMuted()).
- Render(fmt.Sprintf("%s", toolAction))
-
- content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
- toolMsg := uiMessage{
- messageType: toolMessageType,
- position: position,
- height: lipgloss.Height(content),
- content: content,
- }
- return toolMsg
- }
-
- params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
- responseContent := ""
- if response != nil {
- responseContent = renderToolResponse(toolCall, *response, width-2)
- responseContent = strings.TrimSuffix(responseContent, "\n")
- } else {
- responseContent = baseStyle.
- Italic(true).
- Width(width - 2).
- Foreground(t.TextMuted()).
- Render("Waiting for response...")
- }
-
- parts := []string{}
- if !nested {
- formattedParams := baseStyle.
- Width(width - 2 - lipgloss.Width(toolNameText)).
- Foreground(t.TextMuted()).
- Render(params)
-
- parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
- } else {
- prefix := baseStyle.
- Foreground(t.TextMuted()).
- Render(" └ ")
- formattedParams := baseStyle.
- Width(width - 2 - lipgloss.Width(toolNameText)).
- Foreground(t.TextMuted()).
- Render(params)
- parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
- }
-
- if toolCall.Name == agent.AgentToolName {
- taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
- toolCalls := []message.ToolCall{}
- for _, v := range taskMessages {
- toolCalls = append(toolCalls, v.ToolCalls()...)
- }
- for _, call := range toolCalls {
- rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
- parts = append(parts, rendered.content)
- }
- }
- if responseContent != "" && !nested {
- parts = append(parts, responseContent)
- }
-
- content := style.Render(
- lipgloss.JoinVertical(
- lipgloss.Left,
- parts...,
- ),
- )
- if nested {
- content = lipgloss.JoinVertical(
- lipgloss.Left,
- parts...,
- )
- }
- toolMsg := uiMessage{
- messageType: toolMessageType,
- position: position,
- height: lipgloss.Height(content),
- content: content,
- }
- return toolMsg
-}
-
-func removeWorkingDirPrefix(path string) string {
- wd := config.WorkingDirectory()
- path = strings.TrimPrefix(path, wd)
- return path
-}
-
-func truncateHeight(content string, height int) string {
- lines := strings.Split(content, "\n")
- if len(lines) > height {
- return strings.Join(lines[:height], "\n")
- }
- return content
-}
-
-func renderParams(paramsWidth int, params ...string) string {
- if len(params) == 0 {
- return ""
- }
- mainParam := params[0]
- if len(mainParam) > paramsWidth {
- mainParam = mainParam[:paramsWidth-3] + "..."
- }
-
- if len(params) == 1 {
- return mainParam
- }
- otherParams := params[1:]
- // create pairs of key/value
- // if odd number of params, the last one is a key without value
- if len(otherParams)%2 != 0 {
- otherParams = append(otherParams, "")
- }
- parts := make([]string, 0, len(otherParams)/2)
- for i := 0; i < len(otherParams); i += 2 {
- key := otherParams[i]
- value := otherParams[i+1]
- if value == "" {
- continue
- }
- parts = append(parts, fmt.Sprintf("%s=%s", key, value))
- }
-
- partsRendered := strings.Join(parts, ", ")
- remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
- if remainingWidth < 30 {
- // No space for the params, just show the main
- return mainParam
- }
-
- if len(parts) > 0 {
- mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
- }
-
- return ansi.Truncate(mainParam, paramsWidth, "...")
-}
-
-// Helper function to format the time difference between two Unix timestamps
-func formatTimestampDiff(start, end int64) string {
- diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
- if diffSeconds < 1 {
- return fmt.Sprintf("%dms", int(diffSeconds*1000))
- }
- if diffSeconds < 60 {
- return fmt.Sprintf("%.1fs", diffSeconds)
- }
- return fmt.Sprintf("%.1fm", diffSeconds/60)
-}
@@ -492,6 +492,7 @@ func (m *model) rerenderItem(inx int) {
}
// check if the item is in the content
start := cachedItem.start
+ logging.Info("rerenderItem", "inx", inx, "start", start, "cachedItem.start", cachedItem.start, "cachedItem.height", cachedItem.height)
end := start + cachedItem.height
totalLines := len(m.renderedLines)
if m.reverse {
@@ -504,6 +505,9 @@ func (m *model) rerenderItem(inx int) {
}
// TODO: if hight changed do something
if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
+ if inx == len(m.items)-1 {
+ m.finalHight = max(0, start+len(rerenderedLines)-m.listHeight())
+ }
}
m.renderedItems.Store(inx, renderedItem{
lines: rerenderedLines,
@@ -541,7 +545,12 @@ func (m *model) decreaseOffset(n int) {
// UpdateItem implements List.
func (m *model) UpdateItem(inx int, item util.Model) {
m.items[inx] = item
- m.rerenderItem(inx)
+ if m.selectedItemInx == inx {
+ if i, ok := m.items[m.selectedItemInx].(layout.Focusable); ok {
+ i.Focus()
+ }
+ }
+ m.ResetView()
m.needsRerender = true
}
@@ -614,6 +623,10 @@ func (m *model) AppendItem(item util.Model) tea.Cmd {
func (m *model) DeleteItem(i int) {
m.items = slices.Delete(m.items, i, i+1)
m.renderedItems.Delete(i)
+ if m.selectedItemInx == i {
+ m.selectedItemInx--
+ }
+ m.ResetView()
m.needsRerender = true
}