summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-13 10:47:51 -0500
committeradamdottv <[email protected]>2025-06-13 10:47:51 -0500
commit62b9a30a9c70bd48768055f0049400d27a849c3e (patch)
tree10872b9a5ec6bf98338c5c7641747a3506acb79e /packages
parent5706c6ad3add2ad4eb6c3d152f0fa72b701027c4 (diff)
downloadopencode-62b9a30a9c70bd48768055f0049400d27a849c3e.tar.gz
opencode-62b9a30a9c70bd48768055f0049400d27a849c3e.zip
wip: refactoring tui
Diffstat (limited to 'packages')
-rw-r--r--packages/tui/cmd/opencode/main.go3
-rw-r--r--packages/tui/internal/app/app.go32
-rw-r--r--packages/tui/internal/commands/command.go54
-rw-r--r--packages/tui/internal/components/chat/editor.go25
-rw-r--r--packages/tui/internal/components/chat/messages.go9
-rw-r--r--packages/tui/internal/components/dialog/complete.go36
-rw-r--r--packages/tui/internal/components/dialog/filepicker.go486
-rw-r--r--packages/tui/internal/components/dialog/help.go20
-rw-r--r--packages/tui/internal/components/dialog/init.go5
-rw-r--r--packages/tui/internal/components/dialog/permission.go5
-rw-r--r--packages/tui/internal/components/dialog/session.go6
-rw-r--r--packages/tui/internal/components/dialog/theme.go6
-rw-r--r--packages/tui/internal/components/util/simple-list.go40
-rw-r--r--packages/tui/internal/layout/container.go9
-rw-r--r--packages/tui/internal/layout/flex.go14
-rw-r--r--packages/tui/internal/layout/layout.go4
-rw-r--r--packages/tui/internal/page/chat.go13
-rw-r--r--packages/tui/internal/tui/tui.go60
18 files changed, 111 insertions, 716 deletions
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 06c4554a3..aa52f583c 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -123,9 +123,6 @@ func main() {
// Cancel subscriptions first
cancelSubs()
- // Then shutdown the app
- app_.Shutdown()
-
// Then cancel TUI message handler
tuiCancel()
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index e44c4b846..70f655c62 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -9,6 +9,7 @@ import (
"log/slog"
tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/status"
@@ -26,10 +27,7 @@ type App struct {
Session *client.SessionInfo
Messages []client.MessageInfo
Status status.Service
-
- // UI state
- filepickerOpen bool
- completionDialogOpen bool
+ Commands commands.Registry
}
type AppInfo struct {
@@ -117,6 +115,7 @@ func New(ctx context.Context, version string, httpClient *client.ClientWithRespo
Session: &client.SessionInfo{},
Messages: []client.MessageInfo{},
Status: status.GetService(),
+ Commands: commands.NewCommandRegistry(),
}
theme.SetTheme(appConfig.Theme)
@@ -324,28 +323,3 @@ func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error)
providers := *resp.JSON200
return providers.Providers, nil
}
-
-// IsFilepickerOpen returns whether the filepicker is currently open
-func (app *App) IsFilepickerOpen() bool {
- return app.filepickerOpen
-}
-
-// SetFilepickerOpen sets the state of the filepicker
-func (app *App) SetFilepickerOpen(open bool) {
- app.filepickerOpen = open
-}
-
-// IsCompletionDialogOpen returns whether the completion dialog is currently open
-func (app *App) IsCompletionDialogOpen() bool {
- return app.completionDialogOpen
-}
-
-// SetCompletionDialogOpen sets the state of the completion dialog
-func (app *App) SetCompletionDialogOpen(open bool) {
- app.completionDialogOpen = open
-}
-
-// Shutdown performs a clean shutdown of the application
-func (app *App) Shutdown() {
- // TODO: cleanup?
-}
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index c2b8f2eae..e71e2d299 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -15,11 +15,57 @@ type Command struct {
}
// Registry holds all the available commands.
-type Registry struct {
- Commands map[string]Command
-}
+type Registry map[string]Command
// ExecuteCommandMsg is a message sent when a command should be executed.
type ExecuteCommandMsg struct {
Name string
-} \ No newline at end of file
+}
+
+func NewCommandRegistry() Registry {
+ return Registry{
+ "help": {
+ Name: "help",
+ Description: "show help",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f1", "super+/", "super+h"),
+ ),
+ },
+ "new": {
+ Name: "new",
+ Description: "new session",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f2", "super+n"),
+ ),
+ },
+ "sessions": {
+ Name: "sessions",
+ Description: "switch session",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f3", "super+s"),
+ ),
+ },
+ "model": {
+ Name: "model",
+ Description: "switch model",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f4", "super+m"),
+ ),
+ },
+ "theme": {
+ Name: "theme",
+ Description: "switch theme",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f5", "super+t"),
+ ),
+ },
+ "quit": {
+ Name: "quit",
+ Description: "quit",
+ KeyBinding: key.NewBinding(
+ key.WithKeys("f10", "ctrl+c", "super+q"),
+ ),
+ },
+ }
+}
+
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 7ba961577..e9414e49a 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -112,12 +112,6 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
m.textarea.SetValue(modifiedValue)
return m, nil
- case dialog.AttachmentAddedMsg:
- if len(m.attachments) >= maxAttachments {
- status.Error(fmt.Sprintf("cannot add more than %d images", maxAttachments))
- return m, cmd
- }
- m.attachments = append(m.attachments, msg.Attachment)
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
@@ -190,7 +184,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.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) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
+ 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()
@@ -210,7 +206,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
- if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
+ 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()
@@ -317,13 +315,6 @@ func (m *editorComponent) GetSize() (int, int) {
return m.width, m.height
}
-func (m *editorComponent) BindingKeys() []key.Binding {
- bindings := []key.Binding{}
- bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
- bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
- return bindings
-}
-
func (m *editorComponent) openEditor(value string) tea.Cmd {
editor := os.Getenv("EDITOR")
if editor == "" {
@@ -387,7 +378,9 @@ func (m *editorComponent) send() tea.Cmd {
// Check for slash command
if strings.HasPrefix(value, "/") {
commandName := strings.TrimPrefix(value, "/")
- return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
+ if _, ok := m.app.Commands[commandName]; ok {
+ return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
+ }
}
return tea.Batch(
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 4beaea2d8..a5432e913 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -384,15 +384,6 @@ func (m *messagesComponent) Reload() tea.Cmd {
}
}
-func (m *messagesComponent) BindingKeys() []key.Binding {
- return []key.Binding{
- m.viewport.KeyMap.PageDown,
- m.viewport.KeyMap.PageUp,
- m.viewport.KeyMap.HalfPageUp,
- m.viewport.KeyMap.HalfPageDown,
- }
-}
-
func NewMessagesComponent(app *app.App) layout.ModelWithView {
customSpinner := spinner.Spinner{
Frames: []string{" ", "┃", "┃"},
diff --git a/packages/tui/internal/components/dialog/complete.go b/packages/tui/internal/components/dialog/complete.go
index a399b503b..c797a00df 100644
--- a/packages/tui/internal/components/dialog/complete.go
+++ b/packages/tui/internal/components/dialog/complete.go
@@ -20,7 +20,7 @@ type CompletionItem struct {
}
type CompletionItemI interface {
- utilComponents.SimpleListItem
+ utilComponents.ListItem
GetValue() string
DisplayValue() string
}
@@ -78,8 +78,8 @@ type CompletionDialogCloseMsg struct{}
type CompletionDialog interface {
layout.ModelWithView
- layout.Bindings
SetWidth(width int)
+ IsEmpty() bool
}
type completionDialogComponent struct {
@@ -88,7 +88,7 @@ type completionDialogComponent struct {
width int
height int
pseudoSearchTextArea textarea.Model
- listView utilComponents.SimpleList[CompletionItemI]
+ list utilComponents.List[CompletionItemI]
}
type completionDialogKeyMap struct {
@@ -126,7 +126,7 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
}
func (c *completionDialogComponent) close() tea.Cmd {
- c.listView.SetItems([]CompletionItemI{})
+ c.list.SetItems([]CompletionItemI{})
c.pseudoSearchTextArea.Reset()
c.pseudoSearchTextArea.Blur()
@@ -138,9 +138,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() {
-
if !key.Matches(msg, completionDialogKeys.Complete) {
-
var cmd tea.Cmd
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
cmds = append(cmds, cmd)
@@ -157,25 +155,23 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
status.Error(err.Error())
}
- c.listView.SetItems(items)
+ c.list.SetItems(items)
c.query = query
}
- u, cmd := c.listView.Update(msg)
- c.listView = u.(utilComponents.SimpleList[CompletionItemI])
+ u, cmd := c.list.Update(msg)
+ c.list = u.(utilComponents.List[CompletionItemI])
cmds = append(cmds, cmd)
}
switch {
case key.Matches(msg, completionDialogKeys.Complete):
- item, i := c.listView.GetSelectedItem()
+ item, i := c.list.GetSelectedItem()
if i == -1 {
return c, nil
}
-
cmd := c.complete(item)
-
return c, cmd
case key.Matches(msg, completionDialogKeys.Cancel):
// Only close on backspace when there are no characters left
@@ -191,7 +187,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
status.Error(err.Error())
}
- c.listView.SetItems(items)
+ c.list.SetItems(items)
c.pseudoSearchTextArea.SetValue(msg.String())
return c, c.pseudoSearchTextArea.Focus()
}
@@ -209,7 +205,7 @@ func (c *completionDialogComponent) View() string {
maxWidth := 40
- completions := c.listView.GetItems()
+ completions := c.list.GetItems()
for _, cmd := range completions {
title := cmd.DisplayValue()
@@ -218,7 +214,7 @@ func (c *completionDialogComponent) View() string {
}
}
- c.listView.SetMaxWidth(maxWidth)
+ c.list.SetMaxWidth(maxWidth)
return baseStyle.Padding(0, 0).
Border(lipgloss.NormalBorder()).
@@ -228,15 +224,15 @@ func (c *completionDialogComponent) View() string {
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(c.width).
- Render(c.listView.View())
+ Render(c.list.View())
}
func (c *completionDialogComponent) SetWidth(width int) {
c.width = width
}
-func (c *completionDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(completionDialogKeys)
+func (c *completionDialogComponent) IsEmpty() bool {
+ return c.list.IsEmpty()
}
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
@@ -247,7 +243,7 @@ func NewCompletionDialogComponent(completionProvider CompletionProvider) Complet
status.Error(err.Error())
}
- li := utilComponents.NewSimpleList(
+ li := utilComponents.NewListComponent(
items,
7,
"No file matches found",
@@ -258,6 +254,6 @@ func NewCompletionDialogComponent(completionProvider CompletionProvider) Complet
query: "",
completionProvider: completionProvider,
pseudoSearchTextArea: ti,
- listView: li,
+ list: li,
}
}
diff --git a/packages/tui/internal/components/dialog/filepicker.go b/packages/tui/internal/components/dialog/filepicker.go
deleted file mode 100644
index 408b9cc00..000000000
--- a/packages/tui/internal/components/dialog/filepicker.go
+++ /dev/null
@@ -1,486 +0,0 @@
-package dialog
-
-import (
- "fmt"
- "net/http"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
-
- "log/slog"
-
- "github.com/atotto/clipboard"
- "github.com/charmbracelet/bubbles/v2/key"
- "github.com/charmbracelet/bubbles/v2/textinput"
- "github.com/charmbracelet/bubbles/v2/viewport"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/sst/opencode/internal/app"
- "github.com/sst/opencode/internal/image"
- "github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/status"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
-)
-
-const (
- maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
- downArrow = "down"
- upArrow = "up"
-)
-
-type FilePrickerKeyMap struct {
- Enter key.Binding
- Down key.Binding
- Up key.Binding
- Forward key.Binding
- Backward key.Binding
- OpenFilePicker key.Binding
- Esc key.Binding
- InsertCWD key.Binding
- Paste key.Binding
-}
-
-var filePickerKeyMap = FilePrickerKeyMap{
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select file/enter directory"),
- ),
- Down: key.NewBinding(
- key.WithKeys("j", downArrow),
- key.WithHelp("↓/j", "down"),
- ),
- Up: key.NewBinding(
- key.WithKeys("k", upArrow),
- key.WithHelp("↑/k", "up"),
- ),
- Forward: key.NewBinding(
- key.WithKeys("l"),
- key.WithHelp("l", "enter directory"),
- ),
- Backward: key.NewBinding(
- key.WithKeys("h", "backspace"),
- key.WithHelp("h/backspace", "go back"),
- ),
- OpenFilePicker: key.NewBinding(
- key.WithKeys("ctrl+f"),
- key.WithHelp("ctrl+f", "open file picker"),
- ),
- Esc: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close/exit"),
- ),
- InsertCWD: key.NewBinding(
- key.WithKeys("i"),
- key.WithHelp("i", "manual path input"),
- ),
- Paste: key.NewBinding(
- key.WithKeys("ctrl+v"),
- key.WithHelp("ctrl+v", "paste file/directory path"),
- ),
-}
-
-type filepickerComponent struct {
- basePath string
- width int
- height int
- cursor int
- err error
- cursorChain stack
- viewport viewport.Model
- dirs []os.DirEntry
- cwdDetails *DirNode
- selectedFile string
- cwd textinput.Model
- ShowFilePicker bool
- app *app.App
-}
-
-type DirNode struct {
- parent *DirNode
- child *DirNode
- directory string
-}
-type stack []int
-
-func (s stack) Push(v int) stack {
- return append(s, v)
-}
-
-func (s stack) Pop() (stack, int) {
- l := len(s)
- return s[:l-1], s[l-1]
-}
-
-type AttachmentAddedMsg struct {
- Attachment app.Attachment
-}
-
-func (f *filepickerComponent) Init() tea.Cmd {
- return nil
-}
-
-func (f *filepickerComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- f.width = 60
- f.height = 20
- f.viewport.SetWidth(80)
- f.viewport.SetHeight(22)
- f.cursor = 0
- f.getCurrentFileBelowCursor()
- case tea.KeyMsg:
- if f.cwd.Focused() {
- f.cwd, cmd = f.cwd.Update(msg)
- }
- switch {
- case key.Matches(msg, filePickerKeyMap.InsertCWD):
- f.cwd.Focus()
- return f, cmd
- case key.Matches(msg, filePickerKeyMap.Esc):
- if f.cwd.Focused() {
- f.cwd.Blur()
- }
- case key.Matches(msg, filePickerKeyMap.Down):
- if !f.cwd.Focused() || msg.String() == downArrow {
- if f.cursor < len(f.dirs)-1 {
- f.cursor++
- f.getCurrentFileBelowCursor()
- }
- }
- case key.Matches(msg, filePickerKeyMap.Up):
- if !f.cwd.Focused() || msg.String() == upArrow {
- if f.cursor > 0 {
- f.cursor--
- f.getCurrentFileBelowCursor()
- }
- }
- case key.Matches(msg, filePickerKeyMap.Enter):
- var path string
- var isPathDir bool
- if f.cwd.Focused() {
- path = f.cwd.Value()
- fileInfo, err := os.Stat(path)
- if err != nil {
- status.Error("Invalid path")
- return f, cmd
- }
- isPathDir = fileInfo.IsDir()
- } else {
- path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
- isPathDir = f.dirs[f.cursor].IsDir()
- }
- if isPathDir {
- newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
- f.cwdDetails.child = &newWorkingDir
- f.cwdDetails = f.cwdDetails.child
- f.cursorChain = f.cursorChain.Push(f.cursor)
- f.dirs = readDir(f.cwdDetails.directory, false)
- f.cursor = 0
- f.cwd.SetValue(f.cwdDetails.directory)
- f.getCurrentFileBelowCursor()
- } else {
- f.selectedFile = path
- return f.addAttachmentToMessage()
- }
- case key.Matches(msg, filePickerKeyMap.Esc):
- if !f.cwd.Focused() {
- f.cursorChain = make(stack, 0)
- f.cursor = 0
- } else {
- f.cwd.Blur()
- }
- case key.Matches(msg, filePickerKeyMap.Forward):
- if !f.cwd.Focused() {
- if f.dirs[f.cursor].IsDir() {
- path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
- newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
- f.cwdDetails.child = &newWorkingDir
- f.cwdDetails = f.cwdDetails.child
- f.cursorChain = f.cursorChain.Push(f.cursor)
- f.dirs = readDir(f.cwdDetails.directory, false)
- f.cursor = 0
- f.cwd.SetValue(f.cwdDetails.directory)
- f.getCurrentFileBelowCursor()
- }
- }
- case key.Matches(msg, filePickerKeyMap.Backward):
- if !f.cwd.Focused() {
- if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
- f.cursorChain, f.cursor = f.cursorChain.Pop()
- f.cwdDetails = f.cwdDetails.parent
- f.cwdDetails.child = nil
- f.dirs = readDir(f.cwdDetails.directory, false)
- f.cwd.SetValue(f.cwdDetails.directory)
- f.getCurrentFileBelowCursor()
- }
- }
- case key.Matches(msg, filePickerKeyMap.Paste):
- if f.cwd.Focused() {
- val, err := clipboard.ReadAll()
- if err != nil {
- slog.Error("failed to read clipboard")
- return f, cmd
- }
- f.cwd.SetValue(f.cwd.Value() + val)
- }
- case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
- f.dirs = readDir(f.cwdDetails.directory, false)
- f.cursor = 0
- f.getCurrentFileBelowCursor()
- }
- }
- return f, cmd
-}
-
-func (f *filepickerComponent) addAttachmentToMessage() (tea.Model, tea.Cmd) {
- // modeInfo := GetSelectedModel(config.Get())
- // if !modeInfo.SupportsAttachments {
- // status.Error(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
- // return f, nil
- // }
-
- selectedFilePath := f.selectedFile
- if !isExtSupported(selectedFilePath) {
- status.Error("Unsupported file")
- return f, nil
- }
-
- isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
- if err != nil {
- status.Error("unable to read the image")
- return f, nil
- }
- if isFileLarge {
- status.Error("file too large, max 5MB")
- return f, nil
- }
-
- content, err := os.ReadFile(selectedFilePath)
- if err != nil {
- status.Error("Unable read selected file")
- return f, nil
- }
-
- mimeBufferSize := min(512, len(content))
- mimeType := http.DetectContentType(content[:mimeBufferSize])
- fileName := filepath.Base(selectedFilePath)
- attachment := app.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
- f.selectedFile = ""
- return f, util.CmdHandler(AttachmentAddedMsg{attachment})
-}
-
-func (f *filepickerComponent) View() string {
- t := theme.CurrentTheme()
- const maxVisibleDirs = 20
- const maxWidth = 80
-
- adjustedWidth := maxWidth
- for _, file := range f.dirs {
- if len(file.Name()) > adjustedWidth-4 { // Account for padding
- adjustedWidth = len(file.Name()) + 4
- }
- }
- adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
-
- files := make([]string, 0, maxVisibleDirs)
- startIdx := 0
-
- if len(f.dirs) > maxVisibleDirs {
- halfVisible := maxVisibleDirs / 2
- if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
- startIdx = f.cursor - halfVisible
- } else if f.cursor >= len(f.dirs)-halfVisible {
- startIdx = len(f.dirs) - maxVisibleDirs
- }
- }
-
- endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
-
- for i := startIdx; i < endIdx; i++ {
- file := f.dirs[i]
- itemStyle := styles.BaseStyle().Width(adjustedWidth)
-
- if i == f.cursor {
- itemStyle = itemStyle.
- Background(t.Primary()).
- Foreground(t.Background()).
- Bold(true)
- }
- filename := file.Name()
-
- if len(filename) > adjustedWidth-4 {
- filename = filename[:adjustedWidth-7] + "..."
- }
- if file.IsDir() {
- filename = filename + "/"
- }
-
- files = append(files, itemStyle.Padding(0, 1).Render(filename))
- }
-
- // Pad to always show exactly 21 lines
- for len(files) < maxVisibleDirs {
- files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
- }
-
- currentPath := styles.BaseStyle().
- Height(1).
- Width(adjustedWidth).
- Render(f.cwd.View())
-
- viewportstyle := lipgloss.NewStyle().
- Width(f.viewport.Width()).
- Background(t.Background()).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(t.TextMuted()).
- BorderBackground(t.Background()).
- Padding(2).
- Render(f.viewport.View())
- var insertExitText string
- if f.IsCWDFocused() {
- insertExitText = "Press esc to exit typing path"
- } else {
- insertExitText = "Press i to start typing path"
- }
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- currentPath,
- styles.BaseStyle().Width(adjustedWidth).Render(""),
- styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
- styles.BaseStyle().Width(adjustedWidth).Render(""),
- styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
- )
-
- f.cwd.SetValue(f.cwd.Value())
- contentStyle := styles.BaseStyle().Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4)
-
- return lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle)
-}
-
-type FilepickerComponent interface {
- layout.ModelWithView
- ToggleFilepicker(showFilepicker bool)
- IsCWDFocused() bool
-}
-
-func (f *filepickerComponent) ToggleFilepicker(showFilepicker bool) {
- f.ShowFilePicker = showFilepicker
-}
-
-func (f *filepickerComponent) IsCWDFocused() bool {
- return f.cwd.Focused()
-}
-
-func NewFilepickerCmp(app *app.App) FilepickerComponent {
- homepath, err := os.UserHomeDir()
- if err != nil {
- slog.Error("error loading user files")
- return nil
- }
- baseDir := DirNode{parent: nil, directory: homepath}
- dirs := readDir(homepath, false)
- viewport := viewport.New() // viewport.New(0, 0)
- currentDirectory := textinput.New()
- currentDirectory.CharLimit = 200
- currentDirectory.SetWidth(44)
- // currentDirectory.Cursor.Blink = true
- currentDirectory.SetValue(baseDir.directory)
- return &filepickerComponent{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
-}
-
-func (f *filepickerComponent) getCurrentFileBelowCursor() {
- if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
- slog.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
- f.viewport.SetContent("Preview unavailable")
- return
- }
-
- dir := f.dirs[f.cursor]
- filename := dir.Name()
- if !dir.IsDir() && isExtSupported(filename) {
- fullPath := f.cwdDetails.directory + "/" + dir.Name()
-
- go func() {
- imageString, err := image.ImagePreview(f.viewport.Width()-4, fullPath)
- if err != nil {
- slog.Error(err.Error())
- f.viewport.SetContent("Preview unavailable")
- return
- }
-
- f.viewport.SetContent(imageString)
- }()
- } else {
- f.viewport.SetContent("Preview unavailable")
- }
-}
-
-func readDir(path string, showHidden bool) []os.DirEntry {
- slog.Info(fmt.Sprintf("Reading directory: %s", path))
-
- entriesChan := make(chan []os.DirEntry, 1)
- errChan := make(chan error, 1)
-
- go func() {
- dirEntries, err := os.ReadDir(path)
- if err != nil {
- status.Error(err.Error())
- errChan <- err
- return
- }
- entriesChan <- dirEntries
- }()
-
- select {
- case dirEntries := <-entriesChan:
- sort.Slice(dirEntries, func(i, j int) bool {
- if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
- return dirEntries[i].Name() < dirEntries[j].Name()
- }
- return dirEntries[i].IsDir()
- })
-
- if showHidden {
- return dirEntries
- }
-
- var sanitizedDirEntries []os.DirEntry
- for _, dirEntry := range dirEntries {
- isHidden, _ := IsHidden(dirEntry.Name())
- if !isHidden {
- if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
- sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
- }
- }
- }
-
- return sanitizedDirEntries
-
- case <-errChan:
- status.Error(fmt.Sprintf("Error reading directory %s", path))
- return []os.DirEntry{}
-
- case <-time.After(5 * time.Second):
- status.Error(fmt.Sprintf("Timeout reading directory %s", path))
- return []os.DirEntry{}
- }
-}
-
-func IsHidden(file string) (bool, error) {
- return strings.HasPrefix(file, "."), nil
-}
-
-func isExtSupported(path string) bool {
- ext := strings.ToLower(filepath.Ext(path))
- return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
-}
diff --git a/packages/tui/internal/components/dialog/help.go b/packages/tui/internal/components/dialog/help.go
index a1508a01a..45972e034 100644
--- a/packages/tui/internal/components/dialog/help.go
+++ b/packages/tui/internal/components/dialog/help.go
@@ -50,26 +50,6 @@ func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return h, nil
}
-// func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
-// seen := make(map[string]struct{})
-// result := make([]key.Binding, 0, len(bindings))
-//
-// // Process bindings in reverse order
-// for i := len(bindings) - 1; i >= 0; i-- {
-// b := bindings[i]
-// k := strings.Join(b.Keys(), " ")
-// if _, ok := seen[k]; ok {
-// // duplicate, skip
-// continue
-// }
-// seen[k] = struct{}{}
-// // Add to the beginning of result to maintain original order
-// result = append([]key.Binding{b}, result...)
-// }
-//
-// return result
-// }
-
func (h *helpDialog) View() string {
t := theme.CurrentTheme()
keyStyle := lipgloss.NewStyle().
diff --git a/packages/tui/internal/components/dialog/init.go b/packages/tui/internal/components/dialog/init.go
index d939427c1..339e31ca4 100644
--- a/packages/tui/internal/components/dialog/init.go
+++ b/packages/tui/internal/components/dialog/init.go
@@ -173,11 +173,6 @@ func (m *InitDialogCmp) SetSize(width, height int) {
m.height = height
}
-// Bindings implements layout.Bindings.
-func (m InitDialogCmp) Bindings() []key.Binding {
- return m.keys.ShortHelp()
-}
-
// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
type CloseInitDialogMsg struct {
Initialize bool
diff --git a/packages/tui/internal/components/dialog/permission.go b/packages/tui/internal/components/dialog/permission.go
index 12df79884..ba82c876b 100644
--- a/packages/tui/internal/components/dialog/permission.go
+++ b/packages/tui/internal/components/dialog/permission.go
@@ -31,7 +31,6 @@ type PermissionResponseMsg struct {
// PermissionDialogComponent interface for permission dialog component
type PermissionDialogComponent interface {
layout.ModelWithView
- layout.Bindings
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
}
@@ -424,10 +423,6 @@ func (p *permissionDialogComponent) View() string {
return p.render()
}
-func (p *permissionDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(permissionsKeys)
-}
-
func (p *permissionDialogComponent) SetSize() tea.Cmd {
// if p.permission.ID == "" {
// return nil
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go
index 46e015950..4a41c71f3 100644
--- a/packages/tui/internal/components/dialog/session.go
+++ b/packages/tui/internal/components/dialog/session.go
@@ -48,7 +48,7 @@ type sessionDialog struct {
height int
modal *modal.Modal
selectedSessionID string
- list components.SimpleList[sessionItem]
+ list components.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.SimpleList[sessionItem])
+ s.list = listModel.(components.List[sessionItem])
return s, cmd
}
@@ -98,7 +98,7 @@ func NewSessionDialog(app *app.App) SessionDialog {
sessionItems = append(sessionItems, sessionItem{session: sess})
}
- list := components.NewSimpleList(
+ list := components.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 332d05824..2febc1fe0 100644
--- a/packages/tui/internal/components/dialog/theme.go
+++ b/packages/tui/internal/components/dialog/theme.go
@@ -49,7 +49,7 @@ type themeDialog struct {
height int
modal *modal.Modal
- list components.SimpleList[themeItem]
+ list components.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.SimpleList[themeItem])
+ t.list = listModel.(components.List[themeItem])
return t, cmd
}
@@ -110,7 +110,7 @@ func NewThemeDialog() ThemeDialog {
}
}
- list := components.NewSimpleList(
+ list := components.NewListComponent(
themeItems,
10, // maxVisibleThemes
"No themes available",
diff --git a/packages/tui/internal/components/util/simple-list.go b/packages/tui/internal/components/util/simple-list.go
index 68cda3f5b..6df4e313e 100644
--- a/packages/tui/internal/components/util/simple-list.go
+++ b/packages/tui/internal/components/util/simple-list.go
@@ -8,21 +8,21 @@ import (
"github.com/sst/opencode/internal/styles"
)
-type SimpleListItem interface {
+type ListItem interface {
Render(selected bool, width int) string
}
-type SimpleList[T SimpleListItem] interface {
+type List[T ListItem] interface {
layout.ModelWithView
- layout.Bindings
SetMaxWidth(maxWidth int)
GetSelectedItem() (item T, idx int)
SetItems(items []T)
GetItems() []T
SetSelectedIndex(idx int)
+ IsEmpty() bool
}
-type simpleListComponent[T SimpleListItem] struct {
+type listComponent[T ListItem] struct {
fallbackMsg string
items []T
selectedIdx int
@@ -33,14 +33,14 @@ type simpleListComponent[T SimpleListItem] struct {
height int
}
-type simpleListKeyMap struct {
+type listKeyMap struct {
Up key.Binding
Down key.Binding
UpAlpha key.Binding
DownAlpha key.Binding
}
-var simpleListKeys = simpleListKeyMap{
+var simpleListKeys = listKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous list item"),
@@ -59,11 +59,11 @@ var simpleListKeys = simpleListKeyMap{
),
}
-func (c *simpleListComponent[T]) Init() tea.Cmd {
+func (c *listComponent[T]) Init() tea.Cmd {
return nil
}
-func (c *simpleListComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
@@ -83,11 +83,7 @@ func (c *simpleListComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, nil
}
-func (c *simpleListComponent[T]) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(simpleListKeys)
-}
-
-func (c *simpleListComponent[T]) GetSelectedItem() (T, int) {
+func (c *listComponent[T]) GetSelectedItem() (T, int) {
if len(c.items) > 0 {
return c.items[c.selectedIdx], c.selectedIdx
}
@@ -96,26 +92,30 @@ func (c *simpleListComponent[T]) GetSelectedItem() (T, int) {
return zero, -1
}
-func (c *simpleListComponent[T]) SetItems(items []T) {
+func (c *listComponent[T]) SetItems(items []T) {
c.selectedIdx = 0
c.items = items
}
-func (c *simpleListComponent[T]) GetItems() []T {
+func (c *listComponent[T]) GetItems() []T {
return c.items
}
-func (c *simpleListComponent[T]) SetMaxWidth(width int) {
+func (c *listComponent[T]) IsEmpty() bool {
+ return len(c.items) == 0
+}
+
+func (c *listComponent[T]) SetMaxWidth(width int) {
c.maxWidth = width
}
-func (c *simpleListComponent[T]) SetSelectedIndex(idx int) {
+func (c *listComponent[T]) SetSelectedIndex(idx int) {
if idx >= 0 && idx < len(c.items) {
c.selectedIdx = idx
}
}
-func (c *simpleListComponent[T]) View() string {
+func (c *listComponent[T]) View() string {
baseStyle := styles.BaseStyle()
items := c.items
@@ -152,8 +152,8 @@ func (c *simpleListComponent[T]) View() string {
return lipgloss.JoinVertical(lipgloss.Left, listItems...)
}
-func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
- return &simpleListComponent[T]{
+func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
+ return &listComponent[T]{
fallbackMsg: fallbackMsg,
items: items,
maxVisibleItems: maxVisibleItems,
diff --git a/packages/tui/internal/layout/container.go b/packages/tui/internal/layout/container.go
index 2bed6708f..3fa844afc 100644
--- a/packages/tui/internal/layout/container.go
+++ b/packages/tui/internal/layout/container.go
@@ -1,7 +1,6 @@
package layout
import (
- "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/theme"
@@ -15,7 +14,6 @@ type ModelWithView interface {
type Container interface {
ModelWithView
Sizeable
- Bindings
Focus()
Blur()
MaxWidth() int
@@ -153,13 +151,6 @@ func (c *container) Alignment() lipgloss.Position {
return c.align
}
-func (c *container) BindingKeys() []key.Binding {
- if b, ok := c.content.(Bindings); ok {
- return b.BindingKeys()
- }
- return []key.Binding{}
-}
-
// Focus sets the container as focused
func (c *container) Focus() {
c.focused = true
diff --git a/packages/tui/internal/layout/flex.go b/packages/tui/internal/layout/flex.go
index 292cfcb0f..dd04968d4 100644
--- a/packages/tui/internal/layout/flex.go
+++ b/packages/tui/internal/layout/flex.go
@@ -1,7 +1,6 @@
package layout
import (
- "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
)
@@ -27,7 +26,6 @@ func FlexPaneSizeFixed(size int) FlexPaneSize {
type FlexLayout interface {
ModelWithView
Sizeable
- Bindings
SetPanes(panes []Container) tea.Cmd
SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
SetDirection(direction FlexDirection) tea.Cmd
@@ -199,18 +197,6 @@ func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
return nil
}
-func (f *flexLayout) BindingKeys() []key.Binding {
- keys := []key.Binding{}
- for _, pane := range f.panes {
- if pane != nil {
- if b, ok := pane.(Bindings); ok {
- keys = append(keys, b.BindingKeys()...)
- }
- }
- }
- return keys
-}
-
func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
layout := &flexLayout{
direction: FlexDirectionHorizontal,
diff --git a/packages/tui/internal/layout/layout.go b/packages/tui/internal/layout/layout.go
index b3afc5cc7..f2c16c603 100644
--- a/packages/tui/internal/layout/layout.go
+++ b/packages/tui/internal/layout/layout.go
@@ -45,10 +45,6 @@ type Sizeable interface {
GetSize() (int, int)
}
-type Bindings interface {
- BindingKeys() []key.Binding
-}
-
func KeyMapToSlice(t any) (bindings []key.Binding) {
typ := reflect.TypeOf(t)
if typ.Kind() != reflect.Struct {
diff --git a/packages/tui/internal/page/chat.go b/packages/tui/internal/page/chat.go
index 3e2e530a0..ee8b9ed00 100644
--- a/packages/tui/internal/page/chat.go
+++ b/packages/tui/internal/page/chat.go
@@ -61,13 +61,13 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := p.layout.SetSize(msg.Width, msg.Height)
cmds = append(cmds, cmd)
case chat.SendMsg:
+ p.showCompletionDialog = false
cmd := p.sendMessage(msg.Text, msg.Attachments)
if cmd != nil {
return p, cmd
}
case dialog.CompletionDialogCloseMsg:
p.showCompletionDialog = false
- p.app.SetCompletionDialogOpen(false)
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
@@ -80,7 +80,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, keyMap.ShowCompletionDialog):
p.showCompletionDialog = true
- p.app.SetCompletionDialogOpen(true)
// Continue sending keys to layout->chat
case key.Matches(msg, keyMap.Cancel):
if p.app.Session.Id != "" {
@@ -93,6 +92,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
}
}
+
if p.showCompletionDialog {
context, contextCmd := p.completionDialog.Update(msg)
p.completionDialog = context.(dialog.CompletionDialog)
@@ -100,7 +100,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Doesn't forward event if enter key is pressed
if keyMsg, ok := msg.(tea.KeyMsg); ok {
- if keyMsg.String() == "enter" {
+ if keyMsg.String() == "enter" && !p.completionDialog.IsEmpty() {
return p, tea.Batch(cmds...)
}
}
@@ -149,13 +149,6 @@ func (p *chatPage) View() string {
return layoutView
}
-func (p *chatPage) BindingKeys() []key.Binding {
- bindings := layout.KeyMapToSlice(keyMap)
- bindings = append(bindings, p.messages.BindingKeys()...)
- bindings = append(bindings, p.editor.BindingKeys()...)
- return bindings
-}
-
func NewChatPage(app *app.App) layout.ModelWithView {
cg := completions.NewFileAndFolderContextGroup()
completionDialog := dialog.NewCompletionDialogComponent(cg)
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 0a1515672..70cbf3b47 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -23,8 +23,6 @@ import (
"github.com/sst/opencode/pkg/client"
)
-
-
type appModel struct {
width, height int
currentPage page.PageID
@@ -34,7 +32,6 @@ type appModel struct {
status core.StatusComponent
app *app.App
modal layout.Modal
- commands *commands.Registry
}
func (a appModel) Init() tea.Cmd {
@@ -99,7 +96,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
- for _, cmdDef := range a.commands.Commands {
+ for _, cmdDef := range a.app.Commands {
if key.Matches(msg, cmdDef.KeyBinding) {
isModalTrigger = true
break
@@ -136,7 +133,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.modal = themeDialog
case "help":
var helpBindings []key.Binding
- for _, cmd := range a.commands.Commands {
+ for _, cmd := range a.app.Commands {
// Create a new binding for help display
helpBindings = append(helpBindings, key.NewBinding(
key.WithKeys(cmd.KeyBinding.Keys()...),
@@ -268,6 +265,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
+ // give the editor a chance to clear input
case "ctrl+c":
updated, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = updated.(layout.ModelWithView)
@@ -278,7 +276,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// First, check for modal triggers from the command registry
if a.modal == nil {
- for _, cmdDef := range a.commands.Commands {
+ for _, cmdDef := range a.app.Commands {
if key.Matches(msg, cmdDef.KeyBinding) {
// If a key matches, send an ExecuteCommandMsg to self.
// This unifies keybinding and slash command handling.
@@ -331,55 +329,6 @@ func (a appModel) View() string {
return appView
}
-func newCommandRegistry() *commands.Registry {
- return &commands.Registry{
- Commands: map[string]commands.Command{
- "help": {
- Name: "help",
- Description: "show help",
- KeyBinding: key.NewBinding(
- key.WithKeys("f1", "super+/", "super+h"),
- ),
- },
- "new": {
- Name: "new",
- Description: "new session",
- KeyBinding: key.NewBinding(
- key.WithKeys("f2", "super+n"),
- ),
- },
- "sessions": {
- Name: "sessions",
- Description: "switch session",
- KeyBinding: key.NewBinding(
- key.WithKeys("f3", "super+s"),
- ),
- },
- "model": {
- Name: "model",
- Description: "switch model",
- KeyBinding: key.NewBinding(
- key.WithKeys("f4", "super+m"),
- ),
- },
- "theme": {
- Name: "theme",
- Description: "switch theme",
- KeyBinding: key.NewBinding(
- key.WithKeys("f5", "super+t"),
- ),
- },
- "quit": {
- Name: "quit",
- Description: "quit",
- KeyBinding: key.NewBinding(
- key.WithKeys("f10", "ctrl+c", "super+q"),
- ),
- },
- },
- }
-}
-
func NewModel(app *app.App) tea.Model {
startPage := page.ChatPage
model := &appModel{
@@ -387,7 +336,6 @@ func NewModel(app *app.App) tea.Model {
loadedPages: make(map[page.PageID]bool),
status: core.NewStatusCmp(app),
app: app,
- commands: newCommandRegistry(),
pages: map[page.PageID]layout.ModelWithView{
page.ChatPage: page.NewChatPage(app),
},