فهرست منبع

wip: refactoring tui

adamdottv 8 ماه پیش
والد
کامیت
67023bb007

+ 71 - 0
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
+}
+

+ 18 - 2
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",
 	}
 }

+ 29 - 0
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"]
+}
+

+ 23 - 15
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)

+ 61 - 53
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))
 	}

+ 2 - 1
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).

+ 36 - 21
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,
 	)
 

+ 4 - 4
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",

+ 4 - 4
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",

+ 2 - 8
packages/tui/internal/components/util/simple-list.go → 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 {

+ 6 - 0
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 {

+ 20 - 8
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),

+ 25 - 5
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: