models.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package dialog
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "time"
  7. "github.com/charmbracelet/bubbles/v2/key"
  8. tea "github.com/charmbracelet/bubbletea/v2"
  9. "github.com/sst/opencode-sdk-go"
  10. "github.com/sst/opencode/internal/app"
  11. "github.com/sst/opencode/internal/components/list"
  12. "github.com/sst/opencode/internal/components/modal"
  13. "github.com/sst/opencode/internal/layout"
  14. "github.com/sst/opencode/internal/styles"
  15. "github.com/sst/opencode/internal/theme"
  16. "github.com/sst/opencode/internal/util"
  17. )
  18. const (
  19. numVisibleModels = 10
  20. minDialogWidth = 40
  21. maxDialogWidth = 80
  22. )
  23. // ModelDialog interface for the model selection dialog
  24. type ModelDialog interface {
  25. layout.Modal
  26. }
  27. type modelDialog struct {
  28. app *app.App
  29. allModels []ModelWithProvider
  30. width int
  31. height int
  32. modal *modal.Modal
  33. modelList list.List[ModelItem]
  34. dialogWidth int
  35. }
  36. type ModelWithProvider struct {
  37. Model opencode.Model
  38. Provider opencode.Provider
  39. }
  40. type ModelItem struct {
  41. ModelName string
  42. ProviderName string
  43. }
  44. func (m ModelItem) Render(selected bool, width int) string {
  45. t := theme.CurrentTheme()
  46. if selected {
  47. displayText := fmt.Sprintf("%s (%s)", m.ModelName, m.ProviderName)
  48. return styles.NewStyle().
  49. Background(t.Primary()).
  50. Foreground(t.BackgroundPanel()).
  51. Width(width).
  52. PaddingLeft(1).
  53. Render(displayText)
  54. } else {
  55. modelStyle := styles.NewStyle().
  56. Foreground(t.Text()).
  57. Background(t.BackgroundPanel())
  58. providerStyle := styles.NewStyle().
  59. Foreground(t.TextMuted()).
  60. Background(t.BackgroundPanel())
  61. modelPart := modelStyle.Render(m.ModelName)
  62. providerPart := providerStyle.Render(fmt.Sprintf(" (%s)", m.ProviderName))
  63. combinedText := modelPart + providerPart
  64. return styles.NewStyle().
  65. Background(t.BackgroundPanel()).
  66. PaddingLeft(1).
  67. Render(combinedText)
  68. }
  69. }
  70. type modelKeyMap struct {
  71. Enter key.Binding
  72. Escape key.Binding
  73. }
  74. var modelKeys = modelKeyMap{
  75. Enter: key.NewBinding(
  76. key.WithKeys("enter"),
  77. key.WithHelp("enter", "select model"),
  78. ),
  79. Escape: key.NewBinding(
  80. key.WithKeys("esc"),
  81. key.WithHelp("esc", "close"),
  82. ),
  83. }
  84. func (m *modelDialog) Init() tea.Cmd {
  85. m.setupAllModels()
  86. return nil
  87. }
  88. func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  89. switch msg := msg.(type) {
  90. case tea.KeyMsg:
  91. switch {
  92. case key.Matches(msg, modelKeys.Enter):
  93. _, selectedIndex := m.modelList.GetSelectedItem()
  94. if selectedIndex >= 0 && selectedIndex < len(m.allModels) {
  95. selectedModel := m.allModels[selectedIndex]
  96. return m, tea.Sequence(
  97. util.CmdHandler(modal.CloseModalMsg{}),
  98. util.CmdHandler(
  99. app.ModelSelectedMsg{
  100. Provider: selectedModel.Provider,
  101. Model: selectedModel.Model,
  102. }),
  103. )
  104. }
  105. return m, util.CmdHandler(modal.CloseModalMsg{})
  106. case key.Matches(msg, modelKeys.Escape):
  107. return m, util.CmdHandler(modal.CloseModalMsg{})
  108. }
  109. case tea.WindowSizeMsg:
  110. m.width = msg.Width
  111. m.height = msg.Height
  112. }
  113. // Update the list component
  114. updatedList, cmd := m.modelList.Update(msg)
  115. m.modelList = updatedList.(list.List[ModelItem])
  116. return m, cmd
  117. }
  118. func (m *modelDialog) View() string {
  119. return m.modelList.View()
  120. }
  121. func (m *modelDialog) calculateOptimalWidth(modelItems []ModelItem) int {
  122. maxWidth := minDialogWidth
  123. for _, item := range modelItems {
  124. // Calculate the width needed for this item: "ModelName (ProviderName)"
  125. // Add 4 for the parentheses, space, and some padding
  126. itemWidth := len(item.ModelName) + len(item.ProviderName) + 4
  127. if itemWidth > maxWidth {
  128. maxWidth = itemWidth
  129. }
  130. }
  131. if maxWidth > maxDialogWidth {
  132. maxWidth = maxDialogWidth
  133. }
  134. return maxWidth
  135. }
  136. func (m *modelDialog) setupAllModels() {
  137. providers, _ := m.app.ListProviders(context.Background())
  138. m.allModels = make([]ModelWithProvider, 0)
  139. for _, provider := range providers {
  140. for _, model := range provider.Models {
  141. m.allModels = append(m.allModels, ModelWithProvider{
  142. Model: model,
  143. Provider: provider,
  144. })
  145. }
  146. }
  147. m.sortModels()
  148. modelItems := make([]ModelItem, len(m.allModels))
  149. for i, modelWithProvider := range m.allModels {
  150. modelItems[i] = ModelItem{
  151. ModelName: modelWithProvider.Model.Name,
  152. ProviderName: modelWithProvider.Provider.Name,
  153. }
  154. }
  155. m.dialogWidth = m.calculateOptimalWidth(modelItems)
  156. m.modelList = list.NewListComponent(modelItems, numVisibleModels, "No models available", true)
  157. m.modelList.SetMaxWidth(m.dialogWidth)
  158. if len(m.allModels) > 0 {
  159. m.modelList.SetSelectedIndex(0)
  160. }
  161. }
  162. func (m *modelDialog) sortModels() {
  163. sort.Slice(m.allModels, func(i, j int) bool {
  164. modelA := m.allModels[i]
  165. modelB := m.allModels[j]
  166. usageA := m.getModelUsageTime(modelA.Provider.ID, modelA.Model.ID)
  167. usageB := m.getModelUsageTime(modelB.Provider.ID, modelB.Model.ID)
  168. // If both have usage times, sort by most recent first
  169. if !usageA.IsZero() && !usageB.IsZero() {
  170. return usageA.After(usageB)
  171. }
  172. // If only one has usage time, it goes first
  173. if !usageA.IsZero() && usageB.IsZero() {
  174. return true
  175. }
  176. if usageA.IsZero() && !usageB.IsZero() {
  177. return false
  178. }
  179. // If neither has usage time, sort by release date desc if available
  180. if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate != "" {
  181. dateA := m.parseReleaseDate(modelA.Model.ReleaseDate)
  182. dateB := m.parseReleaseDate(modelB.Model.ReleaseDate)
  183. if !dateA.IsZero() && !dateB.IsZero() {
  184. return dateA.After(dateB)
  185. }
  186. }
  187. // If only one has release date, it goes first
  188. if modelA.Model.ReleaseDate != "" && modelB.Model.ReleaseDate == "" {
  189. return true
  190. }
  191. if modelA.Model.ReleaseDate == "" && modelB.Model.ReleaseDate != "" {
  192. return false
  193. }
  194. // If neither has usage time nor release date, fall back to alphabetical sorting
  195. return modelA.Model.Name < modelB.Model.Name
  196. })
  197. }
  198. func (m *modelDialog) parseReleaseDate(dateStr string) time.Time {
  199. if parsed, err := time.Parse("2006-01-02", dateStr); err == nil {
  200. return parsed
  201. }
  202. return time.Time{}
  203. }
  204. func (m *modelDialog) getModelUsageTime(providerID, modelID string) time.Time {
  205. for _, usage := range m.app.State.RecentlyUsedModels {
  206. if usage.ProviderID == providerID && usage.ModelID == modelID {
  207. return usage.LastUsed
  208. }
  209. }
  210. return time.Time{}
  211. }
  212. func (m *modelDialog) Render(background string) string {
  213. return m.modal.Render(m.View(), background)
  214. }
  215. func (s *modelDialog) Close() tea.Cmd {
  216. return nil
  217. }
  218. func NewModelDialog(app *app.App) ModelDialog {
  219. dialog := &modelDialog{
  220. app: app,
  221. }
  222. dialog.setupAllModels()
  223. dialog.modal = modal.New(
  224. modal.WithTitle("Select Model"),
  225. modal.WithMaxWidth(dialog.dialogWidth+4),
  226. )
  227. return dialog
  228. }