on main help sign in
wip list sections
commit2329178f668057516c444975f5b56e45f37de628 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 28 May 2025, 20:59 (1 year ago) parentb787dc0
9 files changed+370−764
internal/tui/components/core/list/list.go+214−37 internal/tui/components/dialogs/commands/arguments.go+72 internal/tui/components/dialogs/commands/commands.go+12−6 internal/tui/components/dialogs/commands/item.go+56−3 internal/tui/components/dialogs/commands/keys.go+1 internal/tui/components/dialogs/commands/loader.go+1−3 internal/tui/components/dialogs/dialogs.go+5−10 internal/tui/components/dialogs/keys.go+1−1 internal/tui/tui.go+8−704
internal/tui/components/core/list/list.go+214−37
@@ -14,7 +14,6 @@ import (
1414 "github.com/opencode-ai/opencode/internal/tui/components/anim"
1515 "github.com/opencode-ai/opencode/internal/tui/layout"
1616 "github.com/opencode-ai/opencode/internal/tui/styles"
17− "github.com/opencode-ai/opencode/internal/tui/theme"
1817 "github.com/opencode-ai/opencode/internal/tui/util"
1918 "github.com/sahilm/fuzzy"
2019)
@@ -60,6 +59,13 @@ type HasMatchIndexes interface {
6059 MatchIndexes([]int) // Sets the indexes of matched characters in the item's content
6160}
6261
62+// SectionHeader interface identifies items that are section headers.
63+// Section headers are rendered differently and are skipped during navigation.
64+type SectionHeader interface {
65+ util.Model
66+ IsSectionHeader() bool // Returns true if this item is a section header
67+}
68+
6369// renderedItem represents a cached rendered item with its position and content.
6470type renderedItem struct {
6571 lines []string // The rendered lines of text for this item
@@ -539,6 +545,7 @@ func (r *reverseRenderer) renderItemLines(item util.Model) []string {
539545
540546// selectPreviousItem moves selection to the previous item in the list.
541547// Handles focus management and ensures the selected item remains visible.
548+// Skips section headers during navigation.
542549func (m *model) selectPreviousItem() tea.Cmd {
543550 if m.selectionState.selectedIndex <= 0 {
544551 return nil
@@ -546,6 +553,17 @@ func (m *model) selectPreviousItem() tea.Cmd {
546553
547554 cmds := []tea.Cmd{m.blurSelected()}
548555 m.selectionState.selectedIndex--
556+
557+ // Skip section headers
558+ for m.selectionState.selectedIndex >= 0 && m.isSectionHeader(m.selectionState.selectedIndex) {
559+ m.selectionState.selectedIndex--
560+ }
561+
562+ // If we went past the beginning, stay at the first non-header item
563+ if m.selectionState.selectedIndex < 0 {
564+ m.selectionState.selectedIndex = m.findFirstSelectableItem()
565+ }
566+
549567 cmds = append(cmds, m.focusSelected())
550568 m.ensureSelectedItemVisible()
551569 return tea.Batch(cmds...)
@@ -553,6 +571,7 @@ func (m *model) selectPreviousItem() tea.Cmd {
553571
554572// selectNextItem moves selection to the next item in the list.
555573// Handles focus management and ensures the selected item remains visible.
574+// Skips section headers during navigation.
556575func (m *model) selectNextItem() tea.Cmd {
557576 if m.selectionState.selectedIndex >= len(m.filteredItems)-1 || m.selectionState.selectedIndex < 0 {
558577 return nil
@@ -560,11 +579,53 @@ func (m *model) selectNextItem() tea.Cmd {
560579
561580 cmds := []tea.Cmd{m.blurSelected()}
562581 m.selectionState.selectedIndex++
582+
583+ // Skip section headers
584+ for m.selectionState.selectedIndex < len(m.filteredItems) && m.isSectionHeader(m.selectionState.selectedIndex) {
585+ m.selectionState.selectedIndex++
586+ }
587+
588+ // If we went past the end, stay at the last non-header item
589+ if m.selectionState.selectedIndex >= len(m.filteredItems) {
590+ m.selectionState.selectedIndex = m.findLastSelectableItem()
591+ }
592+
563593 cmds = append(cmds, m.focusSelected())
564594 m.ensureSelectedItemVisible()
565595 return tea.Batch(cmds...)
566596}
567597
598+// isSectionHeader checks if the item at the given index is a section header.
599+func (m *model) isSectionHeader(index int) bool {
600+ if index < 0 || index >= len(m.filteredItems) {
601+ return false
602+ }
603+ if header, ok := m.filteredItems[index].(SectionHeader); ok {
604+ return header.IsSectionHeader()
605+ }
606+ return false
607+}
608+
609+// findFirstSelectableItem finds the first item that is not a section header.
610+func (m *model) findFirstSelectableItem() int {
611+ for i := 0; i < len(m.filteredItems); i++ {
612+ if !m.isSectionHeader(i) {
613+ return i
614+ }
615+ }
616+ return NoSelection
617+}
618+
619+// findLastSelectableItem finds the last item that is not a section header.
620+func (m *model) findLastSelectableItem() int {
621+ for i := len(m.filteredItems) - 1; i >= 0; i-- {
622+ if !m.isSectionHeader(i) {
623+ return i
624+ }
625+ }
626+ return NoSelection
627+}
628+
568629// ensureSelectedItemVisible scrolls the list to make the selected item visible.
569630// Uses different strategies for forward and reverse rendering modes.
570631func (m *model) ensureSelectedItemVisible() {
@@ -631,25 +692,25 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) {
631692 }
632693}
633694
634−// goToBottom switches to reverse mode and selects the last item.
695+// goToBottom switches to reverse mode and selects the last selectable item.
635696// Commonly used for chat-like interfaces where new content appears at the bottom.
697+// Skips section headers when selecting the last item.
636698func (m *model) goToBottom() tea.Cmd {
637699 cmds := []tea.Cmd{m.blurSelected()}
638700 m.viewState.reverse = true
639− m.selectionState.selectedIndex = len(m.filteredItems) - 1
701+ m.selectionState.selectedIndex = m.findLastSelectableItem()
640702 cmds = append(cmds, m.focusSelected())
641703 m.ResetView()
642704 return tea.Batch(cmds...)
643705}
644706
645−// goToTop switches to forward mode and selects the first item.
707+// goToTop switches to forward mode and selects the first selectable item.
646708// Standard behavior for most list interfaces.
709+// Skips section headers when selecting the first item.
647710func (m *model) goToTop() tea.Cmd {
648711 cmds := []tea.Cmd{m.blurSelected()}
649712 m.viewState.reverse = false
650− if len(m.filteredItems) > 0 {
651− m.selectionState.selectedIndex = 0
652− }
713+ m.selectionState.selectedIndex = m.findFirstSelectableItem()
653714 cmds = append(cmds, m.focusSelected())
654715 m.ResetView()
655716 return tea.Batch(cmds...)
@@ -715,8 +776,12 @@ func (m *model) rerenderItem(inx int) {
715776}
716777
717778// getItemLines converts an item to its rendered lines, including any gap spacing.
779+// Handles section headers with special styling.
718780func (m *model) getItemLines(item util.Model) []string {
719− itemLines := strings.Split(item.View().String(), "\n")
781+ var itemLines []string
782+
783+ itemLines = strings.Split(item.View().String(), "\n")
784+
720785 if m.gapSize > 0 {
721786 gap := make([]string, m.gapSize)
722787 itemLines = append(itemLines, gap...)
@@ -995,6 +1060,7 @@ func (m *model) setReverse(reverse bool) {
9951060
9961061// SetItems replaces all items in the list with a new set.
9971062// Initializes all items, sets their sizes, and establishes initial selection.
1063+// Ensures the initial selection skips section headers.
9981064func (m *model) SetItems(items []util.Model) tea.Cmd {
9991065 m.allItems = items
10001066 m.filteredItems = items
@@ -1006,9 +1072,9 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
10061072
10071073 if len(m.filteredItems) > 0 {
10081074 if m.viewState.reverse {
1009− m.selectionState.selectedIndex = len(m.filteredItems) - 1
1075+ m.selectionState.selectedIndex = m.findLastSelectableItem()
10101076 } else {
1011− m.selectionState.selectedIndex = 0
1077+ m.selectionState.selectedIndex = m.findFirstSelectableItem()
10121078 }
10131079 if cmd := m.focusSelected(); cmd != nil {
10141080 cmds = append(cmds, cmd)
@@ -1022,18 +1088,75 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
10221088}
10231089
10241090func (c *model) inputStyle() lipgloss.Style {
1025− t := theme.CurrentTheme()
1026− return styles.BaseStyle().
1027− BorderStyle(lipgloss.NormalBorder()).
1028− BorderForeground(t.TextMuted()).
1029− BorderBackground(t.Background()).
1030− BorderBottom(true)
1091+ return styles.BaseStyle()
1092+}
1093+
1094+// section represents a group of items under a section header.
1095+type section struct {
1096+ header SectionHeader
1097+ items []util.Model
1098+}
1099+
1100+// parseSections parses the flat item list into sections.
1101+func (m *model) parseSections() []section {
1102+ var sections []section
1103+ var currentSection *section
1104+
1105+ for _, item := range m.allItems {
1106+ if header, ok := item.(SectionHeader); ok && header.IsSectionHeader() {
1107+ // Start a new section
1108+ if currentSection != nil {
1109+ sections = append(sections, *currentSection)
1110+ }
1111+ currentSection = &section{
1112+ header: header,
1113+ items: []util.Model{},
1114+ }
1115+ } else if currentSection != nil {
1116+ // Add item to current section
1117+ currentSection.items = append(currentSection.items, item)
1118+ } else {
1119+ // Item without a section header - create an implicit section
1120+ if len(sections) == 0 || sections[len(sections)-1].header != nil {
1121+ sections = append(sections, section{
1122+ header: nil,
1123+ items: []util.Model{item},
1124+ })
1125+ } else {
1126+ // Add to the last implicit section
1127+ sections[len(sections)-1].items = append(sections[len(sections)-1].items, item)
1128+ }
1129+ }
1130+ }
1131+
1132+ // Don't forget the last section
1133+ if currentSection != nil {
1134+ sections = append(sections, *currentSection)
1135+ }
1136+
1137+ return sections
1138+}
1139+
1140+// flattenSections converts sections back to a flat list.
1141+func (m *model) flattenSections(sections []section) []util.Model {
1142+ var result []util.Model
1143+
1144+ for _, sect := range sections {
1145+ if sect.header != nil {
1146+ result = append(result, sect.header)
1147+ }
1148+ result = append(result, sect.items...)
1149+ }
1150+
1151+ return result
10311152}
10321153
10331154func (m *model) filter(search string) tea.Cmd {
10341155 var cmds []tea.Cmd
10351156 search = strings.TrimSpace(search)
10361157 search = strings.ToLower(search)
1158+
1159+ // Clear focus and match indexes from all items
10371160 for _, item := range m.allItems {
10381161 if i, ok := item.(layout.Focusable); ok {
10391162 cmds = append(cmds, i.Blur())
@@ -1042,34 +1165,32 @@ func (m *model) filter(search string) tea.Cmd {
10421165 i.MatchIndexes(make([]int, 0))
10431166 }
10441167 }
1168+
10451169 if search == "" {
1046− cmds = append(cmds, m.SetItems(m.allItems)) // Reset to all items if search is empty
1170+ cmds = append(cmds, m.SetItems(m.allItems))
10471171 return tea.Batch(cmds...)
10481172 }
1049− words := make([]string, 0, len(m.allItems))
1050− for _, cmd := range m.allItems {
1051− if f, ok := cmd.(HasFilterValue); ok {
1052− words = append(words, strings.ToLower(f.FilterValue()))
1053− } else {
1054− words = append(words, strings.ToLower(""))
1055− }
1056− }
1057− matches := fuzzy.Find(search, words)
1058− sort.Sort(matches)
1059− filteredItems := make([]util.Model, 0, len(matches))
1060− for _, match := range matches {
1061− item := m.allItems[match.Index]
1062− if i, ok := item.(HasMatchIndexes); ok {
1063− i.MatchIndexes(match.MatchedIndexes)
1173+
1174+ // Parse items into sections
1175+ sections := m.parseSections()
1176+ var filteredSections []section
1177+
1178+ for _, sect := range sections {
1179+ filteredSection := m.filterSection(sect, search)
1180+ if filteredSection != nil {
1181+ filteredSections = append(filteredSections, *filteredSection)
10641182 }
1065− filteredItems = append(filteredItems, item)
10661183 }
1067− m.filteredItems = filteredItems
1068− if len(filteredItems) > 0 {
1184+
1185+ // Rebuild flat list from filtered sections
1186+ m.filteredItems = m.flattenSections(filteredSections)
1187+
1188+ // Set initial selection
1189+ if len(m.filteredItems) > 0 {
10691190 if m.viewState.reverse {
1070− m.selectionState.selectedIndex = len(filteredItems) - 1
1191+ m.selectionState.selectedIndex = m.findLastSelectableItem()
10711192 } else {
1072− m.selectionState.selectedIndex = 0
1193+ m.selectionState.selectedIndex = m.findFirstSelectableItem()
10731194 }
10741195 if cmd := m.focusSelected(); cmd != nil {
10751196 cmds = append(cmds, cmd)
@@ -1081,3 +1202,59 @@ func (m *model) filter(search string) tea.Cmd {
10811202 m.ResetView()
10821203 return tea.Batch(cmds...)
10831204}
1205+
1206+// filterSection filters items within a section and returns the section if it has matches.
1207+func (m *model) filterSection(sect section, search string) *section {
1208+ var matchedItems []util.Model
1209+ var hasHeaderMatch bool
1210+
1211+ // Check if section header itself matches
1212+ if sect.header != nil {
1213+ headerText := strings.ToLower(sect.header.View().String())
1214+ if strings.Contains(headerText, search) {
1215+ hasHeaderMatch = true
1216+ // If header matches, include all items in the section
1217+ matchedItems = sect.items
1218+ }
1219+ }
1220+
1221+ // If header didn't match, filter items within the section
1222+ if !hasHeaderMatch && len(sect.items) > 0 {
1223+ // Create words array for items in this section
1224+ words := make([]string, len(sect.items))
1225+ for i, item := range sect.items {
1226+ if f, ok := item.(HasFilterValue); ok {
1227+ words[i] = strings.ToLower(f.FilterValue())
1228+ } else {
1229+ words[i] = ""
1230+ }
1231+ }
1232+
1233+ // Find matches within this section
1234+ matches := fuzzy.Find(search, words)
1235+
1236+ // Sort matches by score but preserve relative order for equal scores
1237+ sort.SliceStable(matches, func(i, j int) bool {
1238+ return matches[i].Score > matches[j].Score
1239+ })
1240+
1241+ // Build matched items list
1242+ for _, match := range matches {
1243+ item := sect.items[match.Index]
1244+ if i, ok := item.(HasMatchIndexes); ok {
1245+ i.MatchIndexes(match.MatchedIndexes)
1246+ }
1247+ matchedItems = append(matchedItems, item)
1248+ }
1249+ }
1250+
1251+ // Return section only if it has matches
1252+ if len(matchedItems) > 0 {
1253+ return &section{
1254+ header: sect.header,
1255+ items: matchedItems,
1256+ }
1257+ }
1258+
1259+ return nil
1260+}
internal/tui/components/dialogs/commands/arguments.go+72
@@ -1,5 +1,17 @@
11package commands
22
3+import (
4+ tea "github.com/charmbracelet/bubbletea/v2"
5+ "github.com/charmbracelet/lipgloss/v2"
6+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
7+ "github.com/opencode-ai/opencode/internal/tui/styles"
8+ "github.com/opencode-ai/opencode/internal/tui/theme"
9+)
10+
11+const (
12+ argumentsDialogID dialogs.DialogID = "arguments"
13+)
14+
315// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
416type ShowArgumentsDialogMsg struct {
517 CommandID string
@@ -14,3 +26,63 @@ type CloseArgumentsDialogMsg struct {
1426 Content string
1527 Args map[string]string
1628}
29+
30+// CommandArgumentsDialog represents the commands dialog.
31+type CommandArgumentsDialog interface {
32+ dialogs.DialogModel
33+}
34+
35+type commandArgumentsDialogCmp struct {
36+ width int
37+ wWidth int // Width of the terminal window
38+ wHeight int // Height of the terminal window
39+}
40+
41+func NewCommandArgumentsDialog() CommandArgumentsDialog {
42+ return &commandArgumentsDialogCmp{}
43+}
44+
45+// Init implements CommandArgumentsDialog.
46+func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
47+ return nil
48+}
49+
50+// Update implements CommandArgumentsDialog.
51+func (c *commandArgumentsDialogCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
52+ return c, nil
53+}
54+
55+// View implements CommandArgumentsDialog.
56+func (c *commandArgumentsDialogCmp) View() tea.View {
57+ return tea.NewView("")
58+}
59+
60+func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
61+ offset := 10 + 1
62+ cursor.Y += offset
63+ _, col := c.Position()
64+ cursor.X = cursor.X + col + 2
65+ return cursor
66+}
67+
68+func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
69+ t := theme.CurrentTheme()
70+ return styles.BaseStyle().
71+ Width(c.width).
72+ Padding(1).
73+ Border(lipgloss.RoundedBorder()).
74+ BorderBackground(t.Background()).
75+ BorderForeground(t.TextMuted())
76+}
77+
78+func (q *commandArgumentsDialogCmp) Position() (int, int) {
79+ row := 10
80+ col := q.wWidth / 2
81+ col -= q.width / 2
82+ return row, col
83+}
84+
85+// ID implements CommandArgumentsDialog.
86+func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
87+ return argumentsDialogID
88+}
internal/tui/components/dialogs/commands/commands.go+12−6
@@ -13,7 +13,7 @@ import (
1313)
1414
1515const (
16− id dialogs.DialogID = "commands"
16+ commandsDialogID dialogs.DialogID = "commands"
1717
1818 defaultWidth int = 60
1919)
@@ -54,11 +54,17 @@ func (c *commandDialogCmp) Init() tea.Cmd {
5454 return util.ReportError(err)
5555 }
5656
57− commands = append(commands, c.defaultCommands()...)
57+ commandItems := []util.Model{}
58+ if len(commands) > 0 {
59+ commandItems = append(commandItems, NewItemSection("Custom"))
60+ for _, cmd := range commands {
61+ commandItems = append(commandItems, NewCommandItem(cmd))
62+ }
63+ }
5864
59− commandItems := make([]util.Model, 0, len(commands))
65+ commandItems = append(commandItems, NewItemSection("Default"))
6066
61− for _, cmd := range commands {
67+ for _, cmd := range c.defaultCommands() {
6268 commandItems = append(commandItems, NewCommandItem(cmd))
6369 }
6470
@@ -93,7 +99,7 @@ func (c *commandDialogCmp) listWidth() int {
9399}
94100
95101func (c *commandDialogCmp) listHeight() int {
96− listHeigh := len(c.commandList.Items()) + 2 // height based on items + 2 for the input
102+ listHeigh := len(c.commandList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
97103 return min(listHeigh, c.wHeight/2)
98104}
99105
@@ -158,5 +164,5 @@ func (c *commandDialogCmp) defaultCommands() []Command {
158164}
159165
160166func (c *commandDialogCmp) ID() dialogs.DialogID {
161− return id
167+ return commandsDialogID
162168}
internal/tui/components/dialogs/commands/item.go+56−3
@@ -1,9 +1,12 @@
11package commands
22
33import (
4+ "strings"
5+
46 tea "github.com/charmbracelet/bubbletea/v2"
57 "github.com/charmbracelet/lipgloss/v2"
68 "github.com/charmbracelet/x/ansi"
9+ "github.com/opencode-ai/opencode/internal/tui/components/core/list"
710 "github.com/opencode-ai/opencode/internal/tui/layout"
811 "github.com/opencode-ai/opencode/internal/tui/styles"
912 "github.com/opencode-ai/opencode/internal/tui/theme"
@@ -54,15 +57,15 @@ func (c *commandItem) View() tea.View {
5457 titleMatchStyle = titleMatchStyle.Foreground(t.Background()).Background(t.Primary()).Bold(true)
5558 }
5659 var ranges []lipgloss.Range
57− truncatedTitle := ansi.Truncate(c.command.Title, c.width-2, "…")
58− text := titleStyle.Padding(0, 1).Render(truncatedTitle)
60+ truncatedTitle := ansi.Truncate(c.command.Title, c.width, "…")
61+ text := titleStyle.Render(truncatedTitle)
5962 if len(c.matchIndexes) > 0 {
6063 for _, rng := range matchedRanges(c.matchIndexes) {
6164 // ansi.Cut is grapheme and ansi sequence aware, we match against a ansi.Stripped string, but we might still have graphemes.
6265 // all that to say that rng is byte positions, but we need to pass it down to ansi.Cut as char positions.
6366 // so we need to adjust it here:
6467 start, stop := bytePosToVisibleCharPos(text, rng)
65− ranges = append(ranges, lipgloss.NewRange(start+1, stop+2, titleMatchStyle))
68+ ranges = append(ranges, lipgloss.NewRange(start, stop+1, titleMatchStyle))
6669 }
6770 text = lipgloss.StyleRanges(text, ranges...)
6871 }
@@ -148,3 +151,53 @@ func bytePosToVisibleCharPos(str string, rng [2]int) (int, int) {
148151 stop = pos
149152 return start, stop
150153}
154+
155+type ItemSection interface {
156+ util.Model
157+ layout.Sizeable
158+ list.SectionHeader
159+}
160+type itemSectionModel struct {
161+ width int
162+ title string
163+}
164+
165+func NewItemSection(title string) ItemSection {
166+ return &itemSectionModel{
167+ title: title,
168+ }
169+}
170+
171+func (m *itemSectionModel) Init() tea.Cmd {
172+ return nil
173+}
174+
175+func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
176+ return m, nil
177+}
178+
179+func (m *itemSectionModel) View() tea.View {
180+ t := theme.CurrentTheme()
181+ title := ansi.Truncate(m.title, m.width-1, "…")
182+ style := styles.BaseStyle().Padding(1, 0, 0, 0).Width(m.width).Foreground(t.TextMuted()).Bold(true)
183+ if len(title) < m.width {
184+ remainingWidth := m.width - lipgloss.Width(title)
185+ if remainingWidth > 0 {
186+ title += " " + strings.Repeat("─", remainingWidth-1)
187+ }
188+ }
189+ return tea.NewView(style.Render(title))
190+}
191+
192+func (m *itemSectionModel) GetSize() (int, int) {
193+ return m.width, 1
194+}
195+
196+func (m *itemSectionModel) SetSize(width int, height int) tea.Cmd {
197+ m.width = width
198+ return nil
199+}
200+
201+func (m *itemSectionModel) IsSectionHeader() bool {
202+ return true
203+}
internal/tui/components/dialogs/commands/keys.gonew file+1
@@ -0,0 +1 @@
1+package commands
internal/tui/components/dialogs/commands/loader.go+1−3
@@ -162,7 +162,6 @@ func createCommandHandler(id string, content string) func(Command) tea.Cmd {
162162
163163 return util.CmdHandler(CommandRunCustomMsg{
164164 Content: content,
165− Args: nil,
166165 })
167166 }
168167}
@@ -189,7 +188,7 @@ func extractArgNames(content string) []string {
189188
190189func ensureDir(path string) error {
191190 if _, err := os.Stat(path); os.IsNotExist(err) {
192− return os.MkdirAll(path, 0755)
191+ return os.MkdirAll(path, 0o755)
193192 }
194193 return nil
195194}
@@ -200,5 +199,4 @@ func isMarkdownFile(name string) bool {
200199
201200type CommandRunCustomMsg struct {
202201 Content string
203− Args map[string]string
204202}
internal/tui/components/dialogs/dialogs.go+5−10
@@ -23,11 +23,6 @@ type CloseCallback interface {
2323 Close() tea.Cmd
2424}
2525
26−// AbsolutePositionable is an interface for components that can set their position
27−type AbsolutePositionable interface {
28− SetPosition(x, y int)
29−}
30−
3126// OpenDialogMsg is sent to open a new dialog with specified dimensions.
3227type OpenDialogMsg struct {
3328 Model DialogModel
@@ -50,14 +45,14 @@ type dialogCmp struct {
5045 width, height int
5146 dialogs []DialogModel
5247 idMap map[DialogID]int
53− keymap KeyMap
48+ keyMap KeyMap
5449}
5550
5651// NewDialogCmp creates a new dialog manager.
5752func NewDialogCmp() DialogCmp {
5853 return dialogCmp{
5954 dialogs: []DialogModel{},
60− keymap: DefaultKeymap(),
55+ keyMap: DefaultKeyMap(),
6156 idMap: make(map[DialogID]int),
6257 }
6358}
@@ -94,7 +89,7 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9489 }
9590 return d, nil
9691 case tea.KeyPressMsg:
97− if key.Matches(msg, d.keymap.Close) {
92+ if key.Matches(msg, d.keyMap.Close) {
9893 return d, util.CmdHandler(CloseDialogMsg{})
9994 }
10095 }
@@ -114,10 +109,10 @@ func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) {
114109 return d, nil // Do not open a dialog if it's already the topmost one
115110 }
116111 if dialog.ID() == "quit" {
117− return d, nil // Do not open dialogs ontop of quit
112+ return d, nil // Do not open dialogs on top of quit
118113 }
119114 }
120− // if the dialog is already in thel stack make it the last item
115+ // if the dialog is already in the stack make it the last item
121116 if _, ok := d.idMap[msg.Model.ID()]; ok {
122117 existing := d.dialogs[d.idMap[msg.Model.ID()]]
123118 // Reuse the model so we keep the state
internal/tui/components/dialogs/keys.go+1−1
@@ -10,7 +10,7 @@ type KeyMap struct {
1010 Close key.Binding
1111}
1212
13−func DefaultKeymap() KeyMap {
13+func DefaultKeyMap() KeyMap {
1414 return KeyMap{
1515 Close: key.NewBinding(
1616 key.WithKeys("esc"),
internal/tui/tui.go+8−704
@@ -17,8 +17,6 @@ import (
1717 "github.com/opencode-ai/opencode/internal/tui/util"
1818)
1919
20−// type startCompactSessionMsg struct{}
21−
2220type appModel struct {
2321 width, height int
2422 keyMap KeyMap
@@ -32,40 +30,6 @@ type appModel struct {
3230
3331 app *app.App
3432
35− // selectedSession session.Session
36− //
37− // showPermissions bool
38− // permissions dialog.PermissionDialogCmp
39− //
40− // showHelp bool
41− // help dialog.HelpCmp
42− //
43− // showSessionDialog bool
44− // sessionDialog dialog.SessionDialog
45− //
46− // showCommandDialog bool
47− // commandDialog dialog.CommandDialog
48− // commands []dialog.Command
49− //
50− // showModelDialog bool
51− // modelDialog dialog.ModelDialog
52− //
53− // showInitDialog bool
54− // initDialog dialog.InitDialogCmp
55− //
56− // showFilepicker bool
57− // filepicker dialog.FilepickerCmp
58− //
59− // showThemeDialog bool
60− // themeDialog dialog.ThemeDialog
61− //
62− // showMultiArgumentsDialog bool
63− // multiArgumentsDialog dialog.MultiArgumentsDialogCmp
64− //
65− // isCompacting bool
66− // compactingMessage string
67−
68− // NEW DIALOG
6933 dialog dialogs.DialogCmp
7034}
7135
@@ -77,32 +41,6 @@ func (a appModel) Init() tea.Cmd {
7741
7842 cmd = a.status.Init()
7943 cmds = append(cmds, cmd)
80− // cmd = a.help.Init()
81− // cmds = append(cmds, cmd)
82− // cmd = a.sessionDialog.Init()
83− // cmds = append(cmds, cmd)
84− // cmd = a.commandDialog.Init()
85− // cmds = append(cmds, cmd)
86− // cmd = a.modelDialog.Init()
87− // cmds = append(cmds, cmd)
88− // cmd = a.initDialog.Init()
89− // cmds = append(cmds, cmd)
90− // cmd = a.filepicker.Init()
91− // cmds = append(cmds, cmd)
92− // cmd = a.themeDialog.Init()
93− // cmds = append(cmds, cmd)
94−
95− // Check if we should show the init dialog
96− // cmds = append(cmds, func() tea.Msg {
97− // shouldShow, err := config.ShouldShowInitDialog()
98− // if err != nil {
99− // return util.InfoMsg{
100− // Type: util.InfoTypeError,
101− // Msg: "Failed to check init status: " + err.Error(),
102− // }
103− // }
104− // return dialog.ShowInitDialogMsg{Show: shouldShow}
105− // })
10644 return tea.Batch(cmds...)
10745}
10846
@@ -113,56 +51,13 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
11351 switch msg := msg.(type) {
11452 case tea.WindowSizeMsg:
11553 return a, a.handleWindowResize(msg)
116− // TODO: remove when refactor is done
117− // msg.Height -= 1 // Make space for the status bar
118− // a.width, a.height = msg.Width, msg.Height
119− //
120− // s, _ := a.status.Update(msg)
121− // a.status = s.(core.StatusCmp)
122− // updated, cmd := a.pages[a.currentPage].Update(msg)
123− // a.pages[a.currentPage] = updated.(util.Model)
124− // cmds = append(cmds, cmd)
125− //
126− // prm, permCmd := a.permissions.Update(msg)
127− // a.permissions = prm.(dialog.PermissionDialogCmp)
128− // cmds = append(cmds, permCmd)
129− //
130− // help, helpCmd := a.help.Update(msg)
131− // a.help = help.(dialog.HelpCmp)
132− // cmds = append(cmds, helpCmd)
133− //
134− // session, sessionCmd := a.sessionDialog.Update(msg)
135− // a.sessionDialog = session.(dialog.SessionDialog)
136− // cmds = append(cmds, sessionCmd)
137− //
138− // command, commandCmd := a.commandDialog.Update(msg)
139− // a.commandDialog = command.(dialog.CommandDialog)
140− // cmds = append(cmds, commandCmd)
141− //
142− // filepicker, filepickerCmd := a.filepicker.Update(msg)
143− // a.filepicker = filepicker.(dialog.FilepickerCmp)
144− // cmds = append(cmds, filepickerCmd)
145− //
146− // a.initDialog.SetSize(msg.Width, msg.Height)
147− //
148− // if a.showMultiArgumentsDialog {
149− // a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
150− // args, argsCmd := a.multiArgumentsDialog.Update(msg)
151− // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
152− // cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
153− // }
154− //
155− // dialog, cmd := a.dialog.Update(msg)
156− // a.dialog = dialog.(dialogs.DialogCmp)
157− // cmds = append(cmds, cmd)
158− //
159− // return a, tea.Batch(cmds...)
16054
16155 // Dialog messages
16256 case dialogs.OpenDialogMsg, dialogs.CloseDialogMsg:
16357 u, dialogCmd := a.dialog.Update(msg)
16458 a.dialog = u.(dialogs.DialogCmp)
16559 return a, dialogCmd
60+ case commands.ShowArgumentsDialogMsg:
16661
16762 // Page change messages
16863 case page.PageChangeMsg:
@@ -170,398 +65,28 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
17065
17166 // Status Messages
17267 case util.InfoMsg, util.ClearStatusMsg:
173− s, cmd := a.status.Update(msg)
68+ s, statusCmd := a.status.Update(msg)
17469 a.status = s.(core.StatusCmp)
175− cmds = append(cmds, cmd)
70+ cmds = append(cmds, statusCmd)
17671 return a, tea.Batch(cmds...)
72+
17773 // Logs
17874 case pubsub.Event[logging.LogMessage]:
17975 // Send to the status component
180− s, cmd := a.status.Update(msg)
76+ s, statusCmd := a.status.Update(msg)
18177 a.status = s.(core.StatusCmp)
182− cmds = append(cmds, cmd)
78+ cmds = append(cmds, statusCmd)
18379
18480 // If the current page is logs, update the logs view
18581 if a.currentPage == page.LogsPage {
186− updated, cmd := a.pages[a.currentPage].Update(msg)
82+ updated, pageCmd := a.pages[a.currentPage].Update(msg)
18783 a.pages[a.currentPage] = updated.(util.Model)
188− cmds = append(cmds, cmd)
84+ cmds = append(cmds, pageCmd)
18985 }
19086 return a, tea.Batch(cmds...)
191−
192− // // Permission
193− // case pubsub.Event[permission.PermissionRequest]:
194− // a.showPermissions = true
195− // return a, a.permissions.SetPermissions(msg.Payload)
196− // case dialog.PermissionResponseMsg:
197− // var cmd tea.Cmd
198− // switch msg.Action {
199− // case dialog.PermissionAllow:
200− // a.app.Permissions.Grant(msg.Permission)
201− // case dialog.PermissionAllowForSession:
202− // a.app.Permissions.GrantPersistant(msg.Permission)
203− // case dialog.PermissionDeny:
204− // a.app.Permissions.Deny(msg.Permission)
205− // }
206− // a.showPermissions = false
207− // return a, cmd
208− //
209− // // Theme changed
210− // case dialog.ThemeChangedMsg:
211− // updated, cmd := a.pages[a.currentPage].Update(msg)
212− // a.pages[a.currentPage] = updated.(util.Model)
213− // a.showThemeDialog = false
214− // return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
215− //
216− // case dialog.CloseSessionDialogMsg:
217− // a.showSessionDialog = false
218− // return a, nil
219− //
220− // case dialog.CloseCommandDialogMsg:
221− // a.showCommandDialog = false
222− // return a, nil
223− //
224− // case startCompactSessionMsg:
225− // // Start compacting the current session
226− // a.isCompacting = true
227− // a.compactingMessage = "Starting summarization..."
228− //
229− // if a.selectedSession.ID == "" {
230− // a.isCompacting = false
231− // return a, util.ReportWarn("No active session to summarize")
232− // }
233− //
234− // // Start the summarization process
235− // return a, func() tea.Msg {
236− // ctx := context.Background()
237− // a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID)
238− // return nil
239− // }
240− //
241− // case pubsub.Event[agent.AgentEvent]:
242− // payload := msg.Payload
243− // if payload.Error != nil {
244− // a.isCompacting = false
245− // return a, util.ReportError(payload.Error)
246− // }
247− //
248− // a.compactingMessage = payload.Progress
249− //
250− // if payload.Done && payload.Type == agent.AgentEventTypeSummarize {
251− // a.isCompacting = false
252− // return a, util.ReportInfo("Session summarization complete")
253− // } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" {
254− // model := a.app.CoderAgent.Model()
255− // contextWindow := model.ContextWindow
256− // tokens := a.selectedSession.CompletionTokens + a.selectedSession.PromptTokens
257− // if (tokens >= int64(float64(contextWindow)*0.95)) && config.Get().AutoCompact {
258− // return a, util.CmdHandler(startCompactSessionMsg{})
259− // }
260− // }
261− // // Continue listening for events
262− // return a, nil
263− //
264− // case dialog.CloseThemeDialogMsg:
265− // a.showThemeDialog = false
266− // return a, nil
267− //
268− // case dialog.CloseModelDialogMsg:
269− // a.showModelDialog = false
270− // return a, nil
271− //
272− // case dialog.ModelSelectedMsg:
273− // a.showModelDialog = false
274− //
275− // model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID)
276− // if err != nil {
277− // return a, util.ReportError(err)
278− // }
279− //
280− // return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
281− //
282− // case dialog.ShowInitDialogMsg:
283− // a.showInitDialog = msg.Show
284− // return a, nil
285− //
286− // case dialog.CloseInitDialogMsg:
287− // a.showInitDialog = false
288− // if msg.Initialize {
289− // // Run the initialization command
290− // for _, cmd := range a.commands {
291− // if cmd.ID == "init" {
292− // // Mark the project as initialized
293− // if err := config.MarkProjectInitialized(); err != nil {
294− // return a, util.ReportError(err)
295− // }
296− // return a, cmd.Handler(cmd)
297− // }
298− // }
299− // } else {
300− // // Mark the project as initialized without running the command
301− // if err := config.MarkProjectInitialized(); err != nil {
302− // return a, util.ReportError(err)
303− // }
304− // }
305− // return a, nil
306− //
307− // case chat.SessionSelectedMsg:
308− // a.selectedSession = msg
309− // a.sessionDialog.SetSelectedSession(msg.ID)
310− //
311− // case pubsub.Event[session.Session]:
312− // if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == a.selectedSession.ID {
313− // a.selectedSession = msg.Payload
314− // }
315− // case dialog.SessionSelectedMsg:
316− // a.showSessionDialog = false
317− // if a.currentPage == page.ChatPage {
318− // return a, util.CmdHandler(chat.SessionSelectedMsg(msg.Session))
319− // }
320− // return a, nil
321− //
322− // case dialog.CommandSelectedMsg:
323− // a.showCommandDialog = false
324− // // Execute the command handler if available
325− // if msg.Command.Handler != nil {
326− // return a, msg.Command.Handler(msg.Command)
327− // }
328− // return a, util.ReportInfo("Command selected: " + msg.Command.Title)
329− //
330− // case dialog.ShowMultiArgumentsDialogMsg:
331− // // Show multi-arguments dialog
332− // a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
333− // a.showMultiArgumentsDialog = true
334− // return a, a.multiArgumentsDialog.Init()
335− //
336− // case dialog.CloseMultiArgumentsDialogMsg:
337− // // Close multi-arguments dialog
338− // a.showMultiArgumentsDialog = false
339− //
340− // // If submitted, replace all named arguments and run the command
341− // if msg.Submit {
342− // content := msg.Content
343− //
344− // // Replace each named argument with its value
345− // for name, value := range msg.Args {
346− // placeholder := "$" + name
347− // content = strings.ReplaceAll(content, placeholder, value)
348− // }
349− //
350− // // Execute the command with arguments
351− // return a, util.CmdHandler(dialog.CommandRunCustomMsg{
352− // Content: content,
353− // Args: msg.Args,
354− // })
355− // }
356− // return a, nil
357− //
35887 case tea.KeyPressMsg:
35988 return a, a.handleKeyPressMsg(msg)
360− // if a.dialog.HasDialogs() {
361− // u, dialogCmd := a.dialog.Update(msg)
362− // a.dialog = u.(dialogs.DialogCmp)
363− // return a, dialogCmd
364− // }
365− // // If multi-arguments dialog is open, let it handle the key press first
366− // if a.showMultiArgumentsDialog {
367− // args, cmd := a.multiArgumentsDialog.Update(msg)
368− // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
369− // return a, cmd
370− // }
371− //
372− // switch {
373− // case key.Matches(msg, keys.Quit):
374− // // TODO: fix this after testing
375− // // a.showQuit = !a.showQuit
376− // // if a.showHelp {
377− // // a.showHelp = false
378− // // }
379− // // if a.showSessionDialog {
380− // // a.showSessionDialog = false
381− // // }
382− // // if a.showCommandDialog {
383− // // a.showCommandDialog = false
384− // // }
385− // // if a.showFilepicker {
386− // // a.showFilepicker = false
387− // // a.filepicker.ToggleFilepicker(a.showFilepicker)
388− // // }
389− // // if a.showModelDialog {
390− // // a.showModelDialog = false
391− // // }
392− // // if a.showMultiArgumentsDialog {
393− // // a.showMultiArgumentsDialog = false
394− // // }
395− // return a, util.CmdHandler(dialogs.OpenDialogMsg{
396− // Model: quit.NewQuitDialog(),
397− // })
398− // case key.Matches(msg, keys.SwitchSession):
399− // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showCommandDialog {
400− // // Load sessions and show the dialog
401− // sessions, err := a.app.Sessions.List(context.Background())
402− // if err != nil {
403− // return a, util.ReportError(err)
404− // }
405− // if len(sessions) == 0 {
406− // return a, util.ReportWarn("No sessions available")
407− // }
408− // a.sessionDialog.SetSessions(sessions)
409− // a.showSessionDialog = true
410− // return a, nil
411− // }
412− // return a, nil
413− // case key.Matches(msg, keys.Commands):
414− // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker {
415− // // Show commands dialog
416− // if len(a.commands) == 0 {
417− // return a, util.ReportWarn("No commands available")
418− // }
419− // a.commandDialog.SetCommands(a.commands)
420− // a.showCommandDialog = true
421− // return a, nil
422− // }
423− // return a, util.CmdHandler(dialogs.OpenDialogMsg{
424− // Model: commands.NewCommandDialog(),
425− // })
426− // case key.Matches(msg, keys.Models):
427− // if a.showModelDialog {
428− // a.showModelDialog = false
429− // return a, nil
430− // }
431− // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
432− // a.showModelDialog = true
433− // return a, nil
434− // }
435− // return a, nil
436− // case key.Matches(msg, keys.SwitchTheme):
437− // if !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
438− // // Show theme switcher dialog
439− // a.showThemeDialog = true
440− // // Theme list is dynamically loaded by the dialog component
441− // return a, a.themeDialog.Init()
442− // }
443− // return a, nil
444− // case key.Matches(msg, returnKey) || key.Matches(msg):
445− // if msg.String() == quitKey {
446− // if a.currentPage == page.LogsPage {
447− // return a, a.moveToPage(page.ChatPage)
448− // }
449− // } else if !a.filepicker.IsCWDFocused() {
450− // if a.showHelp {
451− // a.showHelp = !a.showHelp
452− // return a, nil
453− // }
454− // if a.showInitDialog {
455− // a.showInitDialog = false
456− // // Mark the project as initialized without running the command
457− // if err := config.MarkProjectInitialized(); err != nil {
458− // return a, util.ReportError(err)
459− // }
460− // return a, nil
461− // }
462− // if a.showFilepicker {
463− // a.showFilepicker = false
464− // a.filepicker.ToggleFilepicker(a.showFilepicker)
465− // return a, nil
466− // }
467− // if a.currentPage == page.LogsPage {
468− // return a, a.moveToPage(page.ChatPage)
469− // }
470− // }
471− // case key.Matches(msg, keys.Logs):
472− // return a, a.moveToPage(page.LogsPage)
473− // case key.Matches(msg, keys.Help):
474− // a.showHelp = !a.showHelp
475− // return a, nil
476− // case key.Matches(msg, helpEsc):
477− // if a.app.CoderAgent.IsBusy() {
478− // a.showHelp = !a.showHelp
479− // return a, nil
480− // }
481− // case key.Matches(msg, keys.Filepicker):
482− // a.showFilepicker = !a.showFilepicker
483− // a.filepicker.ToggleFilepicker(a.showFilepicker)
484− // return a, nil
485− // }
486− // default:
487− // u, dialogCmd := a.dialog.Update(msg)
488− // a.dialog = u.(dialogs.DialogCmp)
489− // cmds = append(cmds, dialogCmd)
490− // f, filepickerCmd := a.filepicker.Update(msg)
491− // a.filepicker = f.(dialog.FilepickerCmp)
492− // cmds = append(cmds, filepickerCmd)
493− // }
494−
495− // if a.showFilepicker {
496− // f, filepickerCmd := a.filepicker.Update(msg)
497− // a.filepicker = f.(dialog.FilepickerCmp)
498− // cmds = append(cmds, filepickerCmd)
499− // // Only block key messages send all other messages down
500− // if _, ok := msg.(tea.KeyPressMsg); ok {
501− // return a, tea.Batch(cmds...)
502− // }
503− // }
504− //
505− // if a.showPermissions {
506− // d, permissionsCmd := a.permissions.Update(msg)
507− // a.permissions = d.(dialog.PermissionDialogCmp)
508− // cmds = append(cmds, permissionsCmd)
509− // // Only block key messages send all other messages down
510− // if _, ok := msg.(tea.KeyPressMsg); ok {
511− // return a, tea.Batch(cmds...)
512− // }
513− // }
514− //
515− // if a.showSessionDialog {
516− // d, sessionCmd := a.sessionDialog.Update(msg)
517− // a.sessionDialog = d.(dialog.SessionDialog)
518− // cmds = append(cmds, sessionCmd)
519− // // Only block key messages send all other messages down
520− // if _, ok := msg.(tea.KeyPressMsg); ok {
521− // return a, tea.Batch(cmds...)
522− // }
523− // }
524− //
525− // if a.showCommandDialog {
526− // d, commandCmd := a.commandDialog.Update(msg)
527− // a.commandDialog = d.(dialog.CommandDialog)
528− // cmds = append(cmds, commandCmd)
529− // // Only block key messages send all other messages down
530− // if _, ok := msg.(tea.KeyPressMsg); ok {
531− // return a, tea.Batch(cmds...)
532− // }
533− // }
534− //
535− // if a.showModelDialog {
536− // d, modelCmd := a.modelDialog.Update(msg)
537− // a.modelDialog = d.(dialog.ModelDialog)
538− // cmds = append(cmds, modelCmd)
539− // // Only block key messages send all other messages down
540− // if _, ok := msg.(tea.KeyPressMsg); ok {
541− // return a, tea.Batch(cmds...)
542− // }
543− // }
544− //
545− // if a.showInitDialog {
546− // d, initCmd := a.initDialog.Update(msg)
547− // a.initDialog = d.(dialog.InitDialogCmp)
548− // cmds = append(cmds, initCmd)
549− // // Only block key messages send all other messages down
550− // if _, ok := msg.(tea.KeyPressMsg); ok {
551− // return a, tea.Batch(cmds...)
552− // }
553− // }
554− //
555− // if a.showThemeDialog {
556− // d, themeCmd := a.themeDialog.Update(msg)
557− // a.themeDialog = d.(dialog.ThemeDialog)
558− // cmds = append(cmds, themeCmd)
559− // // Only block key messages send all other messages down
560− // if _, ok := msg.(tea.KeyPressMsg); ok {
561− // return a, tea.Batch(cmds...)
562− // }
56389 }
564− //
56590 s, _ := a.status.Update(msg)
56691 a.status = s.(core.StatusCmp)
56792 updated, cmd := a.pages[a.currentPage].Update(msg)
@@ -659,177 +184,6 @@ func (a *appModel) View() tea.View {
659184
660185 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
661186
662− // if a.showPermissions {
663− // overlay := a.permissions.View().String()
664− // row := lipgloss.Height(appView) / 2
665− // row -= lipgloss.Height(overlay) / 2
666− // col := lipgloss.Width(appView) / 2
667− // col -= lipgloss.Width(overlay) / 2
668− // appView = layout.PlaceOverlay(
669− // col,
670− // row,
671− // overlay,
672− // appView,
673− // true,
674− // )
675− // }
676− //
677− // if a.showFilepicker {
678− // overlay := a.filepicker.View().String()
679− // row := lipgloss.Height(appView) / 2
680− // row -= lipgloss.Height(overlay) / 2
681− // col := lipgloss.Width(appView) / 2
682− // col -= lipgloss.Width(overlay) / 2
683− // appView = layout.PlaceOverlay(
684− // col,
685− // row,
686− // overlay,
687− // appView,
688− // true,
689− // )
690− // }
691− //
692− // // Show compacting status overlay
693− // if a.isCompacting {
694− // t := theme.CurrentTheme()
695− // style := lipgloss.NewStyle().
696− // Border(lipgloss.RoundedBorder()).
697− // BorderForeground(t.BorderFocused()).
698− // BorderBackground(t.Background()).
699− // Padding(1, 2).
700− // Background(t.Background()).
701− // Foreground(t.Text())
702− //
703− // overlay := style.Render("Summarizing\n" + a.compactingMessage)
704− // row := lipgloss.Height(appView) / 2
705− // row -= lipgloss.Height(overlay) / 2
706− // col := lipgloss.Width(appView) / 2
707− // col -= lipgloss.Width(overlay) / 2
708− // appView = layout.PlaceOverlay(
709− // col,
710− // row,
711− // overlay,
712− // appView,
713− // true,
714− // )
715− // }
716− //
717− // if a.showHelp {
718− // bindings := layout.KeyMapToSlice(a.keymap)
719− // if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
720− // bindings = append(bindings, p.BindingKeys()...)
721− // }
722− // if a.showPermissions {
723− // bindings = append(bindings, a.permissions.BindingKeys()...)
724− // }
725− // if a.currentPage == page.LogsPage {
726− // // bindings = append(bindings, logsKeyReturnKey)
727− // }
728− // if !a.app.CoderAgent.IsBusy() {
729− // // bindings = append(bindings, helpEsc)
730− // }
731− //
732− // a.help.SetBindings(bindings)
733− //
734− // overlay := a.help.View().String()
735− // row := lipgloss.Height(appView) / 2
736− // row -= lipgloss.Height(overlay) / 2
737− // col := lipgloss.Width(appView) / 2
738− // col -= lipgloss.Width(overlay) / 2
739− // appView = layout.PlaceOverlay(
740− // col,
741− // row,
742− // overlay,
743− // appView,
744− // true,
745− // )
746− // }
747− //
748− // if a.showSessionDialog {
749− // overlay := a.sessionDialog.View().String()
750− // row := lipgloss.Height(appView) / 2
751− // row -= lipgloss.Height(overlay) / 2
752− // col := lipgloss.Width(appView) / 2
753− // col -= lipgloss.Width(overlay) / 2
754− // appView = layout.PlaceOverlay(
755− // col,
756− // row,
757− // overlay,
758− // appView,
759− // true,
760− // )
761− // }
762− //
763− // if a.showModelDialog {
764− // overlay := a.modelDialog.View().String()
765− // row := lipgloss.Height(appView) / 2
766− // row -= lipgloss.Height(overlay) / 2
767− // col := lipgloss.Width(appView) / 2
768− // col -= lipgloss.Width(overlay) / 2
769− // appView = layout.PlaceOverlay(
770− // col,
771− // row,
772− // overlay,
773− // appView,
774− // true,
775− // )
776− // }
777− //
778− // if a.showCommandDialog {
779− // overlay := a.commandDialog.View().String()
780− // row := lipgloss.Height(appView) / 2
781− // row -= lipgloss.Height(overlay) / 2
782− // col := lipgloss.Width(appView) / 2
783− // col -= lipgloss.Width(overlay) / 2
784− // appView = layout.PlaceOverlay(
785− // col,
786− // row,
787− // overlay,
788− // appView,
789− // true,
790− // )
791− // }
792− //
793− // if a.showInitDialog {
794− // overlay := a.initDialog.View()
795− // appView = layout.PlaceOverlay(
796− // a.width/2-lipgloss.Width(overlay)/2,
797− // a.height/2-lipgloss.Height(overlay)/2,
798− // overlay,
799− // appView,
800− // true,
801− // )
802− // }
803− //
804− // if a.showThemeDialog {
805− // overlay := a.themeDialog.View().String()
806− // row := lipgloss.Height(appView) / 2
807− // row -= lipgloss.Height(overlay) / 2
808− // col := lipgloss.Width(appView) / 2
809− // col -= lipgloss.Width(overlay) / 2
810− // appView = layout.PlaceOverlay(
811− // col,
812− // row,
813− // overlay,
814− // appView,
815− // true,
816− // )
817− // }
818− //
819− // if a.showMultiArgumentsDialog {
820− // overlay := a.multiArgumentsDialog.View()
821− // row := lipgloss.Height(appView) / 2
822− // row -= lipgloss.Height(overlay) / 2
823− // col := lipgloss.Width(appView) / 2
824− // col -= lipgloss.Width(overlay) / 2
825− // appView = layout.PlaceOverlay(
826− // col,
827− // row,
828− // overlay,
829− // appView,
830− // true,
831− // )
832− // }
833187 t := theme.CurrentTheme()
834188 if a.dialog.HasDialogs() {
835189 layers := append(
@@ -863,63 +217,13 @@ func New(app *app.App) tea.Model {
863217 loadedPages: make(map[page.PageID]bool),
864218 keyMap: DefaultKeyMap(),
865219
866− // help: dialog.NewHelpCmp(),
867− // sessionDialog: dialog.NewSessionDialogCmp(),
868− // commandDialog: dialog.NewCommandDialogCmp(),
869− // modelDialog: dialog.NewModelDialogCmp(),
870− // permissions: dialog.NewPermissionDialogCmp(),
871− // initDialog: dialog.NewInitDialogCmp(),
872− // themeDialog: dialog.NewThemeDialogCmp(),
873− // commands: []dialog.Command{},
874220 pages: map[page.PageID]util.Model{
875221 page.ChatPage: page.NewChatPage(app),
876222 page.LogsPage: page.NewLogsPage(),
877223 },
878− // filepicker: dialog.NewFilepickerCmp(app),
879224
880− // New dialog
881225 dialog: dialogs.NewDialogCmp(),
882226 }
883227
884− // model.RegisterCommand(dialog.Command{
885− // ID: "init",
886− // Title: "Initialize Project",
887− // Description: "Create/Update the OpenCode.md memory file",
888− // Handler: func(cmd dialog.Command) tea.Cmd {
889− // prompt := `Please analyze this codebase and create a OpenCode.md file containing:
890− // 1. Build/lint/test commands - especially for running a single test
891− // 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
892− //
893− // 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.
894− // If there's already a opencode.md, improve it.
895− // If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
896− // return tea.Batch(
897− // util.CmdHandler(chat.SendMsg{
898− // Text: prompt,
899− // }),
900− // )
901− // },
902− // })
903− //
904− // model.RegisterCommand(dialog.Command{
905− // ID: "compact",
906− // Title: "Compact Session",
907− // Description: "Summarize the current session and create a new one with the summary",
908− // Handler: func(cmd dialog.Command) tea.Cmd {
909− // return func() tea.Msg {
910− // return startCompactSessionMsg{}
911− // }
912− // },
913− // })
914− // // Load custom commands
915− // customCommands, err := dialog.LoadCustomCommands()
916− // if err != nil {
917− // logging.Warn("Failed to load custom commands", "error", err)
918− // } else {
919− // for _, cmd := range customCommands {
920− // model.RegisterCommand(cmd)
921− // }
922− // }
923−
924228 return model
925229}