diff options
| author | adamdottv <[email protected]> | 2025-06-18 13:56:46 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-18 13:56:51 -0500 |
| commit | bd46cf0f868293b501874c1f04632ced3bec7b81 (patch) | |
| tree | bcd5ed78fc41bcf73c0ffd76086bf10c34ed700b /packages/tui/internal/components | |
| parent | d4157d9a9603c099e650af4f6c369a56d3878179 (diff) | |
| download | opencode-bd46cf0f868293b501874c1f04632ced3bec7b81.tar.gz opencode-bd46cf0f868293b501874c1f04632ced3bec7b81.zip | |
feat(tui): configurable keybinds and mouse scroll
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/chat/chat.go | 22 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 371 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/message.go | 30 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 120 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/complete.go | 9 | ||||
| -rw-r--r-- | packages/tui/internal/components/dialog/help.go | 54 |
6 files changed, 224 insertions, 382 deletions
diff --git a/packages/tui/internal/components/chat/chat.go b/packages/tui/internal/components/chat/chat.go deleted file mode 100644 index 29487efb7..000000000 --- a/packages/tui/internal/components/chat/chat.go +++ /dev/null @@ -1,22 +0,0 @@ -package chat - -import ( - "github.com/sst/opencode/internal/app" - "github.com/sst/opencode/internal/styles" - "github.com/sst/opencode/internal/theme" -) - -type SendMsg struct { - Text string - Attachments []app.Attachment -} - -func repo(width int) string { - repo := "github.com/sst/opencode" - t := theme.CurrentTheme() - - return styles.BaseStyle(). - Foreground(t.TextMuted()). - Width(width). - Render(repo) -} diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index a2d33f172..46d160478 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -3,11 +3,8 @@ package chat import ( "fmt" "log/slog" - "os" - "os/exec" "strings" - "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/spinner" "github.com/charmbracelet/bubbles/v2/textarea" tea "github.com/charmbracelet/bubbletea/v2" @@ -16,6 +13,7 @@ import ( "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/image" + "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" @@ -24,78 +22,27 @@ import ( type EditorComponent interface { tea.Model tea.ViewModel + layout.Sizeable Value() string + Submit() (tea.Model, tea.Cmd) + Clear() (tea.Model, tea.Cmd) + Paste() (tea.Model, tea.Cmd) + Newline() (tea.Model, tea.Cmd) + Previous() (tea.Model, tea.Cmd) + Next() (tea.Model, tea.Cmd) } type editorComponent struct { - width int - height int app *app.App + width, height int textarea textarea.Model attachments []app.Attachment - deleteMode bool history []string historyIndex int currentMessage string spinner spinner.Model } -type EditorKeyMaps struct { - Send key.Binding - OpenEditor key.Binding - Paste key.Binding - HistoryUp key.Binding - HistoryDown key.Binding -} - -type DeleteAttachmentKeyMaps struct { - AttachmentDeleteMode key.Binding - Escape key.Binding - DeleteAllAttachments key.Binding -} - -var editorMaps = EditorKeyMaps{ - Send: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "send message"), - ), - OpenEditor: key.NewBinding( - key.WithKeys("f12"), - key.WithHelp("f12", "open editor"), - ), - Paste: key.NewBinding( - 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{ - AttachmentDeleteMode: key.NewBinding( - key.WithKeys("ctrl+r"), - key.WithHelp("ctrl+r+{i}", "delete attachment at index i"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel delete mode"), - ), - DeleteAllAttachments: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("ctrl+r+r", "delete all attachments"), - ), -} - -const ( - maxAttachments = 5 -) - func (m *editorComponent) Init() tea.Cmd { return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus) } @@ -104,153 +51,38 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd var cmd tea.Cmd switch msg := msg.(type) { + case tea.KeyPressMsg: + // Maximize editor responsiveness for printable characters + if msg.Text != "" { + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + } + + // // TODO: ? + // if key.Matches(msg, messageKeys.PageUp) || + // key.Matches(msg, messageKeys.PageDown) || + // key.Matches(msg, messageKeys.HalfPageUp) || + // key.Matches(msg, messageKeys.HalfPageDown) { + // return m, nil + // } + case dialog.ThemeSelectedMsg: m.textarea = createTextArea(&m.textarea) m.spinner = createSpinner() - return m, m.spinner.Tick + return m, tea.Batch(m.spinner.Tick, textarea.Blink) case dialog.CompletionSelectedMsg: if msg.IsCommand { - // Execute the command directly commandName := strings.TrimPrefix(msg.CompletionValue, "/") m.textarea.Reset() - return m, util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName}) + return m, util.CmdHandler( + commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]), + ) } else { - // For files, replace the text in the editor existingValue := m.textarea.Value() modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1) m.textarea.SetValue(modifiedValue) return m, nil } - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c": - if m.textarea.Value() != "" { - m.textarea.Reset() - return m, func() tea.Msg { - return nil - } - } - case "shift+enter": - value := m.textarea.Value() - m.textarea.SetValue(value + "\n") - return m, nil - } - - if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { - m.deleteMode = true - return m, nil - } - if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode { - m.deleteMode = false - m.attachments = nil - return m, nil - } - // if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) { - // num := int(msg.Runes[0] - '0') - // m.deleteMode = false - // if num < 10 && len(m.attachments) > num { - // if num == 0 { - // m.attachments = m.attachments[num+1:] - // } else { - // m.attachments = slices.Delete(m.attachments, num, num+1) - // } - // return m, nil - // } - // } - if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) || - key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) { - return m, nil - } - if key.Matches(msg, editorMaps.OpenEditor) { - if m.app.IsBusy() { - // status.Warn("Agent is working, please wait...") - return m, nil - } - value := m.textarea.Value() - m.textarea.Reset() - return m, m.openEditor(value) - } - if key.Matches(msg, DeleteKeyMaps.Escape) { - 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 := app.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 history navigation with up/down arrow keys - // Only handle history navigation if the filepicker is not open and completion dialog is not open - if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) { - // TODO: fix this - // && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { - // 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) { - // TODO: fix this - // && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { - // 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() - if len(value) > 0 && value[len(value)-1] == '\\' { - // If the last character is a backslash, remove it and add a newline - m.textarea.SetValue(value[:len(value)-1] + "\n") - return m, nil - } else { - // Otherwise, send the message - return m, m.send() - } - } } m.spinner, cmd = m.spinner.Update(msg) @@ -304,10 +136,13 @@ func (m *editorComponent) View() string { info = styles.Padded().Background(t.Background()).Render(info) content := strings.Join([]string{"", textarea, info}, "\n") - return content } +func (m *editorComponent) GetSize() (width, height int) { + return m.width, m.height +} + func (m *editorComponent) SetSize(width, height int) tea.Cmd { m.width = width m.height = height @@ -316,54 +151,22 @@ func (m *editorComponent) SetSize(width, height int) tea.Cmd { return nil } -func (m *editorComponent) GetSize() (int, int) { - return m.width, m.height +func (m *editorComponent) Value() string { + return strings.TrimSpace(m.textarea.Value()) } -func (m *editorComponent) openEditor(value string) tea.Cmd { - editor := os.Getenv("EDITOR") - if editor == "" { - editor = "nvim" +func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { + value := m.Value() + m.textarea.Reset() + if value == "" { + return m, nil } - - tmpfile, err := os.CreateTemp("", "msg_*.md") - tmpfile.WriteString(value) - if err != nil { - // status.Error(err.Error()) - return nil + if len(value) > 0 && value[len(value)-1] == '\\' { + // If the last character is a backslash, remove it and add a newline + m.textarea.SetValue(value[:len(value)-1] + "\n") + return m, nil } - tmpfile.Close() - c := exec.Command(editor, tmpfile.Name()) //nolint:gosec - c.Stdin = os.Stdin - c.Stdout = os.Stdout - c.Stderr = os.Stderr - return tea.ExecProcess(c, func(err error) tea.Msg { - if err != nil { - // status.Error(err.Error()) - return nil - } - content, err := os.ReadFile(tmpfile.Name()) - if err != nil { - // status.Error(err.Error()) - return nil - } - if len(content) == 0 { - // status.Warn("Message is empty") - return nil - } - os.Remove(tmpfile.Name()) - attachments := m.attachments - m.attachments = nil - return SendMsg{ - Text: string(content), - Attachments: attachments, - } - }) -} -func (m *editorComponent) send() tea.Cmd { - value := strings.TrimSpace(m.textarea.Value()) - m.textarea.Reset() attachments := m.attachments // Save to history if not empty and not a duplicate of the last entry @@ -376,26 +179,84 @@ func (m *editorComponent) send() tea.Cmd { } m.attachments = nil - if value == "" { - return nil - } - // Check for slash command - // if strings.HasPrefix(value, "/") { - // commandName := strings.TrimPrefix(value, "/") - // if _, ok := m.app.Commands[commandName]; ok { - // return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName}) - // } - // } - - return tea.Batch( - util.CmdHandler(SendMsg{ + return m, tea.Batch( + util.CmdHandler(app.SendMsg{ Text: value, Attachments: attachments, }), ) } +func (m *editorComponent) Clear() (tea.Model, tea.Cmd) { + m.textarea.Reset() + return m, nil +} + +func (m *editorComponent) Paste() (tea.Model, tea.Cmd) { + imageBytes, text, err := image.GetImageFromClipboard() + if err != nil { + slog.Error(err.Error()) + return m, nil + } + if len(imageBytes) != 0 { + attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) + attachment := app.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, nil +} + +func (m *editorComponent) Newline() (tea.Model, tea.Cmd) { + value := m.textarea.Value() + m.textarea.SetValue(value + "\n") + return m, nil +} + +func (m *editorComponent) Previous() (tea.Model, tea.Cmd) { + 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 + } + return m, nil +} + +func (m *editorComponent) Next() (tea.Model, tea.Cmd) { + 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 + } + return m, nil +} + func createTextArea(existing *textarea.Model) textarea.Model { t := theme.CurrentTheme() bgColor := t.BackgroundElement() @@ -439,10 +300,6 @@ func createSpinner() spinner.Model { ) } -func (m *editorComponent) Value() string { - return m.textarea.Value() -} - func NewEditorComponent(app *app.App) EditorComponent { s := createSpinner() ta := createTextArea(nil) diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 9ff06e74c..4104e028e 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -250,7 +250,7 @@ func renderToolInvocation( toolCall client.MessageToolInvocationToolCall, result *string, metadata client.MessageInfo_Metadata_Tool_AdditionalProperties, - showResult bool, + showDetails bool, isLast bool, ) string { ignoredTools := []string{"opencode_todoread"} @@ -262,7 +262,7 @@ func renderToolInvocation( innerWidth := outerWidth - 6 paddingTop := 0 paddingBottom := 0 - if showResult { + if showDetails { paddingTop = 1 if result == nil || *result == "" { paddingBottom = 1 @@ -284,8 +284,21 @@ func renderToolInvocation( BorderStyle(lipgloss.ThickBorder()) if toolCall.State == "partial-call" { + title := renderToolAction(toolCall.ToolName) + if !showDetails { + title = "∟ " + title + padding := calculatePadding() + style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle()) + return renderContentBlock(style.Render(title), + WithAlign(lipgloss.Left), + WithBorderColor(t.Accent()), + WithPaddingTop(0), + WithPaddingBottom(1), + ) + } + style = style.Foreground(t.TextMuted()) - return style.Render(renderToolAction(toolCall.ToolName)) + return style.Render(title) } toolArgs := "" @@ -370,7 +383,7 @@ func renderToolInvocation( BorderRight(true). Render(formattedDiff) - if showResult { + if showDetails { style = style.Width(lipgloss.Width(formattedDiff)) title += "\n" } @@ -443,7 +456,8 @@ func renderToolInvocation( body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1)) } - if !showResult { + if !showDetails { + title = "∟ " + title padding := calculatePadding() style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle()) paddingBottom := 0 @@ -471,10 +485,10 @@ func renderToolInvocation( content, lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), ) - if showResult && body != "" && error == "" { + if showDetails && body != "" && error == "" { content += "\n" + body } - if showResult && error != "" { + if showDetails && error != "" { content += "\n" + error } return content @@ -561,6 +575,8 @@ func renderToolAction(name string) string { return "Reading file..." case "opencode_write": return "Preparing write..." + case "opencode_todowrite", "opencode_todoread": + return "Planning..." case "opencode_patch": return "Preparing patch..." case "opencode_batch": diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 577592b7e..3016c24d9 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -5,7 +5,6 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/spinner" "github.com/charmbracelet/bubbles/v2/viewport" tea "github.com/charmbracelet/bubbletea/v2" @@ -21,47 +20,29 @@ import ( type MessagesComponent interface { tea.Model tea.ViewModel + PageUp() (tea.Model, tea.Cmd) + PageDown() (tea.Model, tea.Cmd) + HalfPageUp() (tea.Model, tea.Cmd) + HalfPageDown() (tea.Model, tea.Cmd) + First() (tea.Model, tea.Cmd) + Last() (tea.Model, tea.Cmd) + // Previous() (tea.Model, tea.Cmd) + // Next() (tea.Model, tea.Cmd) } type messagesComponent struct { - app *app.App width, height int + app *app.App viewport viewport.Model spinner spinner.Model - rendering bool attachments viewport.Model - showToolResults bool cache *MessageCache + rendering bool + showToolDetails bool tail 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"), - ), -} +type ToggleToolDetailsMsg struct{} func (m *messagesComponent) Init() tea.Cmd { return tea.Batch(m.viewport.Init(), m.spinner.Tick) @@ -69,8 +50,8 @@ func (m *messagesComponent) Init() tea.Cmd { func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - switch msg := msg.(type) { - case SendMsg: + switch msg.(type) { + case app.SendMsg: m.viewport.GotoBottom() m.tail = true return m, nil @@ -78,8 +59,8 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cache.Clear() m.renderView() return m, nil - case ToggleToolMessagesMsg: - m.showToolResults = !m.showToolResults + case ToggleToolDetailsMsg: + m.showToolDetails = !m.showToolDetails m.renderView() return m, nil case app.SessionSelectedMsg: @@ -91,33 +72,23 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.cache.Clear() cmd := m.Reload() return m, cmd - 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 - m.tail = m.viewport.AtBottom() - cmds = append(cmds, cmd) - } case renderFinishedMsg: m.rendering = false if m.tail { m.viewport.GotoBottom() } - case client.EventSessionUpdated: - m.renderView() - if m.tail { - m.viewport.GotoBottom() - } - case client.EventMessageUpdated: + case client.EventSessionUpdated, client.EventMessageUpdated: m.renderView() if m.tail { m.viewport.GotoBottom() } } + viewport, cmd := m.viewport.Update(msg) + m.viewport = viewport + m.tail = m.viewport.AtBottom() + cmds = append(cmds, cmd) + spinner, cmd := m.spinner.Update(msg) m.spinner = spinner cmds = append(cmds, cmd) @@ -208,7 +179,7 @@ func (m *messagesComponent) renderView() { if toolCall.State == "result" { key := m.cache.GenerateKey(message.Id, toolCall.ToolCallId, - m.showToolResults, + m.showToolDetails, layout.Current.Viewport.Width, ) content, cached = m.cache.Get(key) @@ -217,7 +188,7 @@ func (m *messagesComponent) renderView() { toolCall, result, metadata, - m.showToolResults, + m.showToolDetails, isLastToolInvocation, ) m.cache.Set(key, content) @@ -228,12 +199,12 @@ func (m *messagesComponent) renderView() { toolCall, result, metadata, - m.showToolResults, + m.showToolDetails, isLastToolInvocation, ) } - if previousBlockType != toolInvocationBlock && m.showToolResults { + if previousBlockType != toolInvocationBlock && m.showToolDetails { blocks = append(blocks, "") } blocks = append(blocks, content) @@ -423,6 +394,38 @@ func (m *messagesComponent) Reload() tea.Cmd { } } +func (m *messagesComponent) PageUp() (tea.Model, tea.Cmd) { + m.viewport.ViewUp() + return m, nil +} + +func (m *messagesComponent) PageDown() (tea.Model, tea.Cmd) { + m.viewport.ViewDown() + return m, nil +} + +func (m *messagesComponent) HalfPageUp() (tea.Model, tea.Cmd) { + m.viewport.HalfViewUp() + return m, nil +} + +func (m *messagesComponent) HalfPageDown() (tea.Model, tea.Cmd) { + m.viewport.HalfViewDown() + return m, nil +} + +func (m *messagesComponent) First() (tea.Model, tea.Cmd) { + m.viewport.GotoTop() + m.tail = false + return m, nil +} + +func (m *messagesComponent) Last() (tea.Model, tea.Cmd) { + m.viewport.GotoBottom() + m.tail = true + return m, nil +} + func NewMessagesComponent(app *app.App) MessagesComponent { customSpinner := spinner.Spinner{ Frames: []string{" ", "┃", "┃"}, @@ -432,17 +435,14 @@ func NewMessagesComponent(app *app.App) MessagesComponent { vp := viewport.New() attachments := viewport.New() - vp.KeyMap.PageUp = messageKeys.PageUp - vp.KeyMap.PageDown = messageKeys.PageDown - vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp - vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown + vp.KeyMap = viewport.KeyMap{} return &messagesComponent{ app: app, viewport: vp, spinner: s, attachments: attachments, - showToolResults: true, + showToolDetails: true, cache: NewMessageCache(), tail: true, } diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go index ca86b00e6..c627e96e9 100644 --- a/packages/tui/internal/components/dialog/complete.go +++ b/packages/tui/internal/components/dialog/complete.go @@ -1,6 +1,8 @@ package dialog import ( + "log/slog" + "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/textarea" tea "github.com/charmbracelet/bubbletea/v2" @@ -144,6 +146,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { c.list.SetItems(msg) case tea.KeyMsg: if c.pseudoSearchTextArea.Focused() { + slog.Info("CompletionDialog", "key", msg.String(), "focused", true) if !key.Matches(msg, completionDialogKeys.Complete) { var cmd tea.Cmd c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg) @@ -159,10 +162,10 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { c.query = query cmd = func() tea.Msg { items, err := c.completionProvider.GetChildEntries(query) + slog.Info("CompletionDialog", "query", query, "items", len(items)) if err != nil { - // status.Error(err.Error()) + slog.Error("Failed to get completion items", "error", err) } - // c.list.SetItems(items) return items } cmds = append(cmds, cmd) @@ -189,9 +192,11 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return c, tea.Batch(cmds...) } else { + slog.Info("CompletionDialog", "key", msg.String(), "focused", false) cmd := func() tea.Msg { items, err := c.completionProvider.GetChildEntries("") if err != nil { + slog.Error("Failed to get completion items", "error", err) // status.Error(err.Error()) } return items diff --git a/packages/tui/internal/components/dialog/help.go b/packages/tui/internal/components/dialog/help.go index 2ef887890..1886714e4 100644 --- a/packages/tui/internal/components/dialog/help.go +++ b/packages/tui/internal/components/dialog/help.go @@ -3,9 +3,9 @@ package dialog import ( "strings" - "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" + "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/theme" @@ -15,28 +15,9 @@ type helpDialog struct { width int height int modal *modal.Modal - bindings []key.Binding + commands commands.CommandRegistry } -// func (i bindingItem) Render(selected bool, width int) string { -// t := theme.CurrentTheme() -// baseStyle := styles.BaseStyle(). -// Width(width - 2). -// Background(t.BackgroundElement()) -// -// if selected { -// baseStyle = baseStyle. -// Background(t.Primary()). -// Foreground(t.BackgroundElement()). -// Bold(true) -// } else { -// baseStyle = baseStyle. -// Foreground(t.Text()) -// } -// -// return baseStyle.Padding(0, 1).Render(i.binding.Help().Desc) -// } - func (h *helpDialog) Init() tea.Cmd { return nil } @@ -63,19 +44,24 @@ func (h *helpDialog) View() string { PaddingLeft(1).Background(t.BackgroundElement()) lines := []string{} - for _, b := range h.bindings { - content := keyStyle.Render(b.Help().Key) - content += descStyle.Render(" " + b.Help().Desc) - for i, key := range b.Keys() { - if i == 0 { - keyString := " (" + strings.ToUpper(key) + ")" - // space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0) - // spacer := strings.Repeat(" ", space) - // content += descStyle.Render(spacer) - content += descStyle.Render(keyString) - } + for _, b := range h.commands { + // Only interested in slash commands + if b.Trigger == "" { + continue } + content := keyStyle.Render("/" + b.Trigger) + content += descStyle.Render(" " + b.Description) + // for i, key := range b.Keybindings { + // if i == 0 { + // keyString := " (" + key.Key + ")" + // space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0) + // spacer := strings.Repeat(" ", space) + // content += descStyle.Render(spacer) + // content += descStyle.Render(keyString) + // } + // } + lines = append(lines, contentStyle.Render(content)) } @@ -94,9 +80,9 @@ type HelpDialog interface { layout.Modal } -func NewHelpDialog(bindings ...key.Binding) HelpDialog { +func NewHelpDialog(commands commands.CommandRegistry) HelpDialog { return &helpDialog{ - bindings: bindings, + commands: commands, modal: modal.New(), } } |
