summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/tui/internal/completions/commands.go71
-rw-r--r--packages/tui/internal/completions/files-folders.go20
-rw-r--r--packages/tui/internal/completions/manager.go29
-rw-r--r--packages/tui/internal/components/chat/editor.go38
-rw-r--r--packages/tui/internal/components/chat/message.go114
-rw-r--r--packages/tui/internal/components/chat/messages.go3
-rw-r--r--packages/tui/internal/components/dialog/complete.go57
-rw-r--r--packages/tui/internal/components/dialog/session.go8
-rw-r--r--packages/tui/internal/components/dialog/theme.go8
-rw-r--r--packages/tui/internal/components/list/list.go (renamed from packages/tui/internal/components/util/simple-list.go)10
-rw-r--r--packages/tui/internal/layout/container.go6
-rw-r--r--packages/tui/internal/page/chat.go28
-rw-r--r--packages/tui/internal/tui/tui.go30
13 files changed, 301 insertions, 121 deletions
diff --git a/packages/tui/internal/completions/commands.go b/packages/tui/internal/completions/commands.go
new file mode 100644
index 000000000..3bb28ac9b
--- /dev/null
+++ b/packages/tui/internal/completions/commands.go
@@ -0,0 +1,71 @@
+package completions
+
+import (
+ "sort"
+
+ "github.com/lithammer/fuzzysearch/fuzzy"
+ "github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/components/dialog"
+)
+
+type CommandCompletionProvider struct {
+ app *app.App
+}
+
+func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider {
+ return &CommandCompletionProvider{app: app}
+}
+
+func (c *CommandCompletionProvider) GetId() string {
+ return "commands"
+}
+
+func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
+ return dialog.NewCompletionItem(dialog.CompletionItem{
+ Title: "Commands",
+ Value: "commands",
+ })
+}
+
+func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
+ if query == "" {
+ // If no query, return all commands
+ items := []dialog.CompletionItemI{}
+ for _, cmd := range c.app.Commands {
+ items = append(items, dialog.NewCompletionItem(dialog.CompletionItem{
+ Title: " /" + cmd.Name,
+ Value: "/" + cmd.Name,
+ }))
+ }
+ return items, nil
+ }
+
+ // Use fuzzy matching for commands
+ var commandNames []string
+ commandMap := make(map[string]dialog.CompletionItemI)
+
+ for _, cmd := range c.app.Commands {
+ commandNames = append(commandNames, cmd.Name)
+ commandMap[cmd.Name] = dialog.NewCompletionItem(dialog.CompletionItem{
+ Title: " /" + cmd.Name,
+ Value: "/" + cmd.Name,
+ })
+ }
+
+ // Find fuzzy matches
+ matches := fuzzy.RankFind(query, commandNames)
+
+ // Sort by score (best matches first)
+ sort.Sort(matches)
+
+ // Convert matches to completion items
+ items := []dialog.CompletionItemI{}
+ for _, match := range matches {
+ if item, ok := commandMap[match.Target]; ok {
+ items = append(items, item)
+ }
+ }
+
+ return items, nil
+}
+
diff --git a/packages/tui/internal/completions/files-folders.go b/packages/tui/internal/completions/files-folders.go
index a0c5260de..0e6a4e4b6 100644
--- a/packages/tui/internal/completions/files-folders.go
+++ b/packages/tui/internal/completions/files-folders.go
@@ -1,10 +1,15 @@
package completions
import (
+ "context"
+
+ "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
+ "github.com/sst/opencode/pkg/client"
)
type filesAndFoldersContextGroup struct {
+ app *app.App
prefix string
}
@@ -20,7 +25,17 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
}
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
- return []string{}, nil
+ response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
+ Query: query,
+ })
+ if err != nil {
+ return []string{}, err
+ }
+ if response.JSON200 == nil {
+ return []string{}, nil
+ }
+
+ return *response.JSON200, nil
}
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
@@ -41,8 +56,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.C
return items, nil
}
-func NewFileAndFolderContextGroup() dialog.CompletionProvider {
+func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
return &filesAndFoldersContextGroup{
+ app: app,
prefix: "file",
}
}
diff --git a/packages/tui/internal/completions/manager.go b/packages/tui/internal/completions/manager.go
new file mode 100644
index 000000000..19e532f45
--- /dev/null
+++ b/packages/tui/internal/completions/manager.go
@@ -0,0 +1,29 @@
+package completions
+
+import (
+ "strings"
+
+ "github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/components/dialog"
+)
+
+type CompletionManager struct {
+ providers map[string]dialog.CompletionProvider
+}
+
+func NewCompletionManager(app *app.App) *CompletionManager {
+ return &CompletionManager{
+ providers: map[string]dialog.CompletionProvider{
+ "files": NewFileAndFolderContextGroup(app),
+ "commands": NewCommandCompletionProvider(app),
+ },
+ }
+}
+
+func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
+ if strings.HasPrefix(input, "/") {
+ return m.providers["commands"]
+ }
+ return m.providers["files"]
+}
+
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 2e89a7db4..95ca3e14b 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -44,11 +44,6 @@ type EditorKeyMaps struct {
HistoryDown key.Binding
}
-type bluredEditorKeyMaps struct {
- Send key.Binding
- Focus key.Binding
- OpenEditor key.Binding
-}
type DeleteAttachmentKeyMaps struct {
AttachmentDeleteMode key.Binding
Escape key.Binding
@@ -108,10 +103,18 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ThemeChangedMsg:
m.textarea = createTextArea(&m.textarea)
case dialog.CompletionSelectedMsg:
- existingValue := m.textarea.Value()
- modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
- m.textarea.SetValue(modifiedValue)
- return m, nil
+ if msg.IsCommand {
+ // Execute the command directly
+ commandName := strings.TrimPrefix(msg.CompletionValue, "/")
+ m.textarea.Reset()
+ return m, util.CmdHandler(commands.ExecuteCommandMsg{Name: 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":
@@ -378,12 +381,13 @@ func (m *editorComponent) send() tea.Cmd {
}
// 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})
- }
- }
+ // if strings.HasPrefix(value, "/") {
+ // commandName := strings.TrimPrefix(value, "/")
+ // if _, ok := m.app.Commands[commandName]; ok {
+ // return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
+ // }
+ // }
+ slog.Info("Send message", "value", value)
return tea.Batch(
util.CmdHandler(SendMsg{
@@ -452,6 +456,10 @@ func createTextArea(existing *textarea.Model) textarea.Model {
return ta
}
+func (m *editorComponent) GetValue() string {
+ return m.textarea.Value()
+}
+
func NewEditorComponent(app *app.App) layout.ModelWithView {
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
ta := createTextArea(nil)
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 44aceea93..3315d4ba3 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -216,7 +216,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
align = lipgloss.Left
}
- textWidth := lipgloss.Width(text)
+ textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
content = lipgloss.JoinVertical(align, content, info)
@@ -299,13 +299,9 @@ func renderToolInvocation(
body := ""
error := ""
finished := result != nil && *result != ""
- if finished {
- body = *result
- }
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
if m, ok := metadata.Get("message"); ok {
- body = "" // don't show the body if there's an error
style = style.BorderLeftForeground(t.Error())
error = styles.BaseStyle().
Background(t.BackgroundSubtle()).
@@ -336,58 +332,61 @@ func renderToolInvocation(
case "opencode_read":
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
- body = ""
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
body = renderFile(filename, body, WithTruncate(6))
}
case "opencode_edit":
- filename := toolArgsMap["filePath"].(string)
- title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
- if d, ok := metadata.Get("diff"); ok {
- patch := d.(string)
- var formattedDiff string
- if layout.Current.Viewport.Width < 80 {
- formattedDiff, _ = diff.FormatUnifiedDiff(
- filename,
- patch,
- diff.WithWidth(layout.Current.Container.Width-2),
+ if filename, ok := toolArgsMap["filePath"].(string); ok {
+ title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
+ if d, ok := metadata.Get("diff"); ok {
+ patch := d.(string)
+ var formattedDiff string
+ if layout.Current.Viewport.Width < 80 {
+ formattedDiff, _ = diff.FormatUnifiedDiff(
+ filename,
+ patch,
+ diff.WithWidth(layout.Current.Container.Width-2),
+ )
+ } else {
+ diffWidth := min(layout.Current.Viewport.Width-2, 120)
+ formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
+ }
+ formattedDiff = strings.TrimSpace(formattedDiff)
+ formattedDiff = lipgloss.NewStyle().
+ BorderStyle(lipgloss.ThickBorder()).
+ BorderForeground(t.BackgroundSubtle()).
+ BorderLeft(true).
+ BorderRight(true).
+ Render(formattedDiff)
+
+ if showResult {
+ style = style.Width(lipgloss.Width(formattedDiff))
+ title += "\n"
+ }
+
+ body = strings.TrimSpace(formattedDiff)
+ body = lipgloss.Place(
+ layout.Current.Viewport.Width,
+ lipgloss.Height(body)+1,
+ lipgloss.Center,
+ lipgloss.Top,
+ body,
)
- } else {
- diffWidth := min(layout.Current.Viewport.Width-2, 120)
- formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
- }
- formattedDiff = strings.TrimSpace(formattedDiff)
- formattedDiff = lipgloss.NewStyle().
- BorderStyle(lipgloss.ThickBorder()).
- BorderForeground(t.BackgroundSubtle()).
- BorderLeft(true).
- BorderRight(true).
- Render(formattedDiff)
-
- if showResult {
- style = style.Width(lipgloss.Width(formattedDiff))
- title += "\n"
}
-
- body = strings.TrimSpace(formattedDiff)
- body = lipgloss.Place(
- layout.Current.Viewport.Width,
- lipgloss.Height(body)+1,
- lipgloss.Center,
- lipgloss.Top,
- body,
- )
}
case "opencode_write":
- filename := toolArgsMap["filePath"].(string)
- title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
- content := toolArgsMap["content"].(string)
- body = renderFile(filename, content)
+ if filename, ok := toolArgsMap["filePath"].(string); ok {
+ title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
+ if content, ok := toolArgsMap["content"].(string); ok {
+ body = renderFile(filename, content)
+ }
+ }
case "opencode_bash":
- description := toolArgsMap["description"].(string)
- title = fmt.Sprintf("Shell: %s %s", description, elapsed)
+ if description, ok := toolArgsMap["description"].(string); ok {
+ title = fmt.Sprintf("Shell: %s %s", description, elapsed)
+ }
if stdout, ok := metadata.Get("stdout"); ok {
command := toolArgsMap["command"].(string)
stdout := stdout.(string)
@@ -396,18 +395,20 @@ func renderToolInvocation(
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "opencode_webfetch":
+ toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
- format := toolArgsMap["format"].(string)
- body = truncateHeight(body, 10)
- if format == "html" || format == "markdown" {
- body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
+ if format, ok := toolArgsMap["format"].(string); ok {
+ body = *result
+ body = truncateHeight(body, 10)
+ if format == "html" || format == "markdown" {
+ body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
+ }
+ body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
- body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
case "opencode_todowrite":
title = fmt.Sprintf("Planning %s", elapsed)
if to, ok := metadata.Get("todos"); ok && finished {
- body = ""
todos := to.([]any)
for _, todo := range todos {
t := todo.(map[string]any)
@@ -416,7 +417,7 @@ func renderToolInvocation(
case "completed":
body += fmt.Sprintf("- [x] %s\n", content)
// case "in-progress":
- // body += fmt.Sprintf("- [ ] _%s_\n", content)
+ // body += fmt.Sprintf("- [ ] %s\n", content)
default:
body += fmt.Sprintf("- [ ] %s\n", content)
}
@@ -427,6 +428,13 @@ func renderToolInvocation(
default:
toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
+ body = *result
+ body = truncateHeight(body, 10)
+ body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
+ }
+
+ if body == "" && error == "" {
+ body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 8ab11a54a..83c3898a7 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -245,7 +245,7 @@ func (m *messagesComponent) header() string {
base := styles.BaseStyle().Render
muted := styles.Muted().Render
headerLines := []string{}
- headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-4, t.Background()))
+ headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
} else {
@@ -256,6 +256,7 @@ func (m *messagesComponent) header() string {
header = styles.BaseStyle().
Width(width).
PaddingLeft(2).
+ PaddingRight(2).
// Background(t.BackgroundElement()).
BorderLeft(true).
BorderRight(true).
diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go
index 8819184ac..fbf5c79f1 100644
--- a/packages/tui/internal/components/dialog/complete.go
+++ b/packages/tui/internal/components/dialog/complete.go
@@ -5,7 +5,7 @@ import (
"github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
- utilComponents "github.com/sst/opencode/internal/components/util"
+ "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
@@ -20,7 +20,7 @@ type CompletionItem struct {
}
type CompletionItemI interface {
- utilComponents.ListItem
+ list.ListItem
GetValue() string
DisplayValue() string
}
@@ -30,18 +30,18 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
baseStyle := styles.BaseStyle()
itemStyle := baseStyle.
+ Background(t.BackgroundElement()).
Width(width).
Padding(0, 1)
if selected {
itemStyle = itemStyle.
- Background(t.Background()).
Foreground(t.Primary()).
Bold(true)
}
title := itemStyle.Render(
- ci.GetValue(),
+ ci.DisplayValue(),
)
return title
@@ -68,6 +68,7 @@ type CompletionProvider interface {
type CompletionSelectedMsg struct {
SearchString string
CompletionValue string
+ IsCommand bool
}
type CompletionDialogCompleteItemMsg struct {
@@ -80,6 +81,7 @@ type CompletionDialog interface {
layout.ModelWithView
SetWidth(width int)
IsEmpty() bool
+ SetProvider(provider CompletionProvider)
}
type completionDialogComponent struct {
@@ -88,7 +90,7 @@ type completionDialogComponent struct {
width int
height int
pseudoSearchTextArea textarea.Model
- list utilComponents.List[CompletionItemI]
+ list list.List[CompletionItemI]
}
type completionDialogKeyMap struct {
@@ -116,10 +118,14 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
return nil
}
+ // Check if this is a command completion
+ isCommand := c.completionProvider.GetId() == "commands"
+
return tea.Batch(
util.CmdHandler(CompletionSelectedMsg{
SearchString: value,
CompletionValue: item.GetValue(),
+ IsCommand: isCommand,
}),
c.close(),
)
@@ -160,7 +166,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
u, cmd := c.list.Update(msg)
- c.list = u.(utilComponents.List[CompletionItemI])
+ c.list = u.(list.List[CompletionItemI])
cmds = append(cmds, cmd)
}
@@ -171,8 +177,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if i == -1 {
return c, nil
}
- cmd := c.complete(item)
- return c, cmd
+ return c, c.complete(item)
case key.Matches(msg, completionDialogKeys.Cancel):
// Only close on backspace when there are no characters left
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
@@ -203,21 +208,20 @@ func (c *completionDialogComponent) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
- // maxWidth := 40
- //
- // completions := c.list.GetItems()
+ maxWidth := 40
+ completions := c.list.GetItems()
- // for _, cmd := range completions {
- // title := cmd.DisplayValue()
- // if len(title) > maxWidth-4 {
- // maxWidth = len(title) + 4
- // }
- // }
+ for _, cmd := range completions {
+ title := cmd.DisplayValue()
+ if len(title) > maxWidth-4 {
+ maxWidth = len(title) + 4
+ }
+ }
- // c.list.SetMaxWidth(maxWidth)
+ c.list.SetMaxWidth(maxWidth)
return baseStyle.Padding(0, 0).
- Background(t.BackgroundSubtle()).
+ Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder()).
BorderTop(false).
BorderBottom(false).
@@ -236,6 +240,17 @@ func (c *completionDialogComponent) IsEmpty() bool {
return c.list.IsEmpty()
}
+func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
+ if c.completionProvider.GetId() != provider.GetId() {
+ c.completionProvider = provider
+ items, err := provider.GetChildEntries("")
+ if err != nil {
+ status.Error(err.Error())
+ }
+ c.list.SetItems(items)
+ }
+}
+
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
ti := textarea.New()
@@ -244,10 +259,10 @@ func NewCompletionDialogComponent(completionProvider CompletionProvider) Complet
status.Error(err.Error())
}
- li := utilComponents.NewListComponent(
+ li := list.NewListComponent(
items,
7,
- "No matching files",
+ "No matches",
false,
)
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go
index 4a41c71f3..48278d85e 100644
--- a/packages/tui/internal/components/dialog/session.go
+++ b/packages/tui/internal/components/dialog/session.go
@@ -5,8 +5,8 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
- components "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
@@ -48,7 +48,7 @@ type sessionDialog struct {
height int
modal *modal.Modal
selectedSessionID string
- list components.List[sessionItem]
+ list list.List[sessionItem]
}
func (s *sessionDialog) Init() tea.Cmd {
@@ -77,7 +77,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
listModel, cmd := s.list.Update(msg)
- s.list = listModel.(components.List[sessionItem])
+ s.list = listModel.(list.List[sessionItem])
return s, cmd
}
@@ -98,7 +98,7 @@ func NewSessionDialog(app *app.App) SessionDialog {
sessionItems = append(sessionItems, sessionItem{session: sess})
}
- list := components.NewListComponent(
+ list := list.NewListComponent(
sessionItems,
10, // maxVisibleSessions
"No sessions available",
diff --git a/packages/tui/internal/components/dialog/theme.go b/packages/tui/internal/components/dialog/theme.go
index 2febc1fe0..f045b1b09 100644
--- a/packages/tui/internal/components/dialog/theme.go
+++ b/packages/tui/internal/components/dialog/theme.go
@@ -2,8 +2,8 @@ package dialog
import (
tea "github.com/charmbracelet/bubbletea/v2"
+ list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
- components "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
@@ -49,7 +49,7 @@ type themeDialog struct {
height int
modal *modal.Modal
- list components.List[themeItem]
+ list list.List[themeItem]
}
func (t *themeDialog) Init() tea.Cmd {
@@ -84,7 +84,7 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
listModel, cmd := t.list.Update(msg)
- t.list = listModel.(components.List[themeItem])
+ t.list = listModel.(list.List[themeItem])
return t, cmd
}
@@ -110,7 +110,7 @@ func NewThemeDialog() ThemeDialog {
}
}
- list := components.NewListComponent(
+ list := list.NewListComponent(
themeItems,
10, // maxVisibleThemes
"No themes available",
diff --git a/packages/tui/internal/components/util/simple-list.go b/packages/tui/internal/components/list/list.go
index 6df4e313e..8be3c7b01 100644
--- a/packages/tui/internal/components/util/simple-list.go
+++ b/packages/tui/internal/components/list/list.go
@@ -1,11 +1,10 @@
-package utilComponents
+package list
import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
)
type ListItem interface {
@@ -116,18 +115,13 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
}
func (c *listComponent[T]) View() string {
- baseStyle := styles.BaseStyle()
-
items := c.items
maxWidth := c.maxWidth
maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0
if len(items) <= 0 {
- return baseStyle.
- Padding(0, 1).
- Width(maxWidth).
- Render(c.fallbackMsg)
+ return c.fallbackMsg
}
if len(items) > maxVisibleItems {
diff --git a/packages/tui/internal/layout/container.go b/packages/tui/internal/layout/container.go
index c57b7bd7e..b1310f498 100644
--- a/packages/tui/internal/layout/container.go
+++ b/packages/tui/internal/layout/container.go
@@ -19,6 +19,7 @@ type Container interface {
MaxWidth() int
Alignment() lipgloss.Position
GetPosition() (x, y int)
+ GetContent() ModelWithView
}
type container struct {
@@ -177,6 +178,11 @@ func (c *container) GetPosition() (x, y int) {
return c.x, c.y
}
+// GetContent returns the content of the container
+func (c *container) GetContent() ModelWithView {
+ return c.content
+}
+
type ContainerOption func(*container)
func NewContainer(content ModelWithView, options ...ContainerOption) Container {
diff --git a/packages/tui/internal/page/chat.go b/packages/tui/internal/page/chat.go
index a117e80f5..d97171e16 100644
--- a/packages/tui/internal/page/chat.go
+++ b/packages/tui/internal/page/chat.go
@@ -22,6 +22,7 @@ type chatPage struct {
messages layout.Container
layout layout.FlexLayout
completionDialog dialog.CompletionDialog
+ completionManager *completions.CompletionManager
showCompletionDialog bool
}
@@ -94,13 +95,20 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if p.showCompletionDialog {
+ // Get the current text from the editor to determine which provider to use
+ editorModel := p.editor.GetContent().(interface{ GetValue() string })
+ currentInput := editorModel.GetValue()
+
+ provider := p.completionManager.GetProvider(currentInput)
+ p.completionDialog.SetProvider(provider)
+
context, contextCmd := p.completionDialog.Update(msg)
p.completionDialog = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
- // Doesn't forward event if enter key is pressed
+ // Doesn't forward event if enter key is pressed and there are completions
if keyMsg, ok := msg.(tea.KeyMsg); ok {
- if keyMsg.String() == "enter" && !p.completionDialog.IsEmpty() {
+ if keyMsg.String() == "enter" { // && !p.completionDialog.IsEmpty() {
return p, tea.Batch(cmds...)
}
}
@@ -149,8 +157,10 @@ func (p *chatPage) View() string {
}
func NewChatPage(app *app.App) layout.ModelWithView {
- cg := completions.NewFileAndFolderContextGroup()
- completionDialog := dialog.NewCompletionDialogComponent(cg)
+ completionManager := completions.NewCompletionManager(app)
+ initialProvider := completionManager.GetProvider("")
+ completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
+
messagesContainer := layout.NewContainer(
chat.NewMessagesComponent(app),
)
@@ -159,11 +169,13 @@ func NewChatPage(app *app.App) layout.ModelWithView {
layout.WithMaxWidth(layout.Current.Container.Width),
layout.WithAlignCenter(),
)
+
return &chatPage{
- app: app,
- editor: editorContainer,
- messages: messagesContainer,
- completionDialog: completionDialog,
+ app: app,
+ editor: editorContainer,
+ messages: messagesContainer,
+ completionDialog: completionDialog,
+ completionManager: completionManager,
layout: layout.NewFlexLayout(
layout.WithPanes(messagesContainer, editorContainer),
layout.WithDirection(layout.FlexDirectionVertical),
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 70cbf3b47..b678d553b 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -2,6 +2,7 @@ package tui
import (
"context"
+ "log/slog"
"github.com/charmbracelet/bubbles/v2/cursor"
"github.com/charmbracelet/bubbles/v2/key"
@@ -78,33 +79,52 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if a.modal != nil {
- isModalTrigger := false
+ bypassModal := false
+
if _, ok := msg.(modal.CloseModalMsg); ok {
a.modal = nil
return a, nil
}
+
if msg, ok := msg.(tea.KeyMsg); ok {
switch msg.String() {
case "esc":
a.modal = nil
return a, nil
case "ctrl+c":
- if _, ok := a.modal.(dialog.QuitDialog); !ok {
+ if _, ok := a.modal.(dialog.QuitDialog); ok {
+ return a, tea.Quit
+ } else {
quitDialog := dialog.NewQuitDialog()
a.modal = quitDialog
return a, nil
}
}
+ // don't send commands to the modal
for _, cmdDef := range a.app.Commands {
if key.Matches(msg, cmdDef.KeyBinding) {
- isModalTrigger = true
+ bypassModal = true
break
}
}
}
- if !isModalTrigger {
+ // thanks i hate this
+ switch msg.(type) {
+ case tea.WindowSizeMsg:
+ bypassModal = true
+ case client.EventSessionUpdated:
+ bypassModal = true
+ case client.EventMessageUpdated:
+ bypassModal = true
+ case cursor.BlinkMsg:
+ bypassModal = true
+ case spinner.TickMsg:
+ bypassModal = true
+ }
+
+ if !bypassModal {
updatedModal, cmd := a.modal.Update(msg)
a.modal = updatedModal.(layout.Modal)
return a, cmd
@@ -112,7 +132,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
switch msg := msg.(type) {
-
case commands.ExecuteCommandMsg:
switch msg.Name {
case "quit":
@@ -143,6 +162,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
helpDialog := dialog.NewHelpDialog(helpBindings...)
a.modal = helpDialog
}
+ slog.Info("Execute command", "cmds", cmds)
return a, tea.Batch(cmds...)
case tea.BackgroundColorMsg: