From 2329178f668057516c444975f5b56e45f37de628 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Wed, 28 May 2025 20:59:47 +0200 Subject: [PATCH] wip list sections --- internal/tui/components/core/list/list.go | 251 +++++- .../components/dialogs/commands/arguments.go | 72 ++ .../components/dialogs/commands/commands.go | 18 +- .../tui/components/dialogs/commands/item.go | 59 +- .../tui/components/dialogs/commands/keys.go | 1 + .../tui/components/dialogs/commands/loader.go | 4 +- internal/tui/components/dialogs/dialogs.go | 15 +- internal/tui/components/dialogs/keys.go | 2 +- internal/tui/tui.go | 712 +----------------- 9 files changed, 370 insertions(+), 764 deletions(-) create mode 100644 internal/tui/components/dialogs/commands/keys.go diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index 235e9ee92d50fc071379464f3a2bfb3b437af13d..e3da3bc36f78ab09197907644a3614f338a1e502 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -14,7 +14,6 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/anim" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" - "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" "github.com/sahilm/fuzzy" ) @@ -60,6 +59,13 @@ type HasMatchIndexes interface { MatchIndexes([]int) // Sets the indexes of matched characters in the item's content } +// SectionHeader interface identifies items that are section headers. +// Section headers are rendered differently and are skipped during navigation. +type SectionHeader interface { + util.Model + IsSectionHeader() bool // Returns true if this item is a section header +} + // renderedItem represents a cached rendered item with its position and content. type renderedItem struct { lines []string // The rendered lines of text for this item @@ -539,6 +545,7 @@ func (r *reverseRenderer) renderItemLines(item util.Model) []string { // selectPreviousItem moves selection to the previous item in the list. // Handles focus management and ensures the selected item remains visible. +// Skips section headers during navigation. func (m *model) selectPreviousItem() tea.Cmd { if m.selectionState.selectedIndex <= 0 { return nil @@ -546,6 +553,17 @@ func (m *model) selectPreviousItem() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.selectionState.selectedIndex-- + + // Skip section headers + for m.selectionState.selectedIndex >= 0 && m.isSectionHeader(m.selectionState.selectedIndex) { + m.selectionState.selectedIndex-- + } + + // If we went past the beginning, stay at the first non-header item + if m.selectionState.selectedIndex < 0 { + m.selectionState.selectedIndex = m.findFirstSelectableItem() + } + cmds = append(cmds, m.focusSelected()) m.ensureSelectedItemVisible() return tea.Batch(cmds...) @@ -553,6 +571,7 @@ func (m *model) selectPreviousItem() tea.Cmd { // selectNextItem moves selection to the next item in the list. // Handles focus management and ensures the selected item remains visible. +// Skips section headers during navigation. func (m *model) selectNextItem() tea.Cmd { if m.selectionState.selectedIndex >= len(m.filteredItems)-1 || m.selectionState.selectedIndex < 0 { return nil @@ -560,11 +579,53 @@ func (m *model) selectNextItem() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.selectionState.selectedIndex++ + + // Skip section headers + for m.selectionState.selectedIndex < len(m.filteredItems) && m.isSectionHeader(m.selectionState.selectedIndex) { + m.selectionState.selectedIndex++ + } + + // If we went past the end, stay at the last non-header item + if m.selectionState.selectedIndex >= len(m.filteredItems) { + m.selectionState.selectedIndex = m.findLastSelectableItem() + } + cmds = append(cmds, m.focusSelected()) m.ensureSelectedItemVisible() return tea.Batch(cmds...) } +// isSectionHeader checks if the item at the given index is a section header. +func (m *model) isSectionHeader(index int) bool { + if index < 0 || index >= len(m.filteredItems) { + return false + } + if header, ok := m.filteredItems[index].(SectionHeader); ok { + return header.IsSectionHeader() + } + return false +} + +// findFirstSelectableItem finds the first item that is not a section header. +func (m *model) findFirstSelectableItem() int { + for i := 0; i < len(m.filteredItems); i++ { + if !m.isSectionHeader(i) { + return i + } + } + return NoSelection +} + +// findLastSelectableItem finds the last item that is not a section header. +func (m *model) findLastSelectableItem() int { + for i := len(m.filteredItems) - 1; i >= 0; i-- { + if !m.isSectionHeader(i) { + return i + } + } + return NoSelection +} + // ensureSelectedItemVisible scrolls the list to make the selected item visible. // Uses different strategies for forward and reverse rendering modes. func (m *model) ensureSelectedItemVisible() { @@ -631,25 +692,25 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) { } } -// goToBottom switches to reverse mode and selects the last item. +// goToBottom switches to reverse mode and selects the last selectable item. // Commonly used for chat-like interfaces where new content appears at the bottom. +// Skips section headers when selecting the last item. func (m *model) goToBottom() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.viewState.reverse = true - m.selectionState.selectedIndex = len(m.filteredItems) - 1 + m.selectionState.selectedIndex = m.findLastSelectableItem() cmds = append(cmds, m.focusSelected()) m.ResetView() return tea.Batch(cmds...) } -// goToTop switches to forward mode and selects the first item. +// goToTop switches to forward mode and selects the first selectable item. // Standard behavior for most list interfaces. +// Skips section headers when selecting the first item. func (m *model) goToTop() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.viewState.reverse = false - if len(m.filteredItems) > 0 { - m.selectionState.selectedIndex = 0 - } + m.selectionState.selectedIndex = m.findFirstSelectableItem() cmds = append(cmds, m.focusSelected()) m.ResetView() return tea.Batch(cmds...) @@ -715,8 +776,12 @@ func (m *model) rerenderItem(inx int) { } // getItemLines converts an item to its rendered lines, including any gap spacing. +// Handles section headers with special styling. func (m *model) getItemLines(item util.Model) []string { - itemLines := strings.Split(item.View().String(), "\n") + var itemLines []string + + itemLines = strings.Split(item.View().String(), "\n") + if m.gapSize > 0 { gap := make([]string, m.gapSize) itemLines = append(itemLines, gap...) @@ -995,6 +1060,7 @@ func (m *model) setReverse(reverse bool) { // SetItems replaces all items in the list with a new set. // Initializes all items, sets their sizes, and establishes initial selection. +// Ensures the initial selection skips section headers. func (m *model) SetItems(items []util.Model) tea.Cmd { m.allItems = items m.filteredItems = items @@ -1006,9 +1072,9 @@ func (m *model) SetItems(items []util.Model) tea.Cmd { if len(m.filteredItems) > 0 { if m.viewState.reverse { - m.selectionState.selectedIndex = len(m.filteredItems) - 1 + m.selectionState.selectedIndex = m.findLastSelectableItem() } else { - m.selectionState.selectedIndex = 0 + m.selectionState.selectedIndex = m.findFirstSelectableItem() } if cmd := m.focusSelected(); cmd != nil { cmds = append(cmds, cmd) @@ -1022,18 +1088,75 @@ func (m *model) SetItems(items []util.Model) tea.Cmd { } func (c *model) inputStyle() lipgloss.Style { - t := theme.CurrentTheme() - return styles.BaseStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(t.TextMuted()). - BorderBackground(t.Background()). - BorderBottom(true) + return styles.BaseStyle() +} + +// section represents a group of items under a section header. +type section struct { + header SectionHeader + items []util.Model +} + +// parseSections parses the flat item list into sections. +func (m *model) parseSections() []section { + var sections []section + var currentSection *section + + for _, item := range m.allItems { + if header, ok := item.(SectionHeader); ok && header.IsSectionHeader() { + // Start a new section + if currentSection != nil { + sections = append(sections, *currentSection) + } + currentSection = §ion{ + header: header, + items: []util.Model{}, + } + } else if currentSection != nil { + // Add item to current section + currentSection.items = append(currentSection.items, item) + } else { + // Item without a section header - create an implicit section + if len(sections) == 0 || sections[len(sections)-1].header != nil { + sections = append(sections, section{ + header: nil, + items: []util.Model{item}, + }) + } else { + // Add to the last implicit section + sections[len(sections)-1].items = append(sections[len(sections)-1].items, item) + } + } + } + + // Don't forget the last section + if currentSection != nil { + sections = append(sections, *currentSection) + } + + return sections +} + +// flattenSections converts sections back to a flat list. +func (m *model) flattenSections(sections []section) []util.Model { + var result []util.Model + + for _, sect := range sections { + if sect.header != nil { + result = append(result, sect.header) + } + result = append(result, sect.items...) + } + + return result } func (m *model) filter(search string) tea.Cmd { var cmds []tea.Cmd search = strings.TrimSpace(search) search = strings.ToLower(search) + + // Clear focus and match indexes from all items for _, item := range m.allItems { if i, ok := item.(layout.Focusable); ok { cmds = append(cmds, i.Blur()) @@ -1042,34 +1165,32 @@ func (m *model) filter(search string) tea.Cmd { i.MatchIndexes(make([]int, 0)) } } + if search == "" { - cmds = append(cmds, m.SetItems(m.allItems)) // Reset to all items if search is empty + cmds = append(cmds, m.SetItems(m.allItems)) return tea.Batch(cmds...) } - words := make([]string, 0, len(m.allItems)) - for _, cmd := range m.allItems { - if f, ok := cmd.(HasFilterValue); ok { - words = append(words, strings.ToLower(f.FilterValue())) - } else { - words = append(words, strings.ToLower("")) - } - } - matches := fuzzy.Find(search, words) - sort.Sort(matches) - filteredItems := make([]util.Model, 0, len(matches)) - for _, match := range matches { - item := m.allItems[match.Index] - if i, ok := item.(HasMatchIndexes); ok { - i.MatchIndexes(match.MatchedIndexes) + + // Parse items into sections + sections := m.parseSections() + var filteredSections []section + + for _, sect := range sections { + filteredSection := m.filterSection(sect, search) + if filteredSection != nil { + filteredSections = append(filteredSections, *filteredSection) } - filteredItems = append(filteredItems, item) } - m.filteredItems = filteredItems - if len(filteredItems) > 0 { + + // Rebuild flat list from filtered sections + m.filteredItems = m.flattenSections(filteredSections) + + // Set initial selection + if len(m.filteredItems) > 0 { if m.viewState.reverse { - m.selectionState.selectedIndex = len(filteredItems) - 1 + m.selectionState.selectedIndex = m.findLastSelectableItem() } else { - m.selectionState.selectedIndex = 0 + m.selectionState.selectedIndex = m.findFirstSelectableItem() } if cmd := m.focusSelected(); cmd != nil { cmds = append(cmds, cmd) @@ -1081,3 +1202,59 @@ func (m *model) filter(search string) tea.Cmd { m.ResetView() return tea.Batch(cmds...) } + +// filterSection filters items within a section and returns the section if it has matches. +func (m *model) filterSection(sect section, search string) *section { + var matchedItems []util.Model + var hasHeaderMatch bool + + // Check if section header itself matches + if sect.header != nil { + headerText := strings.ToLower(sect.header.View().String()) + if strings.Contains(headerText, search) { + hasHeaderMatch = true + // If header matches, include all items in the section + matchedItems = sect.items + } + } + + // If header didn't match, filter items within the section + if !hasHeaderMatch && len(sect.items) > 0 { + // Create words array for items in this section + words := make([]string, len(sect.items)) + for i, item := range sect.items { + if f, ok := item.(HasFilterValue); ok { + words[i] = strings.ToLower(f.FilterValue()) + } else { + words[i] = "" + } + } + + // Find matches within this section + matches := fuzzy.Find(search, words) + + // Sort matches by score but preserve relative order for equal scores + sort.SliceStable(matches, func(i, j int) bool { + return matches[i].Score > matches[j].Score + }) + + // Build matched items list + for _, match := range matches { + item := sect.items[match.Index] + if i, ok := item.(HasMatchIndexes); ok { + i.MatchIndexes(match.MatchedIndexes) + } + matchedItems = append(matchedItems, item) + } + } + + // Return section only if it has matches + if len(matchedItems) > 0 { + return §ion{ + header: sect.header, + items: matchedItems, + } + } + + return nil +} diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 02ecf747c56aa93c8b65763d2931be2030e5975b..69e6a48dcc3b657d1587c62c1be5d3ce180c48c1 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -1,5 +1,17 @@ package commands +import ( + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/opencode-ai/opencode/internal/tui/components/dialogs" + "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" +) + +const ( + argumentsDialogID dialogs.DialogID = "arguments" +) + // ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog. type ShowArgumentsDialogMsg struct { CommandID string @@ -14,3 +26,63 @@ type CloseArgumentsDialogMsg struct { Content string Args map[string]string } + +// CommandArgumentsDialog represents the commands dialog. +type CommandArgumentsDialog interface { + dialogs.DialogModel +} + +type commandArgumentsDialogCmp struct { + width int + wWidth int // Width of the terminal window + wHeight int // Height of the terminal window +} + +func NewCommandArgumentsDialog() CommandArgumentsDialog { + return &commandArgumentsDialogCmp{} +} + +// Init implements CommandArgumentsDialog. +func (c *commandArgumentsDialogCmp) Init() tea.Cmd { + return nil +} + +// Update implements CommandArgumentsDialog. +func (c *commandArgumentsDialogCmp) Update(tea.Msg) (tea.Model, tea.Cmd) { + return c, nil +} + +// View implements CommandArgumentsDialog. +func (c *commandArgumentsDialogCmp) View() tea.View { + return tea.NewView("") +} + +func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor { + offset := 10 + 1 + cursor.Y += offset + _, col := c.Position() + cursor.X = cursor.X + col + 2 + return cursor +} + +func (c *commandArgumentsDialogCmp) style() lipgloss.Style { + t := theme.CurrentTheme() + return styles.BaseStyle(). + Width(c.width). + Padding(1). + Border(lipgloss.RoundedBorder()). + BorderBackground(t.Background()). + BorderForeground(t.TextMuted()) +} + +func (q *commandArgumentsDialogCmp) Position() (int, int) { + row := 10 + col := q.wWidth / 2 + col -= q.width / 2 + return row, col +} + +// ID implements CommandArgumentsDialog. +func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID { + return argumentsDialogID +} diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 41b21064e13a938246fb733d0ff24a4bcdca3a40..b52ad5c6bd6e653295e9acce33dc1013b05fd99e 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -13,7 +13,7 @@ import ( ) const ( - id dialogs.DialogID = "commands" + commandsDialogID dialogs.DialogID = "commands" defaultWidth int = 60 ) @@ -54,11 +54,17 @@ func (c *commandDialogCmp) Init() tea.Cmd { return util.ReportError(err) } - commands = append(commands, c.defaultCommands()...) + commandItems := []util.Model{} + if len(commands) > 0 { + commandItems = append(commandItems, NewItemSection("Custom")) + for _, cmd := range commands { + commandItems = append(commandItems, NewCommandItem(cmd)) + } + } - commandItems := make([]util.Model, 0, len(commands)) + commandItems = append(commandItems, NewItemSection("Default")) - for _, cmd := range commands { + for _, cmd := range c.defaultCommands() { commandItems = append(commandItems, NewCommandItem(cmd)) } @@ -93,7 +99,7 @@ func (c *commandDialogCmp) listWidth() int { } func (c *commandDialogCmp) listHeight() int { - listHeigh := len(c.commandList.Items()) + 2 // height based on items + 2 for the input + listHeigh := len(c.commandList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections return min(listHeigh, c.wHeight/2) } @@ -158,5 +164,5 @@ func (c *commandDialogCmp) defaultCommands() []Command { } func (c *commandDialogCmp) ID() dialogs.DialogID { - return id + return commandsDialogID } diff --git a/internal/tui/components/dialogs/commands/item.go b/internal/tui/components/dialogs/commands/item.go index 5cdeae2112fd5d310587982b2a89ff82d7c2146b..d1395af9c2889808d8a005d0940d017558795b13 100644 --- a/internal/tui/components/dialogs/commands/item.go +++ b/internal/tui/components/dialogs/commands/item.go @@ -1,9 +1,12 @@ package commands import ( + "strings" + tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/ansi" + "github.com/opencode-ai/opencode/internal/tui/components/core/list" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/theme" @@ -54,15 +57,15 @@ func (c *commandItem) View() tea.View { titleMatchStyle = titleMatchStyle.Foreground(t.Background()).Background(t.Primary()).Bold(true) } var ranges []lipgloss.Range - truncatedTitle := ansi.Truncate(c.command.Title, c.width-2, "…") - text := titleStyle.Padding(0, 1).Render(truncatedTitle) + truncatedTitle := ansi.Truncate(c.command.Title, c.width, "…") + text := titleStyle.Render(truncatedTitle) if len(c.matchIndexes) > 0 { for _, rng := range matchedRanges(c.matchIndexes) { // ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes. // all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions. // so we need to adjust it here: start, stop := bytePosToVisibleCharPos(text, rng) - ranges = append(ranges, lipgloss.NewRange(start+1, stop+2, titleMatchStyle)) + ranges = append(ranges, lipgloss.NewRange(start, stop+1, titleMatchStyle)) } text = lipgloss.StyleRanges(text, ranges...) } @@ -148,3 +151,53 @@ func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) { stop = pos return start, stop } + +type ItemSection interface { + util.Model + layout.Sizeable + list.SectionHeader +} +type itemSectionModel struct { + width int + title string +} + +func NewItemSection(title string) ItemSection { + return &itemSectionModel{ + title: title, + } +} + +func (m *itemSectionModel) Init() tea.Cmd { + return nil +} + +func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { + return m, nil +} + +func (m *itemSectionModel) View() tea.View { + t := theme.CurrentTheme() + title := ansi.Truncate(m.title, m.width-1, "…") + style := styles.BaseStyle().Padding(1, 0, 0, 0).Width(m.width).Foreground(t.TextMuted()).Bold(true) + if len(title) < m.width { + remainingWidth := m.width - lipgloss.Width(title) + if remainingWidth > 0 { + title += " " + strings.Repeat("─", remainingWidth-1) + } + } + return tea.NewView(style.Render(title)) +} + +func (m *itemSectionModel) GetSize() (int, int) { + return m.width, 1 +} + +func (m *itemSectionModel) SetSize(width int, height int) tea.Cmd { + m.width = width + return nil +} + +func (m *itemSectionModel) IsSectionHeader() bool { + return true +} diff --git a/internal/tui/components/dialogs/commands/keys.go b/internal/tui/components/dialogs/commands/keys.go new file mode 100644 index 0000000000000000000000000000000000000000..cdff10da75a9b02f8657b3b60631599137203efe --- /dev/null +++ b/internal/tui/components/dialogs/commands/keys.go @@ -0,0 +1 @@ +package commands diff --git a/internal/tui/components/dialogs/commands/loader.go b/internal/tui/components/dialogs/commands/loader.go index 8767d6bf7b0c3a3e901dcdebd029cc29d7da4ed6..92064394fa7b9f832dce7d9fd82b20a24e1127c2 100644 --- a/internal/tui/components/dialogs/commands/loader.go +++ b/internal/tui/components/dialogs/commands/loader.go @@ -162,7 +162,6 @@ func createCommandHandler(id string, content string) func(Command) tea.Cmd { return util.CmdHandler(CommandRunCustomMsg{ Content: content, - Args: nil, }) } } @@ -189,7 +188,7 @@ func extractArgNames(content string) []string { func ensureDir(path string) error { if _, err := os.Stat(path); os.IsNotExist(err) { - return os.MkdirAll(path, 0755) + return os.MkdirAll(path, 0o755) } return nil } @@ -200,5 +199,4 @@ func isMarkdownFile(name string) bool { type CommandRunCustomMsg struct { Content string - Args map[string]string } diff --git a/internal/tui/components/dialogs/dialogs.go b/internal/tui/components/dialogs/dialogs.go index 9862388fc16af59b0dc3ac63a8485cc02924370d..f5e5e285de96ed7b59e0f6600ef9eb78548c22cd 100644 --- a/internal/tui/components/dialogs/dialogs.go +++ b/internal/tui/components/dialogs/dialogs.go @@ -23,11 +23,6 @@ type CloseCallback interface { Close() tea.Cmd } -// AbsolutePositionable is an interface for components that can set their position -type AbsolutePositionable interface { - SetPosition(x, y int) -} - // OpenDialogMsg is sent to open a new dialog with specified dimensions. type OpenDialogMsg struct { Model DialogModel @@ -50,14 +45,14 @@ type dialogCmp struct { width, height int dialogs []DialogModel idMap map[DialogID]int - keymap KeyMap + keyMap KeyMap } // NewDialogCmp creates a new dialog manager. func NewDialogCmp() DialogCmp { return dialogCmp{ dialogs: []DialogModel{}, - keymap: DefaultKeymap(), + keyMap: DefaultKeyMap(), idMap: make(map[DialogID]int), } } @@ -94,7 +89,7 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return d, nil case tea.KeyPressMsg: - if key.Matches(msg, d.keymap.Close) { + if key.Matches(msg, d.keyMap.Close) { return d, util.CmdHandler(CloseDialogMsg{}) } } @@ -114,10 +109,10 @@ func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) { return d, nil // Do not open a dialog if it's already the topmost one } if dialog.ID() == "quit" { - return d, nil // Do not open dialogs ontop of quit + return d, nil // Do not open dialogs on top of quit } } - // if the dialog is already in thel stack make it the last item + // if the dialog is already in the stack make it the last item if _, ok := d.idMap[msg.Model.ID()]; ok { existing := d.dialogs[d.idMap[msg.Model.ID()]] // Reuse the model so we keep the state diff --git a/internal/tui/components/dialogs/keys.go b/internal/tui/components/dialogs/keys.go index 34a5aeb4d5b46b52e4ef6968e5c8bc480a2e3819..a3b68acb6e4d6b1773aa84933668f94bbc6a4e16 100644 --- a/internal/tui/components/dialogs/keys.go +++ b/internal/tui/components/dialogs/keys.go @@ -10,7 +10,7 @@ type KeyMap struct { Close key.Binding } -func DefaultKeymap() KeyMap { +func DefaultKeyMap() KeyMap { return KeyMap{ Close: key.NewBinding( key.WithKeys("esc"), diff --git a/internal/tui/tui.go b/internal/tui/tui.go index e2dabdd777b464d16b84eeaf159e6ce5e685768a..a176d895b69278252a4184fe879aca30b9cbe0ad 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -17,8 +17,6 @@ import ( "github.com/opencode-ai/opencode/internal/tui/util" ) -// type startCompactSessionMsg struct{} - type appModel struct { width, height int keyMap KeyMap @@ -32,40 +30,6 @@ type appModel struct { app *app.App - // selectedSession session.Session - // - // showPermissions bool - // permissions dialog.PermissionDialogCmp - // - // showHelp bool - // help dialog.HelpCmp - // - // showSessionDialog bool - // sessionDialog dialog.SessionDialog - // - // showCommandDialog bool - // commandDialog dialog.CommandDialog - // commands []dialog.Command - // - // showModelDialog bool - // modelDialog dialog.ModelDialog - // - // showInitDialog bool - // initDialog dialog.InitDialogCmp - // - // showFilepicker bool - // filepicker dialog.FilepickerCmp - // - // showThemeDialog bool - // themeDialog dialog.ThemeDialog - // - // showMultiArgumentsDialog bool - // multiArgumentsDialog dialog.MultiArgumentsDialogCmp - // - // isCompacting bool - // compactingMessage string - - // NEW DIALOG dialog dialogs.DialogCmp } @@ -77,32 +41,6 @@ func (a appModel) Init() tea.Cmd { cmd = a.status.Init() cmds = append(cmds, cmd) - // cmd = a.help.Init() - // cmds = append(cmds, cmd) - // cmd = a.sessionDialog.Init() - // cmds = append(cmds, cmd) - // cmd = a.commandDialog.Init() - // cmds = append(cmds, cmd) - // cmd = a.modelDialog.Init() - // cmds = append(cmds, cmd) - // cmd = a.initDialog.Init() - // cmds = append(cmds, cmd) - // cmd = a.filepicker.Init() - // cmds = append(cmds, cmd) - // cmd = a.themeDialog.Init() - // cmds = append(cmds, cmd) - - // Check if we should show the init dialog - // cmds = append(cmds, func() tea.Msg { - // shouldShow, err := config.ShouldShowInitDialog() - // if err != nil { - // return util.InfoMsg{ - // Type: util.InfoTypeError, - // Msg: "Failed to check init status: " + err.Error(), - // } - // } - // return dialog.ShowInitDialogMsg{Show: shouldShow} - // }) return tea.Batch(cmds...) } @@ -113,56 +51,13 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: return a, a.handleWindowResize(msg) - // TODO: remove when refactor is done - // msg.Height -= 1 // Make space for the status bar - // a.width, a.height = msg.Width, msg.Height - // - // s, _ := a.status.Update(msg) - // a.status = s.(core.StatusCmp) - // updated, cmd := a.pages[a.currentPage].Update(msg) - // a.pages[a.currentPage] = updated.(util.Model) - // cmds = append(cmds, cmd) - // - // prm, permCmd := a.permissions.Update(msg) - // a.permissions = prm.(dialog.PermissionDialogCmp) - // cmds = append(cmds, permCmd) - // - // help, helpCmd := a.help.Update(msg) - // a.help = help.(dialog.HelpCmp) - // cmds = append(cmds, helpCmd) - // - // session, sessionCmd := a.sessionDialog.Update(msg) - // a.sessionDialog = session.(dialog.SessionDialog) - // cmds = append(cmds, sessionCmd) - // - // command, commandCmd := a.commandDialog.Update(msg) - // a.commandDialog = command.(dialog.CommandDialog) - // cmds = append(cmds, commandCmd) - // - // filepicker, filepickerCmd := a.filepicker.Update(msg) - // a.filepicker = filepicker.(dialog.FilepickerCmp) - // cmds = append(cmds, filepickerCmd) - // - // a.initDialog.SetSize(msg.Width, msg.Height) - // - // if a.showMultiArgumentsDialog { - // a.multiArgumentsDialog.SetSize(msg.Width, msg.Height) - // args, argsCmd := a.multiArgumentsDialog.Update(msg) - // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp) - // cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init()) - // } - // - // dialog, cmd := a.dialog.Update(msg) - // a.dialog = dialog.(dialogs.DialogCmp) - // cmds = append(cmds, cmd) - // - // return a, tea.Batch(cmds...) // Dialog messages case dialogs.OpenDialogMsg, dialogs.CloseDialogMsg: u, dialogCmd := a.dialog.Update(msg) a.dialog = u.(dialogs.DialogCmp) return a, dialogCmd + case commands.ShowArgumentsDialogMsg: // Page change messages case page.PageChangeMsg: @@ -170,398 +65,28 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Status Messages case util.InfoMsg, util.ClearStatusMsg: - s, cmd := a.status.Update(msg) + s, statusCmd := a.status.Update(msg) a.status = s.(core.StatusCmp) - cmds = append(cmds, cmd) + cmds = append(cmds, statusCmd) return a, tea.Batch(cmds...) + // Logs case pubsub.Event[logging.LogMessage]: // Send to the status component - s, cmd := a.status.Update(msg) + s, statusCmd := a.status.Update(msg) a.status = s.(core.StatusCmp) - cmds = append(cmds, cmd) + cmds = append(cmds, statusCmd) // If the current page is logs, update the logs view if a.currentPage == page.LogsPage { - updated, cmd := a.pages[a.currentPage].Update(msg) + updated, pageCmd := a.pages[a.currentPage].Update(msg) a.pages[a.currentPage] = updated.(util.Model) - cmds = append(cmds, cmd) + cmds = append(cmds, pageCmd) } return a, tea.Batch(cmds...) - - // // Permission - // case pubsub.Event[permission.PermissionRequest]: - // a.showPermissions = true - // return a, a.permissions.SetPermissions(msg.Payload) - // case dialog.PermissionResponseMsg: - // var cmd tea.Cmd - // switch msg.Action { - // case dialog.PermissionAllow: - // a.app.Permissions.Grant(msg.Permission) - // case dialog.PermissionAllowForSession: - // a.app.Permissions.GrantPersistant(msg.Permission) - // case dialog.PermissionDeny: - // a.app.Permissions.Deny(msg.Permission) - // } - // a.showPermissions = false - // return a, cmd - // - // // Theme changed - // case dialog.ThemeChangedMsg: - // updated, cmd := a.pages[a.currentPage].Update(msg) - // a.pages[a.currentPage] = updated.(util.Model) - // a.showThemeDialog = false - // return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName)) - // - // case dialog.CloseSessionDialogMsg: - // a.showSessionDialog = false - // return a, nil - // - // case dialog.CloseCommandDialogMsg: - // a.showCommandDialog = false - // return a, nil - // - // case startCompactSessionMsg: - // // Start compacting the current session - // a.isCompacting = true - // a.compactingMessage = "Starting summarization..." - // - // if a.selectedSession.ID == "" { - // a.isCompacting = false - // return a, util.ReportWarn("No active session to summarize") - // } - // - // // Start the summarization process - // return a, func() tea.Msg { - // ctx := context.Background() - // a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID) - // return nil - // } - // - // case pubsub.Event[agent.AgentEvent]: - // payload := msg.Payload - // if payload.Error != nil { - // a.isCompacting = false - // return a, util.ReportError(payload.Error) - // } - // - // a.compactingMessage = payload.Progress - // - // if payload.Done && payload.Type == agent.AgentEventTypeSummarize { - // a.isCompacting = false - // return a, util.ReportInfo("Session summarization complete") - // } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" { - // model := a.app.CoderAgent.Model() - // contextWindow := model.ContextWindow - // tokens := a.selectedSession.CompletionTokens + a.selectedSession.PromptTokens - // if (tokens >= int64(float64(contextWindow)*0.95)) && config.Get().AutoCompact { - // return a, util.CmdHandler(startCompactSessionMsg{}) - // } - // } - // // Continue listening for events - // return a, nil - // - // case dialog.CloseThemeDialogMsg: - // a.showThemeDialog = false - // return a, nil - // - // case dialog.CloseModelDialogMsg: - // a.showModelDialog = false - // return a, nil - // - // case dialog.ModelSelectedMsg: - // a.showModelDialog = false - // - // model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID) - // if err != nil { - // return a, util.ReportError(err) - // } - // - // return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name)) - // - // case dialog.ShowInitDialogMsg: - // a.showInitDialog = msg.Show - // return a, nil - // - // case dialog.CloseInitDialogMsg: - // a.showInitDialog = false - // if msg.Initialize { - // // Run the initialization command - // for _, cmd := range a.commands { - // if cmd.ID == "init" { - // // Mark the project as initialized - // if err := config.MarkProjectInitialized(); err != nil { - // return a, util.ReportError(err) - // } - // return a, cmd.Handler(cmd) - // } - // } - // } else { - // // Mark the project as initialized without running the command - // if err := config.MarkProjectInitialized(); err != nil { - // return a, util.ReportError(err) - // } - // } - // return a, nil - // - // case chat.SessionSelectedMsg: - // a.selectedSession = msg - // a.sessionDialog.SetSelectedSession(msg.ID) - // - // case pubsub.Event[session.Session]: - // if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == a.selectedSession.ID { - // a.selectedSession = msg.Payload - // } - // case dialog.SessionSelectedMsg: - // a.showSessionDialog = false - // if a.currentPage == page.ChatPage { - // return a, util.CmdHandler(chat.SessionSelectedMsg(msg.Session)) - // } - // return a, nil - // - // case dialog.CommandSelectedMsg: - // a.showCommandDialog = false - // // Execute the command handler if available - // if msg.Command.Handler != nil { - // return a, msg.Command.Handler(msg.Command) - // } - // return a, util.ReportInfo("Command selected: " + msg.Command.Title) - // - // case dialog.ShowMultiArgumentsDialogMsg: - // // Show multi-arguments dialog - // a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames) - // a.showMultiArgumentsDialog = true - // return a, a.multiArgumentsDialog.Init() - // - // case dialog.CloseMultiArgumentsDialogMsg: - // // Close multi-arguments dialog - // a.showMultiArgumentsDialog = false - // - // // If submitted, replace all named arguments and run the command - // if msg.Submit { - // content := msg.Content - // - // // Replace each named argument with its value - // for name, value := range msg.Args { - // placeholder := "$" + name - // content = strings.ReplaceAll(content, placeholder, value) - // } - // - // // Execute the command with arguments - // return a, util.CmdHandler(dialog.CommandRunCustomMsg{ - // Content: content, - // Args: msg.Args, - // }) - // } - // return a, nil - // case tea.KeyPressMsg: return a, a.handleKeyPressMsg(msg) - // if a.dialog.HasDialogs() { - // u, dialogCmd := a.dialog.Update(msg) - // a.dialog = u.(dialogs.DialogCmp) - // return a, dialogCmd - // } - // // If multi-arguments dialog is open, let it handle the key press first - // if a.showMultiArgumentsDialog { - // args, cmd := a.multiArgumentsDialog.Update(msg) - // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp) - // return a, cmd - // } - // - // switch { - // case key.Matches(msg, keys.Quit): - // // TODO: fix this after testing - // // a.showQuit = !a.showQuit - // // if a.showHelp { - // // a.showHelp = false - // // } - // // if a.showSessionDialog { - // // a.showSessionDialog = false - // // } - // // if a.showCommandDialog { - // // a.showCommandDialog = false - // // } - // // if a.showFilepicker { - // // a.showFilepicker = false - // // a.filepicker.ToggleFilepicker(a.showFilepicker) - // // } - // // if a.showModelDialog { - // // a.showModelDialog = false - // // } - // // if a.showMultiArgumentsDialog { - // // a.showMultiArgumentsDialog = false - // // } - // return a, util.CmdHandler(dialogs.OpenDialogMsg{ - // Model: quit.NewQuitDialog(), - // }) - // case key.Matches(msg, keys.SwitchSession): - // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showCommandDialog { - // // Load sessions and show the dialog - // sessions, err := a.app.Sessions.List(context.Background()) - // if err != nil { - // return a, util.ReportError(err) - // } - // if len(sessions) == 0 { - // return a, util.ReportWarn("No sessions available") - // } - // a.sessionDialog.SetSessions(sessions) - // a.showSessionDialog = true - // return a, nil - // } - // return a, nil - // case key.Matches(msg, keys.Commands): - // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker { - // // Show commands dialog - // if len(a.commands) == 0 { - // return a, util.ReportWarn("No commands available") - // } - // a.commandDialog.SetCommands(a.commands) - // a.showCommandDialog = true - // return a, nil - // } - // return a, util.CmdHandler(dialogs.OpenDialogMsg{ - // Model: commands.NewCommandDialog(), - // }) - // case key.Matches(msg, keys.Models): - // if a.showModelDialog { - // a.showModelDialog = false - // return a, nil - // } - // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog { - // a.showModelDialog = true - // return a, nil - // } - // return a, nil - // case key.Matches(msg, keys.SwitchTheme): - // if !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog { - // // Show theme switcher dialog - // a.showThemeDialog = true - // // Theme list is dynamically loaded by the dialog component - // return a, a.themeDialog.Init() - // } - // return a, nil - // case key.Matches(msg, returnKey) || key.Matches(msg): - // if msg.String() == quitKey { - // if a.currentPage == page.LogsPage { - // return a, a.moveToPage(page.ChatPage) - // } - // } else if !a.filepicker.IsCWDFocused() { - // if a.showHelp { - // a.showHelp = !a.showHelp - // return a, nil - // } - // if a.showInitDialog { - // a.showInitDialog = false - // // Mark the project as initialized without running the command - // if err := config.MarkProjectInitialized(); err != nil { - // return a, util.ReportError(err) - // } - // return a, nil - // } - // if a.showFilepicker { - // a.showFilepicker = false - // a.filepicker.ToggleFilepicker(a.showFilepicker) - // return a, nil - // } - // if a.currentPage == page.LogsPage { - // return a, a.moveToPage(page.ChatPage) - // } - // } - // case key.Matches(msg, keys.Logs): - // return a, a.moveToPage(page.LogsPage) - // case key.Matches(msg, keys.Help): - // a.showHelp = !a.showHelp - // return a, nil - // case key.Matches(msg, helpEsc): - // if a.app.CoderAgent.IsBusy() { - // a.showHelp = !a.showHelp - // return a, nil - // } - // case key.Matches(msg, keys.Filepicker): - // a.showFilepicker = !a.showFilepicker - // a.filepicker.ToggleFilepicker(a.showFilepicker) - // return a, nil - // } - // default: - // u, dialogCmd := a.dialog.Update(msg) - // a.dialog = u.(dialogs.DialogCmp) - // cmds = append(cmds, dialogCmd) - // f, filepickerCmd := a.filepicker.Update(msg) - // a.filepicker = f.(dialog.FilepickerCmp) - // cmds = append(cmds, filepickerCmd) - // } - - // if a.showFilepicker { - // f, filepickerCmd := a.filepicker.Update(msg) - // a.filepicker = f.(dialog.FilepickerCmp) - // cmds = append(cmds, filepickerCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showPermissions { - // d, permissionsCmd := a.permissions.Update(msg) - // a.permissions = d.(dialog.PermissionDialogCmp) - // cmds = append(cmds, permissionsCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showSessionDialog { - // d, sessionCmd := a.sessionDialog.Update(msg) - // a.sessionDialog = d.(dialog.SessionDialog) - // cmds = append(cmds, sessionCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showCommandDialog { - // d, commandCmd := a.commandDialog.Update(msg) - // a.commandDialog = d.(dialog.CommandDialog) - // cmds = append(cmds, commandCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showModelDialog { - // d, modelCmd := a.modelDialog.Update(msg) - // a.modelDialog = d.(dialog.ModelDialog) - // cmds = append(cmds, modelCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showInitDialog { - // d, initCmd := a.initDialog.Update(msg) - // a.initDialog = d.(dialog.InitDialogCmp) - // cmds = append(cmds, initCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } - // } - // - // if a.showThemeDialog { - // d, themeCmd := a.themeDialog.Update(msg) - // a.themeDialog = d.(dialog.ThemeDialog) - // cmds = append(cmds, themeCmd) - // // Only block key messages send all other messages down - // if _, ok := msg.(tea.KeyPressMsg); ok { - // return a, tea.Batch(cmds...) - // } } - // s, _ := a.status.Update(msg) a.status = s.(core.StatusCmp) updated, cmd := a.pages[a.currentPage].Update(msg) @@ -659,177 +184,6 @@ func (a *appModel) View() tea.View { appView := lipgloss.JoinVertical(lipgloss.Top, components...) - // if a.showPermissions { - // overlay := a.permissions.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showFilepicker { - // overlay := a.filepicker.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // // Show compacting status overlay - // if a.isCompacting { - // t := theme.CurrentTheme() - // style := lipgloss.NewStyle(). - // Border(lipgloss.RoundedBorder()). - // BorderForeground(t.BorderFocused()). - // BorderBackground(t.Background()). - // Padding(1, 2). - // Background(t.Background()). - // Foreground(t.Text()) - // - // overlay := style.Render("Summarizing\n" + a.compactingMessage) - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showHelp { - // bindings := layout.KeyMapToSlice(a.keymap) - // if p, ok := a.pages[a.currentPage].(layout.Bindings); ok { - // bindings = append(bindings, p.BindingKeys()...) - // } - // if a.showPermissions { - // bindings = append(bindings, a.permissions.BindingKeys()...) - // } - // if a.currentPage == page.LogsPage { - // // bindings = append(bindings, logsKeyReturnKey) - // } - // if !a.app.CoderAgent.IsBusy() { - // // bindings = append(bindings, helpEsc) - // } - // - // a.help.SetBindings(bindings) - // - // overlay := a.help.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showSessionDialog { - // overlay := a.sessionDialog.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showModelDialog { - // overlay := a.modelDialog.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showCommandDialog { - // overlay := a.commandDialog.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showInitDialog { - // overlay := a.initDialog.View() - // appView = layout.PlaceOverlay( - // a.width/2-lipgloss.Width(overlay)/2, - // a.height/2-lipgloss.Height(overlay)/2, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showThemeDialog { - // overlay := a.themeDialog.View().String() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } - // - // if a.showMultiArgumentsDialog { - // overlay := a.multiArgumentsDialog.View() - // row := lipgloss.Height(appView) / 2 - // row -= lipgloss.Height(overlay) / 2 - // col := lipgloss.Width(appView) / 2 - // col -= lipgloss.Width(overlay) / 2 - // appView = layout.PlaceOverlay( - // col, - // row, - // overlay, - // appView, - // true, - // ) - // } t := theme.CurrentTheme() if a.dialog.HasDialogs() { layers := append( @@ -863,63 +217,13 @@ func New(app *app.App) tea.Model { loadedPages: make(map[page.PageID]bool), keyMap: DefaultKeyMap(), - // help: dialog.NewHelpCmp(), - // sessionDialog: dialog.NewSessionDialogCmp(), - // commandDialog: dialog.NewCommandDialogCmp(), - // modelDialog: dialog.NewModelDialogCmp(), - // permissions: dialog.NewPermissionDialogCmp(), - // initDialog: dialog.NewInitDialogCmp(), - // themeDialog: dialog.NewThemeDialogCmp(), - // commands: []dialog.Command{}, pages: map[page.PageID]util.Model{ page.ChatPage: page.NewChatPage(app), page.LogsPage: page.NewLogsPage(), }, - // filepicker: dialog.NewFilepickerCmp(app), - // New dialog dialog: dialogs.NewDialogCmp(), } - // model.RegisterCommand(dialog.Command{ - // ID: "init", - // Title: "Initialize Project", - // Description: "Create/Update the OpenCode.md memory file", - // Handler: func(cmd dialog.Command) tea.Cmd { - // prompt := `Please analyze this codebase and create a OpenCode.md file containing: - // 1. Build/lint/test commands - especially for running a single test - // 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc. - // - // The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long. - // If there's already a opencode.md, improve it. - // If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.` - // return tea.Batch( - // util.CmdHandler(chat.SendMsg{ - // Text: prompt, - // }), - // ) - // }, - // }) - // - // model.RegisterCommand(dialog.Command{ - // ID: "compact", - // Title: "Compact Session", - // Description: "Summarize the current session and create a new one with the summary", - // Handler: func(cmd dialog.Command) tea.Cmd { - // return func() tea.Msg { - // return startCompactSessionMsg{} - // } - // }, - // }) - // // Load custom commands - // customCommands, err := dialog.LoadCustomCommands() - // if err != nil { - // logging.Warn("Failed to load custom commands", "error", err) - // } else { - // for _, cmd := range customCommands { - // model.RegisterCommand(cmd) - // } - // } - return model }