models.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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/modal"
  13. "github.com/sst/opencode/internal/layout"
  14. "github.com/sst/opencode/internal/state"
  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. selectedIdx int
  33. width int
  34. height int
  35. scrollOffset int
  36. hScrollOffset int
  37. hScrollPossible bool
  38. modal *modal.Modal
  39. }
  40. type modelKeyMap struct {
  41. Up key.Binding
  42. Down key.Binding
  43. Left key.Binding
  44. Right key.Binding
  45. Enter key.Binding
  46. Escape key.Binding
  47. }
  48. var modelKeys = modelKeyMap{
  49. Up: key.NewBinding(
  50. key.WithKeys("up", "k"),
  51. key.WithHelp("↑", "previous model"),
  52. ),
  53. Down: key.NewBinding(
  54. key.WithKeys("down", "j"),
  55. key.WithHelp("↓", "next model"),
  56. ),
  57. Left: key.NewBinding(
  58. key.WithKeys("left", "h"),
  59. key.WithHelp("←", "scroll left"),
  60. ),
  61. Right: key.NewBinding(
  62. key.WithKeys("right", "l"),
  63. key.WithHelp("→", "scroll right"),
  64. ),
  65. Enter: key.NewBinding(
  66. key.WithKeys("enter"),
  67. key.WithHelp("enter", "select model"),
  68. ),
  69. Escape: key.NewBinding(
  70. key.WithKeys("esc"),
  71. key.WithHelp("esc", "close"),
  72. ),
  73. }
  74. func (m *modelDialog) Init() tea.Cmd {
  75. // cfg := config.Get()
  76. // modelInfo := GetSelectedModel(cfg)
  77. // m.availableProviders = getEnabledProviders(cfg)
  78. // m.hScrollPossible = len(m.availableProviders) > 1
  79. // m.provider = modelInfo.Provider
  80. // m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
  81. // m.setupModelsForProvider(m.provider)
  82. return nil
  83. }
  84. func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  85. switch msg := msg.(type) {
  86. case tea.KeyMsg:
  87. switch {
  88. case key.Matches(msg, modelKeys.Up):
  89. m.moveSelectionUp()
  90. case key.Matches(msg, modelKeys.Down):
  91. m.moveSelectionDown()
  92. case key.Matches(msg, modelKeys.Left):
  93. if m.hScrollPossible {
  94. m.switchProvider(-1)
  95. }
  96. case key.Matches(msg, modelKeys.Right):
  97. if m.hScrollPossible {
  98. m.switchProvider(1)
  99. }
  100. case key.Matches(msg, modelKeys.Enter):
  101. models := m.models()
  102. return m, tea.Sequence(
  103. util.CmdHandler(modal.CloseModalMsg{}),
  104. util.CmdHandler(
  105. state.ModelSelectedMsg{
  106. Provider: m.provider,
  107. Model: models[m.selectedIdx],
  108. }),
  109. )
  110. case key.Matches(msg, modelKeys.Escape):
  111. return m, util.CmdHandler(modal.CloseModalMsg{})
  112. }
  113. case tea.WindowSizeMsg:
  114. m.width = msg.Width
  115. m.height = msg.Height
  116. }
  117. return m, nil
  118. }
  119. func (m *modelDialog) models() []client.ModelInfo {
  120. models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ModelInfo) int {
  121. return strings.Compare(a.Name, b.Name)
  122. })
  123. return models
  124. }
  125. // moveSelectionUp moves the selection up or wraps to bottom
  126. func (m *modelDialog) moveSelectionUp() {
  127. if m.selectedIdx > 0 {
  128. m.selectedIdx--
  129. } else {
  130. m.selectedIdx = len(m.provider.Models) - 1
  131. m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
  132. }
  133. // Keep selection visible
  134. if m.selectedIdx < m.scrollOffset {
  135. m.scrollOffset = m.selectedIdx
  136. }
  137. }
  138. // moveSelectionDown moves the selection down or wraps to top
  139. func (m *modelDialog) moveSelectionDown() {
  140. if m.selectedIdx < len(m.provider.Models)-1 {
  141. m.selectedIdx++
  142. } else {
  143. m.selectedIdx = 0
  144. m.scrollOffset = 0
  145. }
  146. // Keep selection visible
  147. if m.selectedIdx >= m.scrollOffset+numVisibleModels {
  148. m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
  149. }
  150. }
  151. func (m *modelDialog) switchProvider(offset int) {
  152. newOffset := m.hScrollOffset + offset
  153. // Ensure we stay within bounds
  154. if newOffset < 0 {
  155. newOffset = len(m.availableProviders) - 1
  156. }
  157. if newOffset >= len(m.availableProviders) {
  158. newOffset = 0
  159. }
  160. m.hScrollOffset = newOffset
  161. m.provider = m.availableProviders[m.hScrollOffset]
  162. m.setupModelsForProvider(m.provider.Id)
  163. }
  164. func (m *modelDialog) View() string {
  165. t := theme.CurrentTheme()
  166. baseStyle := lipgloss.NewStyle().
  167. Background(t.BackgroundElement()).
  168. Foreground(t.Text())
  169. // Capitalize first letter of provider name
  170. title := baseStyle.
  171. Foreground(t.Primary()).
  172. Bold(true).
  173. Width(maxDialogWidth).
  174. Padding(0, 0, 1).
  175. Render(fmt.Sprintf("Select %s Model", m.provider.Name))
  176. // Render visible models
  177. endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
  178. modelItems := make([]string, 0, endIdx-m.scrollOffset)
  179. models := m.models()
  180. for i := m.scrollOffset; i < endIdx; i++ {
  181. itemStyle := baseStyle.Width(maxDialogWidth)
  182. if i == m.selectedIdx {
  183. itemStyle = itemStyle.
  184. Background(t.Primary()).
  185. Foreground(t.BackgroundElement()).
  186. Bold(true)
  187. }
  188. modelItems = append(modelItems, itemStyle.Render(models[i].Name))
  189. }
  190. scrollIndicator := m.getScrollIndicators(maxDialogWidth)
  191. content := lipgloss.JoinVertical(
  192. lipgloss.Left,
  193. title,
  194. baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
  195. scrollIndicator,
  196. )
  197. return content
  198. }
  199. func (m *modelDialog) getScrollIndicators(maxWidth int) string {
  200. var indicator string
  201. if len(m.provider.Models) > numVisibleModels {
  202. if m.scrollOffset > 0 {
  203. indicator += "↑ "
  204. }
  205. if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
  206. indicator += "↓ "
  207. }
  208. }
  209. if m.hScrollPossible {
  210. if m.hScrollOffset > 0 {
  211. indicator = "← " + indicator
  212. }
  213. if m.hScrollOffset < len(m.availableProviders)-1 {
  214. indicator += "→"
  215. }
  216. }
  217. if indicator == "" {
  218. return ""
  219. }
  220. t := theme.CurrentTheme()
  221. baseStyle := styles.BaseStyle()
  222. return baseStyle.
  223. Foreground(t.Primary()).
  224. Width(maxWidth).
  225. Align(lipgloss.Right).
  226. Bold(true).
  227. Render(indicator)
  228. }
  229. // findProviderIndex returns the index of the provider in the list, or -1 if not found
  230. // func findProviderIndex(providers []string, provider string) int {
  231. // for i, p := range providers {
  232. // if p == provider {
  233. // return i
  234. // }
  235. // }
  236. // return -1
  237. // }
  238. func (m *modelDialog) setupModelsForProvider(_ string) {
  239. m.selectedIdx = 0
  240. m.scrollOffset = 0
  241. // cfg := config.Get()
  242. // agentCfg := cfg.Agents[config.AgentPrimary]
  243. // selectedModelId := agentCfg.Model
  244. // m.provider = provider
  245. // m.models = getModelsForProvider(provider)
  246. // Try to select the current model if it belongs to this provider
  247. // if provider == models.SupportedModels[selectedModelId].Provider {
  248. // for i, model := range m.models {
  249. // if model.ID == selectedModelId {
  250. // m.selectedIdx = i
  251. // // Adjust scroll position to keep selected model visible
  252. // if m.selectedIdx >= numVisibleModels {
  253. // m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
  254. // }
  255. // break
  256. // }
  257. // }
  258. // }
  259. }
  260. func (m *modelDialog) Render(background string) string {
  261. return m.modal.Render(m.View(), background)
  262. }
  263. func (s *modelDialog) Close() tea.Cmd {
  264. return nil
  265. }
  266. func NewModelDialog(app *app.App) ModelDialog {
  267. availableProviders, _ := app.ListProviders(context.Background())
  268. return &modelDialog{
  269. availableProviders: availableProviders,
  270. hScrollOffset: 0,
  271. hScrollPossible: len(availableProviders) > 1,
  272. provider: availableProviders[0],
  273. modal: modal.New(),
  274. }
  275. }