models.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. package dialog
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/sst/opencode/internal/config"
  7. "github.com/sst/opencode/internal/tui/layout"
  8. "github.com/sst/opencode/internal/tui/styles"
  9. "github.com/sst/opencode/internal/tui/theme"
  10. "github.com/sst/opencode/internal/tui/util"
  11. )
  12. const (
  13. numVisibleModels = 10
  14. maxDialogWidth = 40
  15. )
  16. // ModelSelectedMsg is sent when a model is selected
  17. type ModelSelectedMsg struct {
  18. // Model models.Model
  19. }
  20. // CloseModelDialogMsg is sent when a model is selected
  21. type CloseModelDialogMsg struct{}
  22. // ModelDialog interface for the model selection dialog
  23. type ModelDialog interface {
  24. tea.Model
  25. layout.Bindings
  26. }
  27. type modelDialogCmp struct {
  28. // models []models.Model
  29. // provider models.ModelProvider
  30. // availableProviders []models.ModelProvider
  31. selectedIdx int
  32. width int
  33. height int
  34. scrollOffset int
  35. hScrollOffset int
  36. hScrollPossible bool
  37. }
  38. type modelKeyMap struct {
  39. Up key.Binding
  40. Down key.Binding
  41. Left key.Binding
  42. Right key.Binding
  43. Enter key.Binding
  44. Escape key.Binding
  45. J key.Binding
  46. K key.Binding
  47. H key.Binding
  48. L key.Binding
  49. }
  50. var modelKeys = modelKeyMap{
  51. Up: key.NewBinding(
  52. key.WithKeys("up"),
  53. key.WithHelp("↑", "previous model"),
  54. ),
  55. Down: key.NewBinding(
  56. key.WithKeys("down"),
  57. key.WithHelp("↓", "next model"),
  58. ),
  59. Left: key.NewBinding(
  60. key.WithKeys("left"),
  61. key.WithHelp("←", "scroll left"),
  62. ),
  63. Right: key.NewBinding(
  64. key.WithKeys("right"),
  65. key.WithHelp("→", "scroll right"),
  66. ),
  67. Enter: key.NewBinding(
  68. key.WithKeys("enter"),
  69. key.WithHelp("enter", "select model"),
  70. ),
  71. Escape: key.NewBinding(
  72. key.WithKeys("esc"),
  73. key.WithHelp("esc", "close"),
  74. ),
  75. J: key.NewBinding(
  76. key.WithKeys("j"),
  77. key.WithHelp("j", "next model"),
  78. ),
  79. K: key.NewBinding(
  80. key.WithKeys("k"),
  81. key.WithHelp("k", "previous model"),
  82. ),
  83. H: key.NewBinding(
  84. key.WithKeys("h"),
  85. key.WithHelp("h", "scroll left"),
  86. ),
  87. L: key.NewBinding(
  88. key.WithKeys("l"),
  89. key.WithHelp("l", "scroll right"),
  90. ),
  91. }
  92. func (m *modelDialogCmp) Init() tea.Cmd {
  93. m.setupModels()
  94. return nil
  95. }
  96. func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  97. switch msg := msg.(type) {
  98. case tea.KeyMsg:
  99. switch {
  100. case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
  101. m.moveSelectionUp()
  102. case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
  103. m.moveSelectionDown()
  104. case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
  105. if m.hScrollPossible {
  106. m.switchProvider(-1)
  107. }
  108. case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
  109. if m.hScrollPossible {
  110. m.switchProvider(1)
  111. }
  112. case key.Matches(msg, modelKeys.Enter):
  113. // return m, util.CmdHandler(ModelSelectedMsg{Model: m.models[m.selectedIdx]})
  114. case key.Matches(msg, modelKeys.Escape):
  115. return m, util.CmdHandler(CloseModelDialogMsg{})
  116. }
  117. case tea.WindowSizeMsg:
  118. m.width = msg.Width
  119. m.height = msg.Height
  120. }
  121. return m, nil
  122. }
  123. // moveSelectionUp moves the selection up or wraps to bottom
  124. func (m *modelDialogCmp) moveSelectionUp() {
  125. if m.selectedIdx > 0 {
  126. m.selectedIdx--
  127. } else {
  128. // m.selectedIdx = len(m.models) - 1
  129. // m.scrollOffset = max(0, len(m.models)-numVisibleModels)
  130. }
  131. // Keep selection visible
  132. if m.selectedIdx < m.scrollOffset {
  133. m.scrollOffset = m.selectedIdx
  134. }
  135. }
  136. // moveSelectionDown moves the selection down or wraps to top
  137. func (m *modelDialogCmp) moveSelectionDown() {
  138. // if m.selectedIdx < len(m.models)-1 {
  139. // m.selectedIdx++
  140. // } else {
  141. // m.selectedIdx = 0
  142. // m.scrollOffset = 0
  143. // }
  144. // Keep selection visible
  145. if m.selectedIdx >= m.scrollOffset+numVisibleModels {
  146. m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
  147. }
  148. }
  149. func (m *modelDialogCmp) switchProvider(offset int) {
  150. newOffset := m.hScrollOffset + offset
  151. // Ensure we stay within bounds
  152. // if newOffset < 0 {
  153. // newOffset = len(m.availableProviders) - 1
  154. // }
  155. // if newOffset >= len(m.availableProviders) {
  156. // newOffset = 0
  157. // }
  158. m.hScrollOffset = newOffset
  159. // m.provider = m.availableProviders[m.hScrollOffset]
  160. // m.setupModelsForProvider(m.provider)
  161. }
  162. func (m *modelDialogCmp) View() string {
  163. t := theme.CurrentTheme()
  164. baseStyle := styles.BaseStyle()
  165. // Capitalize first letter of provider name
  166. // providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
  167. // title := baseStyle.
  168. // Foreground(t.Primary()).
  169. // Bold(true).
  170. // Width(maxDialogWidth).
  171. // Padding(0, 0, 1).
  172. // Render(fmt.Sprintf("Select %s Model", providerName))
  173. // Render visible models
  174. // endIdx := min(m.scrollOffset+numVisibleModels, len(m.models))
  175. // modelItems := make([]string, 0, endIdx-m.scrollOffset)
  176. //
  177. // for i := m.scrollOffset; i < endIdx; i++ {
  178. // itemStyle := baseStyle.Width(maxDialogWidth)
  179. // if i == m.selectedIdx {
  180. // itemStyle = itemStyle.Background(t.Primary()).
  181. // Foreground(t.Background()).Bold(true)
  182. // }
  183. // modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
  184. // }
  185. scrollIndicator := m.getScrollIndicators(maxDialogWidth)
  186. content := lipgloss.JoinVertical(
  187. lipgloss.Left,
  188. // title,
  189. // baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
  190. scrollIndicator,
  191. )
  192. return baseStyle.Padding(1, 2).
  193. Border(lipgloss.RoundedBorder()).
  194. BorderBackground(t.Background()).
  195. BorderForeground(t.TextMuted()).
  196. Width(lipgloss.Width(content) + 4).
  197. Render(content)
  198. }
  199. func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
  200. var indicator string
  201. // if len(m.models) > numVisibleModels {
  202. // if m.scrollOffset > 0 {
  203. // indicator += "↑ "
  204. // }
  205. // if m.scrollOffset+numVisibleModels < len(m.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. func (m *modelDialogCmp) BindingKeys() []key.Binding {
  230. return layout.KeyMapToSlice(modelKeys)
  231. }
  232. func (m *modelDialogCmp) setupModels() {
  233. // cfg := config.Get()
  234. // modelInfo := GetSelectedModel(cfg)
  235. // m.availableProviders = getEnabledProviders(cfg)
  236. // m.hScrollPossible = len(m.availableProviders) > 1
  237. //
  238. // m.provider = modelInfo.Provider
  239. // m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
  240. //
  241. // m.setupModelsForProvider(m.provider)
  242. }
  243. func GetSelectedModel(cfg *config.Config) string {
  244. return "Claude Sonnet 4"
  245. // agentCfg := cfg.Agents[config.AgentPrimary]
  246. // selectedModelId := agentCfg.Model
  247. // return models.SupportedModels[selectedModelId]
  248. }
  249. func getEnabledProviders(cfg *config.Config) []string {
  250. return []string{"anthropic", "openai", "google"}
  251. // var providers []models.ModelProvider
  252. // for providerId, provider := range cfg.Providers {
  253. // if !provider.Disabled {
  254. // providers = append(providers, providerId)
  255. // }
  256. // }
  257. //
  258. // // Sort by provider popularity
  259. // slices.SortFunc(providers, func(a, b models.ModelProvider) int {
  260. // rA := models.ProviderPopularity[a]
  261. // rB := models.ProviderPopularity[b]
  262. //
  263. // // models not included in popularity ranking default to last
  264. // if rA == 0 {
  265. // rA = 999
  266. // }
  267. // if rB == 0 {
  268. // rB = 999
  269. // }
  270. // return rA - rB
  271. // })
  272. // return providers
  273. }
  274. // findProviderIndex returns the index of the provider in the list, or -1 if not found
  275. func findProviderIndex(providers []string, provider string) int {
  276. for i, p := range providers {
  277. if p == provider {
  278. return i
  279. }
  280. }
  281. return -1
  282. }
  283. func (m *modelDialogCmp) setupModelsForProvider(provider string) {
  284. // cfg := config.Get()
  285. // agentCfg := cfg.Agents[config.AgentPrimary]
  286. // selectedModelId := agentCfg.Model
  287. // m.provider = provider
  288. // m.models = getModelsForProvider(provider)
  289. m.selectedIdx = 0
  290. m.scrollOffset = 0
  291. // Try to select the current model if it belongs to this provider
  292. // if provider == models.SupportedModels[selectedModelId].Provider {
  293. // for i, model := range m.models {
  294. // if model.ID == selectedModelId {
  295. // m.selectedIdx = i
  296. // // Adjust scroll position to keep selected model visible
  297. // if m.selectedIdx >= numVisibleModels {
  298. // m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
  299. // }
  300. // break
  301. // }
  302. // }
  303. // }
  304. }
  305. func getModelsForProvider(provider string) []string {
  306. return []string{"Claude Sonnet 4"}
  307. // var providerModels []models.Model
  308. // for _, model := range models.SupportedModels {
  309. // if model.Provider == provider {
  310. // providerModels = append(providerModels, model)
  311. // }
  312. // }
  313. // reverse alphabetical order (if llm naming was consistent latest would appear first)
  314. // slices.SortFunc(providerModels, func(a, b models.Model) int {
  315. // if a.Name > b.Name {
  316. // return -1
  317. // } else if a.Name < b.Name {
  318. // return 1
  319. // }
  320. // return 0
  321. // })
  322. // return providerModels
  323. }
  324. func NewModelDialogCmp() ModelDialog {
  325. return &modelDialogCmp{}
  326. }