summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-18 13:56:46 -0500
committeradamdottv <[email protected]>2025-06-18 13:56:51 -0500
commitbd46cf0f868293b501874c1f04632ced3bec7b81 (patch)
treebcd5ed78fc41bcf73c0ffd76086bf10c34ed700b /packages/tui/internal/components
parentd4157d9a9603c099e650af4f6c369a56d3878179 (diff)
downloadopencode-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.go22
-rw-r--r--packages/tui/internal/components/chat/editor.go371
-rw-r--r--packages/tui/internal/components/chat/message.go30
-rw-r--r--packages/tui/internal/components/chat/messages.go120
-rw-r--r--packages/tui/internal/components/dialog/complete.go9
-rw-r--r--packages/tui/internal/components/dialog/help.go54
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(),
}
}