@@ -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
+}
@@ -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
}