From 3944930fc04a57c3da9c80d9d7377effd1277004 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Thu, 15 May 2025 15:45:22 -0500 Subject: chore: cleanup --- internal/tui/components/chat/editor.go | 1 - internal/tui/components/chat/list.go | 483 ------------------------------- internal/tui/components/chat/messages.go | 483 +++++++++++++++++++++++++++++++ 3 files changed, 483 insertions(+), 484 deletions(-) delete mode 100644 internal/tui/components/chat/list.go create mode 100644 internal/tui/components/chat/messages.go (limited to 'internal/tui/components/chat') diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 0b2c9abb8..4d5ba0128 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -243,7 +243,6 @@ func (m *editorCmp) SetSize(width, height int) tea.Cmd { m.height = height m.textarea.SetWidth(width - 3) // account for the prompt and padding right m.textarea.SetHeight(height) - m.textarea.SetWidth(width) return nil } diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go deleted file mode 100644 index baa7c7e6d..000000000 --- a/internal/tui/components/chat/list.go +++ /dev/null @@ -1,483 +0,0 @@ -package chat - -import ( - "context" - "fmt" - "math" - "time" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" - "github.com/sst/opencode/internal/message" - "github.com/sst/opencode/internal/pubsub" - "github.com/sst/opencode/internal/session" - "github.com/sst/opencode/internal/status" - "github.com/sst/opencode/internal/tui/components/dialog" - "github.com/sst/opencode/internal/tui/state" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" -) - -type cacheItem struct { - width int - content []uiMessage -} - -type messagesCmp struct { - app *app.App - width, height int - viewport viewport.Model - messages []message.Message - uiMessages []uiMessage - currentMsgID string - cachedContent map[string]cacheItem - spinner spinner.Model - rendering bool - attachments viewport.Model - showToolMessages bool -} -type renderFinishedMsg struct{} -type ToggleToolMessagesMsg struct{} - -type MessageKeys struct { - PageDown key.Binding - PageUp key.Binding - HalfPageUp key.Binding - HalfPageDown key.Binding -} - -var messageKeys = MessageKeys{ - PageDown: key.NewBinding( - key.WithKeys("pgdown"), - key.WithHelp("f/pgdn", "page down"), - ), - PageUp: key.NewBinding( - key.WithKeys("pgup"), - key.WithHelp("b/pgup", "page up"), - ), - HalfPageUp: key.NewBinding( - key.WithKeys("ctrl+u"), - key.WithHelp("ctrl+u", "½ page up"), - ), - HalfPageDown: key.NewBinding( - key.WithKeys("ctrl+d", "ctrl+d"), - key.WithHelp("ctrl+d", "½ page down"), - ), -} - -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 ToggleToolMessagesMsg: - m.showToolMessages = !m.showToolMessages - // Clear the cache to force re-rendering of all messages - m.cachedContent = make(map[string]cacheItem) - m.renderView() - return m, nil - case state.SessionSelectedMsg: - cmd := m.Reload(msg) - return m, cmd - case state.SessionClearedMsg: - 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[message.Message]: - needsRerender := false - if msg.Type == message.EventMessageCreated { - if msg.Payload.SessionID == m.app.CurrentSession.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 == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSession.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 == message.EventMessageCreated) || - (msg.Type == message.EventMessageUpdated && 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.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.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 - } - assistantMessages := renderAssistantMessage( - msg, - inx, - m.messages, - m.app.Messages, - m.currentMsgID, - m.width, - pos, - m.showToolMessages, - ) - 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( - m.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.PrimaryAgent.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 interrupt"), - ) - } else { - text += lipgloss.JoinHorizontal( - lipgloss.Left, - baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"), - baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"), - baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), - baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), - ) - } - return baseStyle. - Width(m.width). - Render(text) -} - -func (m *messagesCmp) initialScreen() string { - baseStyle := styles.BaseStyle() - - return baseStyle.Width(m.width).Render( - lipgloss.JoinVertical( - lipgloss.Top, - header(m.width), - "", - lspsConfigured(m.width), - ), - ) -} - -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.Width = width - m.viewport.Height = height - 2 - m.attachments.Width = width + 40 - m.attachments.Height = 3 - m.rerender() - return nil -} - -func (m *messagesCmp) GetSize() (int, int) { - return m.width, m.height -} - -func (m *messagesCmp) Reload(session *session.Session) tea.Cmd { - messages, err := m.app.Messages.List(context.Background(), session.ID) - if err != nil { - status.Error(err.Error()) - return nil - } - 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) tea.Model { - customSpinner := spinner.Spinner{ - Frames: []string{" ", "┃", "┃"}, - FPS: time.Second / 3, - } - s := spinner.New(spinner.WithSpinner(customSpinner)) - vp := viewport.New(0, 0) - attachmets := viewport.New(0, 0) - 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, - showToolMessages: true, - } -} diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go new file mode 100644 index 000000000..baa7c7e6d --- /dev/null +++ b/internal/tui/components/chat/messages.go @@ -0,0 +1,483 @@ +package chat + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/message" + "github.com/sst/opencode/internal/pubsub" + "github.com/sst/opencode/internal/session" + "github.com/sst/opencode/internal/status" + "github.com/sst/opencode/internal/tui/components/dialog" + "github.com/sst/opencode/internal/tui/state" + "github.com/sst/opencode/internal/tui/styles" + "github.com/sst/opencode/internal/tui/theme" +) + +type cacheItem struct { + width int + content []uiMessage +} + +type messagesCmp struct { + app *app.App + width, height int + viewport viewport.Model + messages []message.Message + uiMessages []uiMessage + currentMsgID string + cachedContent map[string]cacheItem + spinner spinner.Model + rendering bool + attachments viewport.Model + showToolMessages bool +} +type renderFinishedMsg struct{} +type ToggleToolMessagesMsg struct{} + +type MessageKeys struct { + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding +} + +var messageKeys = MessageKeys{ + PageDown: key.NewBinding( + key.WithKeys("pgdown"), + key.WithHelp("f/pgdn", "page down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup"), + key.WithHelp("b/pgup", "page up"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("ctrl+u"), + key.WithHelp("ctrl+u", "½ page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("ctrl+d", "ctrl+d"), + key.WithHelp("ctrl+d", "½ page down"), + ), +} + +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 ToggleToolMessagesMsg: + m.showToolMessages = !m.showToolMessages + // Clear the cache to force re-rendering of all messages + m.cachedContent = make(map[string]cacheItem) + m.renderView() + return m, nil + case state.SessionSelectedMsg: + cmd := m.Reload(msg) + return m, cmd + case state.SessionClearedMsg: + 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[message.Message]: + needsRerender := false + if msg.Type == message.EventMessageCreated { + if msg.Payload.SessionID == m.app.CurrentSession.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 == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSession.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 == message.EventMessageCreated) || + (msg.Type == message.EventMessageUpdated && 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.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.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 + } + assistantMessages := renderAssistantMessage( + msg, + inx, + m.messages, + m.app.Messages, + m.currentMsgID, + m.width, + pos, + m.showToolMessages, + ) + 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( + m.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.PrimaryAgent.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 interrupt"), + ) + } else { + text += lipgloss.JoinHorizontal( + lipgloss.Left, + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"), + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), + ) + } + return baseStyle. + Width(m.width). + Render(text) +} + +func (m *messagesCmp) initialScreen() string { + baseStyle := styles.BaseStyle() + + return baseStyle.Width(m.width).Render( + lipgloss.JoinVertical( + lipgloss.Top, + header(m.width), + "", + lspsConfigured(m.width), + ), + ) +} + +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.Width = width + m.viewport.Height = height - 2 + m.attachments.Width = width + 40 + m.attachments.Height = 3 + m.rerender() + return nil +} + +func (m *messagesCmp) GetSize() (int, int) { + return m.width, m.height +} + +func (m *messagesCmp) Reload(session *session.Session) tea.Cmd { + messages, err := m.app.Messages.List(context.Background(), session.ID) + if err != nil { + status.Error(err.Error()) + return nil + } + 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) tea.Model { + customSpinner := spinner.Spinner{ + Frames: []string{" ", "┃", "┃"}, + FPS: time.Second / 3, + } + s := spinner.New(spinner.WithSpinner(customSpinner)) + vp := viewport.New(0, 0) + attachmets := viewport.New(0, 0) + 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, + showToolMessages: true, + } +} -- cgit v1.2.3 From c9cca48d087cd512cea0b26a0245cbb1a64981a9 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Thu, 15 May 2025 15:57:15 -0500 Subject: fix: layout --- internal/tui/components/chat/sidebar.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'internal/tui/components/chat') diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index f2dec7878..973b03ef1 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -71,8 +71,7 @@ func (m *sidebarCmp) View() string { return baseStyle. Width(m.width). PaddingLeft(4). - PaddingRight(2). - Height(m.height - 1). + PaddingRight(1). Render( lipgloss.JoinVertical( lipgloss.Top, @@ -98,14 +97,9 @@ func (m *sidebarCmp) sessionSection() string { sessionValue := baseStyle. Foreground(t.Text()). - Width(m.width - lipgloss.Width(sessionKey)). Render(fmt.Sprintf(": %s", m.app.CurrentSession.Title)) - return lipgloss.JoinHorizontal( - lipgloss.Left, - sessionKey, - sessionValue, - ) + return sessionKey + sessionValue } func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string { -- cgit v1.2.3 From ba416e787b651ea045ff955eb32c0e7109a169e8 Mon Sep 17 00:00:00 2001 From: phantomreactor Date: Sat, 17 May 2025 01:01:50 +0530 Subject: paste images with ctrl+v (#26) --- go.mod | 4 +- internal/tui/components/chat/editor.go | 23 ++++ internal/tui/image/clipboard_unix.go | 49 ++++++++ internal/tui/image/clipboard_windows.go | 192 ++++++++++++++++++++++++++++++++ internal/tui/image/images.go | 12 ++ 5 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 internal/tui/image/clipboard_unix.go create mode 100644 internal/tui/image/clipboard_windows.go (limited to 'internal/tui/components/chat') diff --git a/go.mod b/go.mod index 777ba525b..7136d8784 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect - github.com/atotto/clipboard v0.1.4 // indirect + github.com/atotto/clipboard v0.1.4 github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect @@ -115,7 +115,7 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.37.0 // indirect - golang.org/x/image v0.26.0 // indirect + golang.org/x/image v0.26.0 golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 4d5ba0128..607aaedf3 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -2,6 +2,7 @@ package chat import ( "fmt" + "log/slog" "os" "os/exec" "slices" @@ -16,6 +17,7 @@ import ( "github.com/sst/opencode/internal/message" "github.com/sst/opencode/internal/status" "github.com/sst/opencode/internal/tui/components/dialog" + "github.com/sst/opencode/internal/tui/image" "github.com/sst/opencode/internal/tui/layout" "github.com/sst/opencode/internal/tui/styles" "github.com/sst/opencode/internal/tui/theme" @@ -34,6 +36,7 @@ type editorCmp struct { type EditorKeyMaps struct { Send key.Binding OpenEditor key.Binding + Paste key.Binding } type bluredEditorKeyMaps struct { @@ -56,6 +59,10 @@ var editorMaps = EditorKeyMaps{ key.WithKeys("ctrl+e"), key.WithHelp("ctrl+e", "open editor"), ), + Paste: key.NewBinding( + key.WithKeys("ctrl+v"), + key.WithHelp("ctrl+v", "paste content"), + ), } var DeleteKeyMaps = DeleteAttachmentKeyMaps{ @@ -200,6 +207,22 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.deleteMode = false return m, nil } + + if key.Matches(msg, editorMaps.Paste) { + imageBytes, text, err := image.GetImageFromClipboard() + if err != nil { + slog.Error(err.Error()) + return m, cmd + } + if len(imageBytes) != 0 { + attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) + attachment := message.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"} + m.attachments = append(m.attachments, attachment) + } else { + m.textarea.SetValue(m.textarea.Value() + text) + } + return m, cmd + } // Handle Enter key if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) { value := m.textarea.Value() diff --git a/internal/tui/image/clipboard_unix.go b/internal/tui/image/clipboard_unix.go new file mode 100644 index 000000000..3cb590207 --- /dev/null +++ b/internal/tui/image/clipboard_unix.go @@ -0,0 +1,49 @@ +//go:build !windows + +package image + +import ( + "bytes" + "fmt" + "image" + "github.com/atotto/clipboard" +) + +func GetImageFromClipboard() ([]byte, string, error) { + text, err := clipboard.ReadAll() + if err != nil { + return nil, "", fmt.Errorf("Error reading clipboard") + } + + if text == "" { + return nil, "", nil + } + + binaryData := []byte(text) + imageBytes, err := binaryToImage(binaryData) + if err != nil { + return nil, text, nil + } + return imageBytes, "", nil + +} + + + +func binaryToImage(data []byte) ([]byte, error) { + reader := bytes.NewReader(data) + img, _, err := image.Decode(reader) + if err != nil { + return nil, fmt.Errorf("Unable to covert bytes to image") + } + + return ImageToBytes(img) +} + + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/internal/tui/image/clipboard_windows.go b/internal/tui/image/clipboard_windows.go new file mode 100644 index 000000000..6431ce3d4 --- /dev/null +++ b/internal/tui/image/clipboard_windows.go @@ -0,0 +1,192 @@ +//go:build windows + +package image + +import ( + "bytes" + "fmt" + "image" + "image/color" + "log/slog" + "syscall" + "unsafe" +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + kernel32 = syscall.NewLazyDLL("kernel32.dll") + openClipboard = user32.NewProc("OpenClipboard") + closeClipboard = user32.NewProc("CloseClipboard") + getClipboardData = user32.NewProc("GetClipboardData") + isClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable") + globalLock = kernel32.NewProc("GlobalLock") + globalUnlock = kernel32.NewProc("GlobalUnlock") + globalSize = kernel32.NewProc("GlobalSize") +) + +const ( + CF_TEXT = 1 + CF_UNICODETEXT = 13 + CF_DIB = 8 +) + +type BITMAPINFOHEADER struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + +func GetImageFromClipboard() ([]byte, string, error) { + ret, _, _ := openClipboard.Call(0) + if ret == 0 { + return nil, "", fmt.Errorf("failed to open clipboard") + } + defer func(closeClipboard *syscall.LazyProc, a ...uintptr) { + _, _, err := closeClipboard.Call(a...) + if err != nil { + slog.Error("close clipboard failed") + return + } + }(closeClipboard) + isTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_TEXT)) + isUnicodeTextAvailable, _, _ := isClipboardFormatAvailable.Call(uintptr(CF_UNICODETEXT)) + + if isTextAvailable != 0 || isUnicodeTextAvailable != 0 { + // Get text from clipboard + var formatToUse uintptr = CF_TEXT + if isUnicodeTextAvailable != 0 { + formatToUse = CF_UNICODETEXT + } + + hClipboardText, _, _ := getClipboardData.Call(formatToUse) + if hClipboardText != 0 { + textPtr, _, _ := globalLock.Call(hClipboardText) + if textPtr != 0 { + defer func(globalUnlock *syscall.LazyProc, a ...uintptr) { + _, _, err := globalUnlock.Call(a...) + if err != nil { + slog.Error("Global unlock failed") + return + } + }(globalUnlock, hClipboardText) + + // Get clipboard text + var clipboardText string + if formatToUse == CF_UNICODETEXT { + // Convert wide string to Go string + clipboardText = syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(textPtr))[:]) + } else { + // Get size of ANSI text + size, _, _ := globalSize.Call(hClipboardText) + if size > 0 { + // Convert ANSI string to Go string + textBytes := make([]byte, size) + copy(textBytes, (*[1 << 20]byte)(unsafe.Pointer(textPtr))[:size:size]) + clipboardText = bytesToString(textBytes) + } + } + + // Check if the text is not empty + if clipboardText != "" { + return nil, clipboardText, nil + } + } + } + } + hClipboardData, _, _ := getClipboardData.Call(uintptr(CF_DIB)) + if hClipboardData == 0 { + return nil, "", fmt.Errorf("failed to get clipboard data") + } + + dataPtr, _, _ := globalLock.Call(hClipboardData) + if dataPtr == 0 { + return nil, "", fmt.Errorf("failed to lock clipboard data") + } + defer func(globalUnlock *syscall.LazyProc, a ...uintptr) { + _, _, err := globalUnlock.Call(a...) + if err != nil { + slog.Error("Global unlock failed") + return + } + }(globalUnlock, hClipboardData) + + bmiHeader := (*BITMAPINFOHEADER)(unsafe.Pointer(dataPtr)) + + width := int(bmiHeader.BiWidth) + height := int(bmiHeader.BiHeight) + if height < 0 { + height = -height + } + bitsPerPixel := int(bmiHeader.BiBitCount) + + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + var bitsOffset uintptr + if bitsPerPixel <= 8 { + numColors := uint32(1) << bitsPerPixel + if bmiHeader.BiClrUsed > 0 { + numColors = bmiHeader.BiClrUsed + } + bitsOffset = unsafe.Sizeof(*bmiHeader) + uintptr(numColors*4) + } else { + bitsOffset = unsafe.Sizeof(*bmiHeader) + } + + for y := range height { + for x := range width { + + srcY := height - y - 1 + if bmiHeader.BiHeight < 0 { + srcY = y + } + + var pixelPointer unsafe.Pointer + var r, g, b, a uint8 + + switch bitsPerPixel { + case 24: + stride := (width*3 + 3) &^ 3 + pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*stride+x*3)) + b = *(*byte)(pixelPointer) + g = *(*byte)(unsafe.Add(pixelPointer, 1)) + r = *(*byte)(unsafe.Add(pixelPointer, 2)) + a = 255 + case 32: + pixelPointer = unsafe.Pointer(dataPtr + bitsOffset + uintptr(srcY*width*4+x*4)) + b = *(*byte)(pixelPointer) + g = *(*byte)(unsafe.Add(pixelPointer, 1)) + r = *(*byte)(unsafe.Add(pixelPointer, 2)) + a = *(*byte)(unsafe.Add(pixelPointer, 3)) + if a == 0 { + a = 255 + } + default: + return nil, "", fmt.Errorf("unsupported bit count: %d", bitsPerPixel) + } + + img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a}) + } + } + + imageBytes, err := ImageToBytes(img) + if err != nil { + return nil, "", err + } + return imageBytes, "", nil +} + +func bytesToString(b []byte) string { + i := bytes.IndexByte(b, 0) + if i == -1 { + return string(b) + } + return string(b[:i]) +} diff --git a/internal/tui/image/images.go b/internal/tui/image/images.go index b55884d11..f476b201c 100644 --- a/internal/tui/image/images.go +++ b/internal/tui/image/images.go @@ -1,8 +1,10 @@ package image import ( + "bytes" "fmt" "image" + "image/png" "os" "strings" @@ -71,3 +73,13 @@ func ImagePreview(width int, filename string) (string, error) { return imageString, nil } + +func ImageToBytes(image image.Image) ([]byte, error) { + buf := new(bytes.Buffer) + err := png.Encode(buf, image) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} -- cgit v1.2.3 From 37429978898100cb0038d92a54cef8ec65018f70 Mon Sep 17 00:00:00 2001 From: Pierre Berube Date: Thu, 15 May 2025 01:55:06 -0600 Subject: fix: typo --- internal/tui/components/chat/editor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'internal/tui/components/chat') diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 607aaedf3..37ac60368 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -76,7 +76,7 @@ var DeleteKeyMaps = DeleteAttachmentKeyMaps{ ), DeleteAllAttachments: key.NewBinding( key.WithKeys("r"), - key.WithHelp("ctrl+r+r", "delete all attchments"), + key.WithHelp("ctrl+r+r", "delete all attachments"), ), } -- cgit v1.2.3 From c84918cb47d17c10286985bce7539161dfd13869 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Sat, 17 May 2025 21:35:49 +0300 Subject: feat: Add message history navigation with arrow keys (#30) --- internal/tui/components/chat/editor.go | 85 +++++++++++++++++++++++++++++--- internal/tui/components/chat/messages.go | 2 + 2 files changed, 79 insertions(+), 8 deletions(-) (limited to 'internal/tui/components/chat') diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 37ac60368..dbaa05181 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -25,18 +25,23 @@ import ( ) type editorCmp struct { - width int - height int - app *app.App - textarea textarea.Model - attachments []message.Attachment - deleteMode bool + width int + height int + app *app.App + textarea textarea.Model + attachments []message.Attachment + deleteMode bool + history []string + historyIndex int + currentMessage string } type EditorKeyMaps struct { Send key.Binding OpenEditor key.Binding Paste key.Binding + HistoryUp key.Binding + HistoryDown key.Binding } type bluredEditorKeyMaps struct { @@ -63,6 +68,14 @@ var editorMaps = EditorKeyMaps{ key.WithKeys("ctrl+v"), key.WithHelp("ctrl+v", "paste content"), ), + HistoryUp: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("up", "previous message"), + ), + HistoryDown: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("down", "next message"), + ), } var DeleteKeyMaps = DeleteAttachmentKeyMaps{ @@ -139,6 +152,15 @@ func (m *editorCmp) send() tea.Cmd { m.textarea.Reset() attachments := m.attachments + // Save to history if not empty and not a duplicate of the last entry + if value != "" { + if len(m.history) == 0 || m.history[len(m.history)-1] != value { + m.history = append(m.history, value) + } + m.historyIndex = len(m.history) + m.currentMessage = "" + } + m.attachments = nil if value == "" { return nil @@ -223,6 +245,50 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, cmd } + + // Handle history navigation with up/down arrow keys + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) { + // Get the current line number + currentLine := m.textarea.Line() + + // Only navigate history if we're at the first line + if currentLine == 0 && len(m.history) > 0 { + // Save current message if we're just starting to navigate + if m.historyIndex == len(m.history) { + m.currentMessage = m.textarea.Value() + } + + // Go to previous message in history + if m.historyIndex > 0 { + m.historyIndex-- + m.textarea.SetValue(m.history[m.historyIndex]) + } + return m, nil + } + } + + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) { + // Get the current line number and total lines + currentLine := m.textarea.Line() + value := m.textarea.Value() + lines := strings.Split(value, "\n") + totalLines := len(lines) + + // Only navigate history if we're at the last line + if currentLine == totalLines-1 { + if m.historyIndex < len(m.history)-1 { + // Go to next message in history + m.historyIndex++ + m.textarea.SetValue(m.history[m.historyIndex]) + } else if m.historyIndex == len(m.history)-1 { + // Return to the current message being composed + m.historyIndex = len(m.history) + m.textarea.SetValue(m.currentMessage) + } + return m, nil + } + } + // Handle Enter key if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) { value := m.textarea.Value() @@ -336,7 +402,10 @@ func CreateTextArea(existing *textarea.Model) textarea.Model { func NewEditorCmp(app *app.App) tea.Model { ta := CreateTextArea(nil) return &editorCmp{ - app: app, - textarea: ta, + app: app, + textarea: ta, + history: []string{}, + historyIndex: 0, + currentMessage: "", } } diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index baa7c7e6d..d6f252aad 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -386,6 +386,8 @@ func (m *messagesCmp) help() string { baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"), baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"), baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), ) -- cgit v1.2.3 From 6e854a4df4e4933d478e5eb1d48ccb34db610194 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 19 May 2025 02:41:34 +0300 Subject: fix: disable history navigation when filepicker is open (#39) --- internal/app/app.go | 13 +++++++++++++ internal/tui/components/chat/editor.go | 5 +++-- internal/tui/tui.go | 3 +++ 3 files changed, 19 insertions(+), 2 deletions(-) (limited to 'internal/tui/components/chat') diff --git a/internal/app/app.go b/internal/app/app.go index e7bbfbfa1..943f1b24e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -40,6 +40,9 @@ type App struct { watcherCancelFuncs []context.CancelFunc cancelFuncsMutex sync.Mutex watcherWG sync.WaitGroup + + // UI state + filepickerOpen bool } func New(ctx context.Context, conn *sql.DB) (*App, error) { @@ -128,6 +131,16 @@ func (app *App) initTheme() { } } +// IsFilepickerOpen returns whether the filepicker is currently open +func (app *App) IsFilepickerOpen() bool { + return app.filepickerOpen +} + +// SetFilepickerOpen sets the state of the filepicker +func (app *App) SetFilepickerOpen(open bool) { + app.filepickerOpen = open +} + // Shutdown performs a clean shutdown of the application func (app *App) Shutdown() { // Cancel all watcher goroutines diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index dbaa05181..212ad5529 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -247,7 +247,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // Handle history navigation with up/down arrow keys - if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) { + // Only handle history navigation if the filepicker is not open + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() { // Get the current line number currentLine := m.textarea.Line() @@ -267,7 +268,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) { + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() { // Get the current line number and total lines currentLine := m.textarea.Line() value := m.textarea.Value() diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 299c69793..56be04619 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -417,6 +417,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if a.showFilepicker { a.showFilepicker = false a.filepicker.ToggleFilepicker(a.showFilepicker) + a.app.SetFilepickerOpen(a.showFilepicker) } if a.showModelDialog { a.showModelDialog = false @@ -539,6 +540,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if a.showFilepicker { a.showFilepicker = false a.filepicker.ToggleFilepicker(a.showFilepicker) + a.app.SetFilepickerOpen(a.showFilepicker) return a, nil } if a.currentPage == page.LogsPage { @@ -571,6 +573,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Toggle filepicker a.showFilepicker = !a.showFilepicker a.filepicker.ToggleFilepicker(a.showFilepicker) + a.app.SetFilepickerOpen(a.showFilepicker) // Close other dialogs if opening filepicker if a.showFilepicker { -- cgit v1.2.3