models.go 7.9 KB

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