Detailed changes
@@ -34,44 +34,8 @@ const (
ProviderMock ModelProvider = "__mock"
)
-// Providers in order of popularity
-var ProviderPopularity = map[ModelProvider]int{
- ProviderAnthropic: 1,
- ProviderOpenAI: 2,
- ProviderGemini: 3,
- ProviderGROQ: 4,
- ProviderOpenRouter: 5,
- ProviderBedrock: 6,
- ProviderAzure: 7,
- ProviderVertexAI: 8,
-}
-
var SupportedModels = map[ModelID]Model{
- //
- // // GEMINI
- // GEMINI25: {
- // ID: GEMINI25,
- // Name: "Gemini 2.5 Pro",
- // Provider: ProviderGemini,
- // APIModel: "gemini-2.5-pro-exp-03-25",
- // CostPer1MIn: 0,
- // CostPer1MInCached: 0,
- // CostPer1MOutCached: 0,
- // CostPer1MOut: 0,
- // },
- //
- // GRMINI20Flash: {
- // ID: GRMINI20Flash,
- // Name: "Gemini 2.0 Flash",
- // Provider: ProviderGemini,
- // APIModel: "gemini-2.0-flash",
- // CostPer1MIn: 0.1,
- // CostPer1MInCached: 0,
- // CostPer1MOutCached: 0.025,
- // CostPer1MOut: 0.4,
- // },
- //
- // // Bedrock
+ // Bedrock
BedrockClaude37Sonnet: {
ID: BedrockClaude37Sonnet,
Name: "Bedrock: Claude 3.7 Sonnet",
@@ -12,10 +12,11 @@ import (
func Section(text string, width int) string {
t := styles.CurrentTheme()
char := "─"
- length := len(text) + 1
+ length := lipgloss.Width(text) + 1
remainingWidth := width - length
+ lineStyle := t.S().Base.Foreground(t.Border)
if remainingWidth > 0 {
- text = text + " " + t.S().Base.Foreground(t.Border).Render(strings.Repeat(char, remainingWidth))
+ text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth))
}
return text
}
@@ -23,7 +24,7 @@ func Section(text string, width int) string {
func Title(title string, width int) string {
t := styles.CurrentTheme()
char := "╱"
- length := len(title) + 1
+ length := lipgloss.Width(title) + 1
remainingWidth := width - length
lineStyle := t.S().Base.Foreground(t.Primary)
titleStyle := t.S().Base.Foreground(t.Primary)
@@ -133,11 +133,13 @@ type model struct {
allItems []util.Model // The actual list items
gapSize int // Number of empty lines between items
padding []int // Padding around the list content
+ wrapNavigation bool // Whether to wrap navigation at the ends
filterable bool // Whether items can be filtered
filterPlaceholder string // Placeholder text for filter input
filteredItems []util.Model // Filtered items based on current search
input textinput.Model // Input field for filtering items
+ inputStyle lipgloss.Style // Style for the input field
hideFilterInput bool // Whether to hide the filter input field
currentSearch string // Current search term for filtering
}
@@ -204,10 +206,26 @@ func WithFilterPlaceholder(placeholder string) listOptions {
}
}
+// WithInputStyle sets the style for the filter input field.
+func WithInputStyle(style lipgloss.Style) listOptions {
+ return func(m *model) {
+ m.inputStyle = style
+ }
+}
+
+// WithWrapNavigation enables wrapping navigation at the ends of the list.
+func WithWrapNavigation(wrap bool) listOptions {
+ return func(m *model) {
+ m.wrapNavigation = wrap
+ }
+}
+
// New creates a new list model with the specified options.
// The list starts with no items selected and requires SetItems to be called
// or items to be provided via WithItems option.
func New(opts ...listOptions) ListModel {
+ t := styles.CurrentTheme()
+
m := &model{
help: help.New(),
keyMap: DefaultKeyMap(),
@@ -218,6 +236,7 @@ func New(opts ...listOptions) ListModel {
padding: []int{},
selectionState: selectionState{selectedIndex: NoSelection},
filterPlaceholder: "Type to filter...",
+ inputStyle: t.S().Base.Padding(0, 1, 1, 1),
}
for _, opt := range opts {
opt(m)
@@ -281,7 +300,7 @@ func (m *model) View() tea.View {
if m.filterable && !m.hideFilterInput {
content = lipgloss.JoinVertical(
lipgloss.Left,
- m.inputStyle().Render(m.input.View()),
+ m.inputStyle.Render(m.input.View()),
content,
)
}
@@ -400,7 +419,7 @@ func (m *model) renderVisibleForward() {
renderer := &forwardRenderer{
model: m,
start: 0,
- cutoff: m.viewState.offset + m.listHeight(),
+ cutoff: m.viewState.offset + m.listHeight() + m.listHeight()/2, // We render a bit more so we make sure we have smooth movementsd
items: m.filteredItems,
realIdx: m.renderState.lastIndex,
}
@@ -420,7 +439,7 @@ func (m *model) renderVisibleReverse() {
renderer := &reverseRenderer{
model: m,
start: 0,
- cutoff: m.viewState.offset + m.listHeight(),
+ cutoff: m.viewState.offset + m.listHeight() + m.listHeight()/2,
items: m.filteredItems,
realIdx: m.renderState.lastIndex,
}
@@ -567,6 +586,10 @@ func (r *reverseRenderer) renderItemLines(item util.Model) []string {
// 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 == m.findFirstSelectableItem() && m.wrapNavigation {
+ // If at the beginning and wrapping is enabled, go to the last item
+ return m.goToBottom()
+ }
if m.selectionState.selectedIndex <= 0 {
return nil
}
@@ -580,8 +603,9 @@ func (m *model) selectPreviousItem() tea.Cmd {
}
// If we went past the beginning, stay at the first non-header item
- if m.selectionState.selectedIndex < 0 {
- m.selectionState.selectedIndex = m.findFirstSelectableItem()
+ if m.selectionState.selectedIndex <= 0 {
+ cmds = append(cmds, m.goToTop()) // Ensure we scroll to the top if needed
+ return tea.Batch(cmds...)
}
cmds = append(cmds, m.focusSelected())
@@ -593,6 +617,10 @@ func (m *model) selectPreviousItem() tea.Cmd {
// 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 >= m.findLastSelectableItem() && m.wrapNavigation {
+ // If at the end and wrapping is enabled, go to the first item
+ return m.goToTop()
+ }
if m.selectionState.selectedIndex >= len(m.filteredItems)-1 || m.selectionState.selectedIndex < 0 {
return nil
}
@@ -1008,6 +1036,9 @@ func (m *model) listHeight() int {
case 3, 4:
height -= m.padding[0] + m.padding[2]
}
+ if m.filterable && !m.hideFilterInput {
+ height -= lipgloss.Height(m.inputStyle.Render("dummy"))
+ }
return max(0, height)
}
@@ -1107,10 +1138,6 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
return tea.Batch(cmds...)
}
-func (c *model) inputStyle() lipgloss.Style {
- return styles.BaseStyle().Padding(0, 1, 1, 1)
-}
-
// section represents a group of items under a section header.
type section struct {
header SectionHeader
@@ -15,7 +15,6 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/opencode-ai/opencode/internal/app"
- "github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/tui/image"
@@ -222,11 +221,11 @@ func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
- modeInfo := GetSelectedModel(config.Get())
- if !modeInfo.SupportsAttachments {
- logging.ErrorPersist(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
- return f, nil
- }
+ // modeInfo := GetSelectedModel(config.Get())
+ // if !modeInfo.SupportsAttachments {
+ // logging.ErrorPersist(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
+ // return f, nil
+ // }
selectedFilePath := f.selectedFile
if !isExtSupported(selectedFilePath) {
@@ -1,374 +0,0 @@
-package dialog
-
-import (
- "fmt"
- "slices"
- "strings"
-
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/opencode-ai/opencode/internal/config"
- "github.com/opencode-ai/opencode/internal/llm/models"
- "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"
-)
-
-const (
- numVisibleModels = 10
- maxDialogWidth = 40
-)
-
-// ModelSelectedMsg is sent when a model is selected
-type ModelSelectedMsg struct {
- Model models.Model
-}
-
-// CloseModelDialogMsg is sent when a model is selected
-type CloseModelDialogMsg struct{}
-
-// ModelDialog interface for the model selection dialog
-type ModelDialog interface {
- util.Model
- layout.Bindings
-}
-
-type modelDialogCmp struct {
- models []models.Model
- provider models.ModelProvider
- availableProviders []models.ModelProvider
-
- selectedIdx int
- width int
- height int
- scrollOffset int
- hScrollOffset int
- hScrollPossible bool
-}
-
-type modelKeyMap struct {
- Up key.Binding
- Down key.Binding
- Left key.Binding
- Right key.Binding
- Enter key.Binding
- Escape key.Binding
- J key.Binding
- K key.Binding
- H key.Binding
- L key.Binding
-}
-
-var modelKeys = modelKeyMap{
- Up: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "previous model"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "next model"),
- ),
- Left: key.NewBinding(
- key.WithKeys("left"),
- key.WithHelp("←", "scroll left"),
- ),
- Right: key.NewBinding(
- key.WithKeys("right"),
- key.WithHelp("→", "scroll right"),
- ),
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select model"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next model"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous model"),
- ),
- H: key.NewBinding(
- key.WithKeys("h"),
- key.WithHelp("h", "scroll left"),
- ),
- L: key.NewBinding(
- key.WithKeys("l"),
- key.WithHelp("l", "scroll right"),
- ),
-}
-
-func (m *modelDialogCmp) Init() tea.Cmd {
- m.setupModels()
- return nil
-}
-
-func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.KeyPressMsg:
- switch {
- case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
- m.moveSelectionUp()
- case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
- m.moveSelectionDown()
- case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
- if m.hScrollPossible {
- m.switchProvider(-1)
- }
- case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
- if m.hScrollPossible {
- m.switchProvider(1)
- }
- case key.Matches(msg, modelKeys.Enter):
- util.ReportInfo(fmt.Sprintf("selected model: %s", m.models[m.selectedIdx].Name))
- return m, util.CmdHandler(ModelSelectedMsg{Model: m.models[m.selectedIdx]})
- case key.Matches(msg, modelKeys.Escape):
- return m, util.CmdHandler(CloseModelDialogMsg{})
- }
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- }
-
- return m, nil
-}
-
-// moveSelectionUp moves the selection up or wraps to bottom
-func (m *modelDialogCmp) moveSelectionUp() {
- if m.selectedIdx > 0 {
- m.selectedIdx--
- } else {
- m.selectedIdx = len(m.models) - 1
- m.scrollOffset = max(0, len(m.models)-numVisibleModels)
- }
-
- // Keep selection visible
- if m.selectedIdx < m.scrollOffset {
- m.scrollOffset = m.selectedIdx
- }
-}
-
-// moveSelectionDown moves the selection down or wraps to top
-func (m *modelDialogCmp) moveSelectionDown() {
- if m.selectedIdx < len(m.models)-1 {
- m.selectedIdx++
- } else {
- m.selectedIdx = 0
- m.scrollOffset = 0
- }
-
- // Keep selection visible
- if m.selectedIdx >= m.scrollOffset+numVisibleModels {
- m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
- }
-}
-
-func (m *modelDialogCmp) switchProvider(offset int) {
- newOffset := m.hScrollOffset + offset
-
- // Ensure we stay within bounds
- if newOffset < 0 {
- newOffset = len(m.availableProviders) - 1
- }
- if newOffset >= len(m.availableProviders) {
- newOffset = 0
- }
-
- m.hScrollOffset = newOffset
- m.provider = m.availableProviders[m.hScrollOffset]
- m.setupModelsForProvider(m.provider)
-}
-
-func (m *modelDialogCmp) View() tea.View {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- // Capitalize first letter of provider name
- providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
- title := baseStyle.
- Foreground(t.Primary()).
- Bold(true).
- Width(maxDialogWidth).
- Padding(0, 0, 1).
- Render(fmt.Sprintf("Select %s Model", providerName))
-
- // Render visible models
- endIdx := min(m.scrollOffset+numVisibleModels, len(m.models))
- modelItems := make([]string, 0, endIdx-m.scrollOffset)
-
- for i := m.scrollOffset; i < endIdx; i++ {
- itemStyle := baseStyle.Width(maxDialogWidth)
- if i == m.selectedIdx {
- itemStyle = itemStyle.Background(t.Primary()).
- Foreground(t.Background()).Bold(true)
- }
- modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
- }
-
- scrollIndicator := m.getScrollIndicators(maxDialogWidth)
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
- scrollIndicator,
- )
-
- return tea.NewView(
- baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content),
- )
-}
-
-func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
- var indicator string
-
- if len(m.models) > numVisibleModels {
- if m.scrollOffset > 0 {
- indicator += "↑ "
- }
- if m.scrollOffset+numVisibleModels < len(m.models) {
- indicator += "↓ "
- }
- }
-
- if m.hScrollPossible {
- if m.hScrollOffset > 0 {
- indicator = "← " + indicator
- }
- if m.hScrollOffset < len(m.availableProviders)-1 {
- indicator += "→"
- }
- }
-
- if indicator == "" {
- return ""
- }
-
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- return baseStyle.
- Foreground(t.Primary()).
- Width(maxWidth).
- Align(lipgloss.Right).
- Bold(true).
- Render(indicator)
-}
-
-func (m *modelDialogCmp) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(modelKeys)
-}
-
-func (m *modelDialogCmp) setupModels() {
- cfg := config.Get()
- modelInfo := GetSelectedModel(cfg)
- m.availableProviders = getEnabledProviders(cfg)
- m.hScrollPossible = len(m.availableProviders) > 1
-
- m.provider = modelInfo.Provider
- m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
-
- m.setupModelsForProvider(m.provider)
-}
-
-func GetSelectedModel(cfg *config.Config) models.Model {
- agentCfg := cfg.Agents[config.AgentCoder]
- selectedModelId := agentCfg.Model
- return models.SupportedModels[selectedModelId]
-}
-
-func getEnabledProviders(cfg *config.Config) []models.ModelProvider {
- var providers []models.ModelProvider
- for providerId, provider := range cfg.Providers {
- if !provider.Disabled {
- providers = append(providers, providerId)
- }
- }
-
- // Sort by provider popularity
- slices.SortFunc(providers, func(a, b models.ModelProvider) int {
- rA := models.ProviderPopularity[a]
- rB := models.ProviderPopularity[b]
-
- // models not included in popularity ranking default to last
- if rA == 0 {
- rA = 999
- }
- if rB == 0 {
- rB = 999
- }
- return rA - rB
- })
- return providers
-}
-
-// findProviderIndex returns the index of the provider in the list, or -1 if not found
-func findProviderIndex(providers []models.ModelProvider, provider models.ModelProvider) int {
- for i, p := range providers {
- if p == provider {
- return i
- }
- }
- return -1
-}
-
-func (m *modelDialogCmp) setupModelsForProvider(provider models.ModelProvider) {
- cfg := config.Get()
- agentCfg := cfg.Agents[config.AgentCoder]
- selectedModelId := agentCfg.Model
-
- m.provider = provider
- m.models = getModelsForProvider(provider)
- m.selectedIdx = 0
- m.scrollOffset = 0
-
- // Try to select the current model if it belongs to this provider
- if provider == models.SupportedModels[selectedModelId].Provider {
- for i, model := range m.models {
- if model.ID == selectedModelId {
- m.selectedIdx = i
- // Adjust scroll position to keep selected model visible
- if m.selectedIdx >= numVisibleModels {
- m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
- }
- break
- }
- }
- }
-}
-
-func getModelsForProvider(provider models.ModelProvider) []models.Model {
- var providerModels []models.Model
- for _, model := range models.SupportedModels {
- if model.Provider == provider {
- providerModels = append(providerModels, model)
- }
- }
-
- // reverse alphabetical order (if llm naming was consistent latest would appear first)
- slices.SortFunc(providerModels, func(a, b models.Model) int {
- if a.Name > b.Name {
- return -1
- } else if a.Name < b.Name {
- return 1
- }
- return 0
- })
-
- return providerModels
-}
-
-func NewModelDialogCmp() ModelDialog {
- return &modelDialogCmp{}
-}
@@ -1,233 +0,0 @@
-package dialog
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/opencode-ai/opencode/internal/session"
- "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"
-)
-
-// SessionSelectedMsg is sent when a session is selected
-type SessionSelectedMsg struct {
- Session session.Session
-}
-
-// CloseSessionDialogMsg is sent when the session dialog is closed
-type CloseSessionDialogMsg struct{}
-
-// SessionDialog interface for the session switching dialog
-type SessionDialog interface {
- util.Model
- layout.Bindings
- SetSessions(sessions []session.Session)
- SetSelectedSession(sessionID string)
-}
-
-type sessionDialogCmp struct {
- sessions []session.Session
- selectedIdx int
- width int
- height int
- selectedSessionID string
-}
-
-type sessionKeyMap struct {
- Up key.Binding
- Down key.Binding
- Enter key.Binding
- Escape key.Binding
- J key.Binding
- K key.Binding
-}
-
-var sessionKeys = sessionKeyMap{
- Up: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "previous session"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "next session"),
- ),
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select session"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next session"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous session"),
- ),
-}
-
-func (s *sessionDialogCmp) Init() tea.Cmd {
- return nil
-}
-
-func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.KeyPressMsg:
- switch {
- case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
- if s.selectedIdx > 0 {
- s.selectedIdx--
- }
- return s, nil
- case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
- if s.selectedIdx < len(s.sessions)-1 {
- s.selectedIdx++
- }
- return s, nil
- case key.Matches(msg, sessionKeys.Enter):
- if len(s.sessions) > 0 {
- return s, util.CmdHandler(SessionSelectedMsg{
- Session: s.sessions[s.selectedIdx],
- })
- }
- case key.Matches(msg, sessionKeys.Escape):
- return s, util.CmdHandler(CloseSessionDialogMsg{})
- }
- case tea.WindowSizeMsg:
- s.width = msg.Width
- s.height = msg.Height
- }
- return s, nil
-}
-
-func (s *sessionDialogCmp) View() tea.View {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- if len(s.sessions) == 0 {
- return tea.NewView(
- baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(40).
- Render("No sessions available"),
- )
- }
-
- // Calculate max width needed for session titles
- maxWidth := 40 // Minimum width
- for _, sess := range s.sessions {
- if len(sess.Title) > maxWidth-4 { // Account for padding
- maxWidth = len(sess.Title) + 4
- }
- }
-
- maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
-
- // Limit height to avoid taking up too much screen space
- maxVisibleSessions := min(10, len(s.sessions))
-
- // Build the session list
- sessionItems := make([]string, 0, maxVisibleSessions)
- startIdx := 0
-
- // If we have more sessions than can be displayed, adjust the start index
- if len(s.sessions) > maxVisibleSessions {
- // Center the selected item when possible
- halfVisible := maxVisibleSessions / 2
- if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
- startIdx = s.selectedIdx - halfVisible
- } else if s.selectedIdx >= len(s.sessions)-halfVisible {
- startIdx = len(s.sessions) - maxVisibleSessions
- }
- }
-
- endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
-
- for i := startIdx; i < endIdx; i++ {
- sess := s.sessions[i]
- itemStyle := baseStyle.Width(maxWidth)
-
- if i == s.selectedIdx {
- itemStyle = itemStyle.
- Background(t.Primary()).
- Foreground(t.Background()).
- Bold(true)
- }
-
- sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
- }
-
- title := baseStyle.
- Foreground(t.Primary()).
- Bold(true).
- Width(maxWidth).
- Padding(0, 1).
- Render("Switch Session")
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- baseStyle.Width(maxWidth).Render(""),
- baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
- baseStyle.Width(maxWidth).Render(""),
- )
-
- return tea.NewView(
- baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Render(content),
- )
-}
-
-func (s *sessionDialogCmp) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(sessionKeys)
-}
-
-func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
- s.sessions = sessions
-
- // If we have a selected session ID, find its index
- if s.selectedSessionID != "" {
- for i, sess := range sessions {
- if sess.ID == s.selectedSessionID {
- s.selectedIdx = i
- return
- }
- }
- }
-
- // Default to first session if selected not found
- s.selectedIdx = 0
-}
-
-func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
- s.selectedSessionID = sessionID
-
- // Update the selected index if sessions are already loaded
- if len(s.sessions) > 0 {
- for i, sess := range s.sessions {
- if sess.ID == sessionID {
- s.selectedIdx = i
- return
- }
- }
- }
-}
-
-// NewSessionDialogCmp creates a new session switching dialog
-func NewSessionDialogCmp() SessionDialog {
- return &sessionDialogCmp{
- sessions: []session.Session{},
- selectedIdx: 0,
- selectedSessionID: "",
- }
-}
@@ -1,23 +1,29 @@
package commands
import (
+ "github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/opencode-ai/opencode/internal/tui/components/chat"
"github.com/opencode-ai/opencode/internal/tui/components/completions"
+ "github.com/opencode-ai/opencode/internal/tui/components/core"
"github.com/opencode-ai/opencode/internal/tui/components/core/list"
"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"
"github.com/opencode-ai/opencode/internal/tui/util"
)
const (
commandsDialogID dialogs.DialogID = "commands"
- defaultWidth int = 60
+ defaultWidth int = 70
+)
+
+const (
+ SystemCommands int = iota
+ UserCommands
)
// Command represents a command that can be executed
@@ -38,11 +44,18 @@ type commandDialogCmp struct {
wWidth int // Width of the terminal window
wHeight int // Height of the terminal window
- commandList list.ListModel
- commands []Command
- keyMap CommandsDialogKeyMap
+ commandList list.ListModel
+ keyMap CommandsDialogKeyMap
+ help help.Model
+ commandType int // SystemCommands or UserCommands
+ userCommands []Command // User-defined commands
}
+type (
+ SwitchSessionsMsg struct{}
+ SwitchModelMsg struct{}
+)
+
func NewCommandDialog() CommandsDialog {
listKeyMap := list.DefaultKeyMap()
keyMap := DefaultCommandsDialogKeyMap()
@@ -59,11 +72,20 @@ func NewCommandDialog() CommandsDialog {
listKeyMap.DownOneItem = keyMap.Next
listKeyMap.UpOneItem = keyMap.Previous
- commandList := list.New(list.WithFilterable(true), list.WithKeyMap(listKeyMap))
+ t := styles.CurrentTheme()
+ commandList := list.New(
+ list.WithFilterable(true),
+ list.WithKeyMap(listKeyMap),
+ list.WithWrapNavigation(true),
+ )
+ help := help.New()
+ help.Styles = t.S().Help
return &commandDialogCmp{
commandList: commandList,
width: defaultWidth,
keyMap: DefaultCommandsDialogKeyMap(),
+ help: help,
+ commandType: SystemCommands,
}
}
@@ -72,24 +94,9 @@ func (c *commandDialogCmp) Init() tea.Cmd {
if err != nil {
return util.ReportError(err)
}
- c.commands = commands
-
- commandItems := []util.Model{}
- if len(commands) > 0 {
- commandItems = append(commandItems, NewItemSection("Custom Commands"))
- for _, cmd := range commands {
- commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
- }
- }
-
- commandItems = append(commandItems, NewItemSection("Default"))
- for _, cmd := range c.defaultCommands() {
- c.commands = append(c.commands, cmd)
- commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
- }
-
- c.commandList.SetItems(commandItems)
+ c.userCommands = commands
+ c.SetCommandType(c.commandType)
return c.commandList.Init()
}
@@ -112,6 +119,13 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
util.CmdHandler(dialogs.CloseDialogMsg{}),
selectedItem.Handler(selectedItem),
)
+ case key.Matches(msg, c.keyMap.Tab):
+ // Toggle command type between System and User commands
+ if c.commandType == SystemCommands {
+ return c, c.SetCommandType(UserCommands)
+ } else {
+ return c, c.SetCommandType(SystemCommands)
+ }
default:
u, cmd := c.commandList.Update(msg)
c.commandList = u.(list.ListModel)
@@ -122,8 +136,17 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (c *commandDialogCmp) View() tea.View {
+ t := styles.CurrentTheme()
listView := c.commandList.View()
- v := tea.NewView(c.style().Render(listView.String()))
+ radio := c.commandTypeRadio()
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
+ listView.String(),
+ "",
+ t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
+ )
+ v := tea.NewView(c.style().Render(content))
if listView.Cursor() != nil {
c := c.moveCursor(listView.Cursor())
v.SetCursor(c)
@@ -131,8 +154,36 @@ func (c *commandDialogCmp) View() tea.View {
return v
}
+func (c *commandDialogCmp) commandTypeRadio() string {
+ t := styles.CurrentTheme()
+ choices := []string{"System", "User"}
+ iconSelected := "◉"
+ iconUnselected := "○"
+ if c.commandType == SystemCommands {
+ return t.S().Text.Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
+ }
+ return t.S().Text.Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
+}
+
func (c *commandDialogCmp) listWidth() int {
- return defaultWidth - 4 // 4 for padding
+ return defaultWidth - 2 // 4 for padding
+}
+
+func (c *commandDialogCmp) SetCommandType(commandType int) tea.Cmd {
+ c.commandType = commandType
+
+ var commands []Command
+ if c.commandType == SystemCommands {
+ commands = c.defaultCommands()
+ } else {
+ commands = c.userCommands
+ }
+
+ commandItems := []util.Model{}
+ for _, cmd := range commands {
+ commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
+ }
+ return c.commandList.SetItems(commandItems)
}
func (c *commandDialogCmp) listHeight() int {
@@ -141,27 +192,25 @@ func (c *commandDialogCmp) listHeight() int {
}
func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
- offset := 10 + 1
+ row, col := c.Position()
+ offset := row + 3
cursor.Y += offset
- _, col := c.Position()
cursor.X = cursor.X + col + 2
return cursor
}
func (c *commandDialogCmp) style() lipgloss.Style {
- t := theme.CurrentTheme()
- return styles.BaseStyle().
+ t := styles.CurrentTheme()
+ return t.S().Base.
Width(c.width).
- Padding(0, 1, 1, 1).
Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted())
+ BorderForeground(t.BorderFocus)
}
-func (q *commandDialogCmp) Position() (int, int) {
- row := 10
- col := q.wWidth / 2
- col -= q.width / 2
+func (c *commandDialogCmp) Position() (int, int) {
+ row := c.wHeight/4 - 2 // just a bit above the center
+ col := c.wWidth / 2
+ col -= c.width / 2
return row, col
}
@@ -197,6 +246,26 @@ func (c *commandDialogCmp) defaultCommands() []Command {
}
},
},
+ {
+ ID: "switch_session",
+ Title: "Switch Session",
+ Description: "Switch to a different session",
+ Handler: func(cmd Command) tea.Cmd {
+ return func() tea.Msg {
+ return SwitchSessionsMsg{}
+ }
+ },
+ },
+ {
+ ID: "switch_model",
+ Title: "Switch Model",
+ Description: "Switch to a different model",
+ Handler: func(cmd Command) tea.Cmd {
+ return func() tea.Msg {
+ return SwitchModelMsg{}
+ }
+ },
+ },
}
}
@@ -1,15 +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"
"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"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -19,8 +16,9 @@ type ItemSection interface {
list.SectionHeader
}
type itemSectionModel struct {
- width int
- title string
+ width int
+ title string
+ noPadding bool // No padding for the section header
}
func NewItemSection(title string) ItemSection {
@@ -38,16 +36,11 @@ func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
}
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))
+ t := styles.CurrentTheme()
+ title := ansi.Truncate(m.title, m.width-2, "…")
+ style := t.S().Base.Padding(1, 1, 0, 1)
+ title = t.S().Muted.Render(title)
+ return tea.NewView(style.Render(core.Section(title, m.width-2)))
}
func (m *itemSectionModel) GetSize() (int, int) {
@@ -9,12 +9,13 @@ type CommandsDialogKeyMap struct {
Select key.Binding
Next key.Binding
Previous key.Binding
+ Tab key.Binding
}
func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
return CommandsDialogKeyMap{
Select: key.NewBinding(
- key.WithKeys("enter", "tab", "ctrl+y"),
+ key.WithKeys("enter", "ctrl+y"),
key.WithHelp("enter", "confirm"),
),
Next: key.NewBinding(
@@ -25,6 +26,10 @@ func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
key.WithKeys("up", "ctrl+p"),
key.WithHelp("↑", "previous item"),
),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch selection"),
+ ),
}
}
@@ -42,9 +47,16 @@ func (k CommandsDialogKeyMap) FullHelp() [][]key.Binding {
// ShortHelp implements help.KeyMap.
func (k CommandsDialogKeyMap) ShortHelp() []key.Binding {
return []key.Binding{
+ k.Tab,
+ key.NewBinding(
+ key.WithKeys("down", "up"),
+ key.WithHelp("↑↓", "choose"),
+ ),
k.Select,
- k.Next,
- k.Previous,
+ key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
}
}
@@ -0,0 +1,56 @@
+package models
+
+import (
+ "github.com/charmbracelet/bubbles/v2/key"
+ "github.com/opencode-ai/opencode/internal/tui/layout"
+)
+
+type KeyMap struct {
+ Select key.Binding
+ Next key.Binding
+ Previous key.Binding
+}
+
+func DefaultKeyMap() KeyMap {
+ return KeyMap{
+ Select: key.NewBinding(
+ key.WithKeys("enter", "tab", "ctrl+y"),
+ key.WithHelp("enter", "confirm"),
+ ),
+ Next: key.NewBinding(
+ key.WithKeys("down", "ctrl+n"),
+ key.WithHelp("↓", "next item"),
+ ),
+ Previous: key.NewBinding(
+ key.WithKeys("up", "ctrl+p"),
+ key.WithHelp("↑", "previous item"),
+ ),
+ }
+}
+
+// FullHelp implements help.KeyMap.
+func (k KeyMap) FullHelp() [][]key.Binding {
+ m := [][]key.Binding{}
+ slice := layout.KeyMapToSlice(k)
+ for i := 0; i < len(slice); i += 4 {
+ end := min(i+4, len(slice))
+ m = append(m, slice[i:end])
+ }
+ return m
+}
+
+// ShortHelp implements help.KeyMap.
+func (k KeyMap) ShortHelp() []key.Binding {
+ return []key.Binding{
+ key.NewBinding(
+
+ key.WithKeys("down", "up"),
+ key.WithHelp("↑↓", "choose"),
+ ),
+ k.Select,
+ key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "cancel"),
+ ),
+ }
+}
@@ -0,0 +1,261 @@
+package models
+
+import (
+ "slices"
+
+ "github.com/charmbracelet/bubbles/v2/help"
+ "github.com/charmbracelet/bubbles/v2/key"
+ tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/lipgloss/v2"
+ "github.com/opencode-ai/opencode/internal/config"
+ "github.com/opencode-ai/opencode/internal/llm/models"
+ "github.com/opencode-ai/opencode/internal/tui/components/completions"
+ "github.com/opencode-ai/opencode/internal/tui/components/core"
+ "github.com/opencode-ai/opencode/internal/tui/components/core/list"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
+ "github.com/opencode-ai/opencode/internal/tui/styles"
+ "github.com/opencode-ai/opencode/internal/tui/util"
+)
+
+const (
+ ID dialogs.DialogID = "models"
+
+ defaultWidth = 60
+)
+
+// ModelSelectedMsg is sent when a model is selected
+type ModelSelectedMsg struct {
+ Model models.Model
+}
+
+// CloseModelDialogMsg is sent when a model is selected
+type CloseModelDialogMsg struct{}
+
+// ModelDialog interface for the model selection dialog
+type ModelDialog interface {
+ dialogs.DialogModel
+}
+
+type modelDialogCmp struct {
+ width int
+ wWidth int // Width of the terminal window
+ wHeight int // Height of the terminal window
+
+ modelList list.ListModel
+ keyMap KeyMap
+ help help.Model
+}
+
+func NewModelDialogCmp() ModelDialog {
+ listKeyMap := list.DefaultKeyMap()
+ keyMap := DefaultKeyMap()
+
+ listKeyMap.Down.SetEnabled(false)
+ listKeyMap.Up.SetEnabled(false)
+ listKeyMap.NDown.SetEnabled(false)
+ listKeyMap.NUp.SetEnabled(false)
+ listKeyMap.HalfPageDown.SetEnabled(false)
+ listKeyMap.HalfPageUp.SetEnabled(false)
+ listKeyMap.Home.SetEnabled(false)
+ listKeyMap.End.SetEnabled(false)
+
+ listKeyMap.DownOneItem = keyMap.Next
+ listKeyMap.UpOneItem = keyMap.Previous
+
+ t := styles.CurrentTheme()
+ inputStyle := t.S().Base.Padding(0, 1, 0, 1)
+ modelList := list.New(
+ list.WithFilterable(true),
+ list.WithKeyMap(listKeyMap),
+ list.WithInputStyle(inputStyle),
+ list.WithWrapNavigation(true),
+ )
+ help := help.New()
+ help.Styles = t.S().Help
+
+ return &modelDialogCmp{
+ modelList: modelList,
+ width: defaultWidth,
+ keyMap: DefaultKeyMap(),
+ help: help,
+ }
+}
+
+var ProviderPopularity = map[models.ModelProvider]int{
+ models.ProviderAnthropic: 1,
+ models.ProviderOpenAI: 2,
+ models.ProviderGemini: 3,
+ models.ProviderGROQ: 4,
+ models.ProviderOpenRouter: 5,
+ models.ProviderBedrock: 6,
+ models.ProviderAzure: 7,
+ models.ProviderVertexAI: 8,
+ models.ProviderXAI: 9,
+}
+
+var ProviderName = map[models.ModelProvider]string{
+ models.ProviderAnthropic: "Anthropic",
+ models.ProviderOpenAI: "OpenAI",
+ models.ProviderGemini: "Gemini",
+ models.ProviderGROQ: "Groq",
+ models.ProviderOpenRouter: "OpenRouter",
+ models.ProviderBedrock: "AWS Bedrock",
+ models.ProviderAzure: "Azure",
+ models.ProviderVertexAI: "VertexAI",
+ models.ProviderXAI: "xAI",
+}
+
+func (m *modelDialogCmp) Init() tea.Cmd {
+ cfg := config.Get()
+ enabledProviders := getEnabledProviders(cfg)
+
+ modelItems := []util.Model{}
+ for _, provider := range enabledProviders {
+ name, ok := ProviderName[provider]
+ if !ok {
+ name = string(provider) // Fallback to provider ID if name is not defined
+ }
+ modelItems = append(modelItems, commands.NewItemSection(name))
+ for _, model := range getModelsForProvider(provider) {
+ modelItems = append(modelItems, completions.NewCompletionItem(model.Name, model))
+ }
+ }
+ m.modelList.SetItems(modelItems)
+ return m.modelList.Init()
+}
+
+func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.wWidth = msg.Width
+ m.wHeight = msg.Height
+ return m, m.modelList.SetSize(m.listWidth(), m.listHeight())
+ case tea.KeyPressMsg:
+ switch {
+ case key.Matches(msg, m.keyMap.Select):
+ selectedItemInx := m.modelList.SelectedIndex()
+ if selectedItemInx == list.NoSelection {
+ return m, nil // No item selected, do nothing
+ }
+ items := m.modelList.Items()
+ selectedItem := items[selectedItemInx].(completions.CompletionItem).Value().(models.Model)
+
+ return m, tea.Sequence(
+ util.CmdHandler(dialogs.CloseDialogMsg{}),
+ util.CmdHandler(ModelSelectedMsg{Model: selectedItem}),
+ )
+ default:
+ u, cmd := m.modelList.Update(msg)
+ m.modelList = u.(list.ListModel)
+ return m, cmd
+ }
+ }
+ return m, nil
+}
+
+func (m *modelDialogCmp) View() tea.View {
+ t := styles.CurrentTheme()
+ listView := m.modelList.View()
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)),
+ listView.String(),
+ "",
+ t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
+ )
+ v := tea.NewView(m.style().Render(content))
+ if listView.Cursor() != nil {
+ c := m.moveCursor(listView.Cursor())
+ v.SetCursor(c)
+ }
+ return v
+}
+
+func (m *modelDialogCmp) style() lipgloss.Style {
+ t := styles.CurrentTheme()
+ return t.S().Base.
+ Width(m.width).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(t.BorderFocus)
+}
+
+func (m *modelDialogCmp) listWidth() int {
+ return defaultWidth - 2 // 4 for padding
+}
+
+func (m *modelDialogCmp) listHeight() int {
+ listHeigh := len(m.modelList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
+ return min(listHeigh, m.wHeight/2)
+}
+
+func GetSelectedModel(cfg *config.Config) models.Model {
+ agentCfg := cfg.Agents[config.AgentCoder]
+ selectedModelId := agentCfg.Model
+ return models.SupportedModels[selectedModelId]
+}
+
+func getEnabledProviders(cfg *config.Config) []models.ModelProvider {
+ var providers []models.ModelProvider
+ for providerId, provider := range cfg.Providers {
+ if !provider.Disabled {
+ providers = append(providers, providerId)
+ }
+ }
+
+ // Sort by provider popularity
+ slices.SortFunc(providers, func(a, b models.ModelProvider) int {
+ rA := ProviderPopularity[a]
+ rB := ProviderPopularity[b]
+
+ // models not included in popularity ranking default to last
+ if rA == 0 {
+ rA = 999
+ }
+ if rB == 0 {
+ rB = 999
+ }
+ return rA - rB
+ })
+ return providers
+}
+
+func getModelsForProvider(provider models.ModelProvider) []models.Model {
+ var providerModels []models.Model
+ for _, model := range models.SupportedModels {
+ if model.Provider == provider {
+ providerModels = append(providerModels, model)
+ }
+ }
+
+ // reverse alphabetical order (if llm naming was consistent latest would appear first)
+ slices.SortFunc(providerModels, func(a, b models.Model) int {
+ if a.Name > b.Name {
+ return -1
+ } else if a.Name < b.Name {
+ return 1
+ }
+ return 0
+ })
+
+ return providerModels
+}
+
+func (m *modelDialogCmp) Position() (int, int) {
+ row := m.wHeight/4 - 2 // just a bit above the center
+ col := m.wWidth / 2
+ col -= m.width / 2
+ return row, col
+}
+
+func (m *modelDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
+ row, col := m.Position()
+ offset := row + 3 // Border + title
+ cursor.Y += offset
+ cursor.X = cursor.X + col + 2
+ return cursor
+}
+
+func (m *modelDialogCmp) ID() dialogs.DialogID {
+ return ID
+}
@@ -63,7 +63,13 @@ func NewSessionDialogCmp(sessions []session.Session, selectedID string) SessionD
}
}
- sessionsList := list.New(list.WithFilterable(true), list.WithFilterPlaceholder("Enter a session name"), list.WithKeyMap(listKeyMap), list.WithItems(items))
+ sessionsList := list.New(
+ list.WithFilterable(true),
+ list.WithFilterPlaceholder("Enter a session name"),
+ list.WithKeyMap(listKeyMap),
+ list.WithItems(items),
+ list.WithWrapNavigation(true),
+ )
help := help.New()
help.Styles = t.S().Help
s := &sessionDialogCmp{
@@ -6,14 +6,13 @@ import (
)
type KeyMap struct {
- Logs key.Binding
- Quit key.Binding
- Help key.Binding
- SwitchSession key.Binding
- Commands key.Binding
- FilePicker key.Binding
- Models key.Binding
- SwitchTheme key.Binding
+ Logs key.Binding
+ Quit key.Binding
+ Help key.Binding
+ Commands key.Binding
+ FilePicker key.Binding
+ Models key.Binding
+ SwitchTheme key.Binding
}
func DefaultKeyMap() KeyMap {
@@ -33,11 +32,6 @@ func DefaultKeyMap() KeyMap {
key.WithHelp("ctrl+?", "toggle help"),
),
- SwitchSession: key.NewBinding(
- key.WithKeys("ctrl+s"),
- key.WithHelp("ctrl+s", "switch session"),
- ),
-
Commands: key.NewBinding(
key.WithKeys("ctrl+k"),
key.WithHelp("ctrl+k", "commands"),
@@ -14,6 +14,7 @@ import (
"github.com/opencode-ai/opencode/internal/tui/components/core"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
+ "github.com/opencode-ai/opencode/internal/tui/components/dialogs/models"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
"github.com/opencode-ai/opencode/internal/tui/components/dialogs/sessions"
"github.com/opencode-ai/opencode/internal/tui/layout"
@@ -116,6 +117,20 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, pageCmd)
}
return a, tea.Batch(cmds...)
+ // Commands
+ case commands.SwitchSessionsMsg:
+ return a, func() tea.Msg {
+ allSessions, _ := a.app.Sessions.List(context.Background())
+ return dialogs.OpenDialogMsg{
+ Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
+ }
+ }
+ case commands.SwitchModelMsg:
+ return a, util.CmdHandler(
+ dialogs.OpenDialogMsg{
+ Model: models.NewModelDialogCmp(),
+ },
+ )
case tea.KeyPressMsg:
return a, a.handleKeyPressMsg(msg)
}
@@ -182,13 +197,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
return util.CmdHandler(dialogs.OpenDialogMsg{
Model: commands.NewCommandDialog(),
})
- case key.Matches(msg, a.keyMap.SwitchSession):
- return func() tea.Msg {
- allSessions, _ := a.app.Sessions.List(context.Background())
- return dialogs.OpenDialogMsg{
- Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
- }
- }
// Page navigation
case key.Matches(msg, a.keyMap.Logs):
return a.moveToPage(page.LogsPage)
@@ -9,7 +9,7 @@
## Dialogs
-- [ ] Move sessions and modal dialog to the commands
+- [x] Cleanup Commands
- [x] Sessions dialog
-- [ ] Commands
- [ ] Models
+- [~] Move sessions and model dialog to the commands