models.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package dialog
  2. import (
  3. "context"
  4. "fmt"
  5. "maps"
  6. "slices"
  7. "strings"
  8. "github.com/charmbracelet/bubbles/v2/key"
  9. tea "github.com/charmbracelet/bubbletea/v2"
  10. "github.com/charmbracelet/lipgloss/v2"
  11. "github.com/sst/opencode/internal/app"
  12. "github.com/sst/opencode/internal/components/list"
  13. "github.com/sst/opencode/internal/components/modal"
  14. "github.com/sst/opencode/internal/layout"
  15. "github.com/sst/opencode/internal/styles"
  16. "github.com/sst/opencode/internal/theme"
  17. "github.com/sst/opencode/internal/util"
  18. "github.com/sst/opencode/pkg/client"
  19. )
  20. const (
  21. numVisibleModels = 6
  22. maxDialogWidth = 40
  23. )
  24. // ModelDialog interface for the model selection dialog
  25. type ModelDialog interface {
  26. layout.Modal
  27. }
  28. type modelDialog struct {
  29. app *app.App
  30. availableProviders []client.ProviderInfo
  31. provider client.ProviderInfo
  32. width int
  33. height int
  34. hScrollOffset int
  35. hScrollPossible bool
  36. modal *modal.Modal
  37. modelList list.List[list.StringItem]
  38. }
  39. type modelKeyMap struct {
  40. Left key.Binding
  41. Right key.Binding
  42. Enter key.Binding
  43. Escape key.Binding
  44. }
  45. var modelKeys = modelKeyMap{
  46. Left: key.NewBinding(
  47. key.WithKeys("left", "h"),
  48. key.WithHelp("←", "scroll left"),
  49. ),
  50. Right: key.NewBinding(
  51. key.WithKeys("right", "l"),
  52. key.WithHelp("→", "scroll right"),
  53. ),
  54. Enter: key.NewBinding(
  55. key.WithKeys("enter"),
  56. key.WithHelp("enter", "select model"),
  57. ),
  58. Escape: key.NewBinding(
  59. key.WithKeys("esc"),
  60. key.WithHelp("esc", "close"),
  61. ),
  62. }
  63. func (m *modelDialog) Init() tea.Cmd {
  64. m.setupModelsForProvider(m.provider.Id)
  65. return nil
  66. }
  67. func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  68. switch msg := msg.(type) {
  69. case tea.KeyMsg:
  70. switch {
  71. case key.Matches(msg, modelKeys.Left):
  72. if m.hScrollPossible {
  73. m.switchProvider(-1)
  74. }
  75. return m, nil
  76. case key.Matches(msg, modelKeys.Right):
  77. if m.hScrollPossible {
  78. m.switchProvider(1)
  79. }
  80. return m, nil
  81. case key.Matches(msg, modelKeys.Enter):
  82. selectedItem, _ := m.modelList.GetSelectedItem()
  83. models := m.models()
  84. var selectedModel client.ModelInfo
  85. for _, model := range models {
  86. if model.Name == string(selectedItem) {
  87. selectedModel = model
  88. break
  89. }
  90. }
  91. return m, tea.Sequence(
  92. util.CmdHandler(modal.CloseModalMsg{}),
  93. util.CmdHandler(
  94. app.ModelSelectedMsg{
  95. Provider: m.provider,
  96. Model: selectedModel,
  97. }),
  98. )
  99. case key.Matches(msg, modelKeys.Escape):
  100. return m, util.CmdHandler(modal.CloseModalMsg{})
  101. }
  102. case tea.WindowSizeMsg:
  103. m.width = msg.Width
  104. m.height = msg.Height
  105. }
  106. // Update the list component
  107. updatedList, cmd := m.modelList.Update(msg)
  108. m.modelList = updatedList.(list.List[list.StringItem])
  109. return m, cmd
  110. }
  111. func (m *modelDialog) models() []client.ModelInfo {
  112. models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ModelInfo) int {
  113. return strings.Compare(a.Name, b.Name)
  114. })
  115. return models
  116. }
  117. func (m *modelDialog) switchProvider(offset int) {
  118. newOffset := m.hScrollOffset + offset
  119. if newOffset < 0 {
  120. newOffset = len(m.availableProviders) - 1
  121. }
  122. if newOffset >= len(m.availableProviders) {
  123. newOffset = 0
  124. }
  125. m.hScrollOffset = newOffset
  126. m.provider = m.availableProviders[m.hScrollOffset]
  127. m.modal.SetTitle(fmt.Sprintf("Select %s Model", m.provider.Name))
  128. m.setupModelsForProvider(m.provider.Id)
  129. }
  130. func (m *modelDialog) View() string {
  131. listView := m.modelList.View()
  132. scrollIndicator := m.getScrollIndicators(maxDialogWidth)
  133. return strings.Join([]string{listView, scrollIndicator}, "\n")
  134. }
  135. func (m *modelDialog) getScrollIndicators(maxWidth int) string {
  136. var indicator string
  137. if m.hScrollPossible {
  138. indicator = "← → (switch provider) "
  139. }
  140. if indicator == "" {
  141. return ""
  142. }
  143. t := theme.CurrentTheme()
  144. return styles.BaseStyle().
  145. Foreground(t.TextMuted()).
  146. Width(maxWidth).
  147. Align(lipgloss.Right).
  148. Render(indicator)
  149. }
  150. func (m *modelDialog) setupModelsForProvider(providerId string) {
  151. models := m.models()
  152. modelNames := make([]string, len(models))
  153. for i, model := range models {
  154. modelNames[i] = model.Name
  155. }
  156. m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
  157. m.modelList.SetMaxWidth(maxDialogWidth)
  158. if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
  159. for i, model := range models {
  160. if model.Id == m.app.Model.Id {
  161. m.modelList.SetSelectedIndex(i)
  162. break
  163. }
  164. }
  165. }
  166. }
  167. func (m *modelDialog) Render(background string) string {
  168. return m.modal.Render(m.View(), background)
  169. }
  170. func (s *modelDialog) Close() tea.Cmd {
  171. return nil
  172. }
  173. func NewModelDialog(app *app.App) ModelDialog {
  174. availableProviders, _ := app.ListProviders(context.Background())
  175. currentProvider := availableProviders[0]
  176. hScrollOffset := 0
  177. if app.Provider != nil {
  178. for i, provider := range availableProviders {
  179. if provider.Id == app.Provider.Id {
  180. currentProvider = provider
  181. hScrollOffset = i
  182. break
  183. }
  184. }
  185. }
  186. dialog := &modelDialog{
  187. app: app,
  188. availableProviders: availableProviders,
  189. hScrollOffset: hScrollOffset,
  190. hScrollPossible: len(availableProviders) > 1,
  191. provider: currentProvider,
  192. modal: modal.New(
  193. modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
  194. modal.WithMaxWidth(maxDialogWidth+4),
  195. ),
  196. }
  197. dialog.setupModelsForProvider(currentProvider.Id)
  198. return dialog
  199. }