| 1 | package tui |
| 2 | |
| 3 | import ( |
| 4 | "github.com/charmbracelet/bubbles/textinput" |
| 5 | "sourcecraft.dev/bigbes/claudio/cache" |
| 6 | "sourcecraft.dev/bigbes/claudio/config" |
| 7 | "sourcecraft.dev/bigbes/claudio/provider" |
| 8 | "sourcecraft.dev/bigbes/claudio/provider/copilot" |
| 9 | ) |
| 10 | |
| 11 | func buildEntries(cfg *config.Config, showHidden bool) []entry { |
| 12 | seen := make(map[string]bool) |
| 13 | var entries []entry |
| 14 | for _, key := range provider.Order { |
| 15 | if !showHidden && cfg.Providers[key].Hidden { |
| 16 | seen[key] = true |
| 17 | continue |
| 18 | } |
| 19 | p := provider.Registry[key] |
| 20 | // Merge ollama and ollama-cloud into a single entry with toggle. |
| 21 | if key == "ollama" { |
| 22 | entries = append(entries, entry{Key: key, Name: p.Name, Description: p.Description, AltKey: "ollama-cloud"}) |
| 23 | seen[key] = true |
| 24 | seen["ollama-cloud"] = true |
| 25 | continue |
| 26 | } |
| 27 | // Skip ollama-cloud as it's handled with ollama. |
| 28 | if key == "ollama-cloud" { |
| 29 | continue |
| 30 | } |
| 31 | // Merge zai and zai-coding into a single entry with toggle. |
| 32 | if key == "zai" { |
| 33 | entries = append(entries, entry{Key: key, Name: p.Name, Description: p.Description, AltKey: "zai-coding"}) |
| 34 | seen[key] = true |
| 35 | seen["zai-coding"] = true |
| 36 | continue |
| 37 | } |
| 38 | // Skip zai-coding as it's handled with zai. |
| 39 | if key == "zai-coding" { |
| 40 | continue |
| 41 | } |
| 42 | // Merge kimi variants into a single entry with toggle. |
| 43 | if key == "kimi" { |
| 44 | entries = append(entries, entry{ |
| 45 | Key: key, |
| 46 | Name: p.Name, |
| 47 | Description: "Kimi K2.5 / K2 via Moonshot", |
| 48 | AltKeys: []string{"kimi-api-cn", "kimi-api-intl"}, |
| 49 | }) |
| 50 | seen[key] = true |
| 51 | seen["kimi-api-cn"] = true |
| 52 | seen["kimi-api-intl"] = true |
| 53 | continue |
| 54 | } |
| 55 | // Skip kimi-api-cn and kimi-api-intl as they're handled with kimi. |
| 56 | if key == "kimi-api-cn" || key == "kimi-api-intl" { |
| 57 | continue |
| 58 | } |
| 59 | entries = append(entries, entry{Key: key, Name: p.Name, Description: p.Description}) |
| 60 | seen[key] = true |
| 61 | } |
| 62 | for key, pc := range cfg.Providers { |
| 63 | if seen[key] || (!showHidden && pc.Hidden) { |
| 64 | continue |
| 65 | } |
| 66 | name := pc.Name |
| 67 | if name == "" { |
| 68 | name = key |
| 69 | } |
| 70 | desc := pc.Description |
| 71 | if desc == "" && pc.Compat == "openai" { |
| 72 | desc = "Custom OpenAI-compatible provider" |
| 73 | } |
| 74 | entries = append(entries, entry{Key: key, Name: name, Description: desc}) |
| 75 | } |
| 76 | return entries |
| 77 | } |
| 78 | |
| 79 | func New(cfg *config.Config, cdb *cache.DB) Model { |
| 80 | ti := textinput.New() |
| 81 | ti.Placeholder = "sk-..." |
| 82 | ti.CharLimit = 256 |
| 83 | ti.Width = 60 |
| 84 | ti.EchoMode = textinput.EchoPassword |
| 85 | ti.EchoCharacter = '•' |
| 86 | |
| 87 | entries := buildEntries(cfg, false) |
| 88 | |
| 89 | cursor := 0 |
| 90 | // Default to cloud mode. If ActiveProvider is "ollama", switch to local mode. |
| 91 | // This way the last chosen variant is persisted via ActiveProvider. |
| 92 | ollamaCloud := true |
| 93 | // Default to Coding mode. If ActiveProvider is "zai", switch to API mode. |
| 94 | // This way the last chosen variant is persisted via ActiveProvider. |
| 95 | zaiCoding := true |
| 96 | // Default to Coding (0). Other variants: 1 = api-cn, 2 = api-intl |
| 97 | kimiVariant := 0 |
| 98 | for i, e := range entries { |
| 99 | if e.Key == cfg.ActiveProvider { |
| 100 | cursor = i |
| 101 | // If the active provider is the local ollama, set cloud mode to false |
| 102 | if e.Key == "ollama" && e.AltKey == "ollama-cloud" { |
| 103 | ollamaCloud = false |
| 104 | } |
| 105 | // If the active provider is zai (API), set coding mode to false |
| 106 | if e.Key == "zai" && e.AltKey == "zai-coding" { |
| 107 | zaiCoding = false |
| 108 | } |
| 109 | // If the active provider is a kimi variant, set the appropriate index |
| 110 | if e.Key == "kimi" && len(e.AltKeys) == 2 { |
| 111 | switch cfg.ActiveProvider { |
| 112 | case "kimi-api-cn": |
| 113 | kimiVariant = 1 |
| 114 | case "kimi-api-intl": |
| 115 | kimiVariant = 2 |
| 116 | default: |
| 117 | kimiVariant = 0 |
| 118 | } |
| 119 | } |
| 120 | break |
| 121 | } |
| 122 | // If active provider is ollama-cloud, find the merged ollama entry. |
| 123 | if e.Key == "ollama" && e.AltKey == "ollama-cloud" && cfg.ActiveProvider == "ollama-cloud" { |
| 124 | cursor = i |
| 125 | ollamaCloud = true |
| 126 | break |
| 127 | } |
| 128 | // If active provider is zai-coding, find the merged zai entry. |
| 129 | if e.Key == "zai" && e.AltKey == "zai-coding" && cfg.ActiveProvider == "zai-coding" { |
| 130 | cursor = i |
| 131 | zaiCoding = true |
| 132 | break |
| 133 | } |
| 134 | // If active provider is a kimi variant, find the merged kimi entry. |
| 135 | if e.Key == "kimi" && len(e.AltKeys) == 2 { |
| 136 | if cfg.ActiveProvider == "kimi-api-cn" { |
| 137 | cursor = i |
| 138 | kimiVariant = 1 |
| 139 | break |
| 140 | } |
| 141 | if cfg.ActiveProvider == "kimi-api-intl" { |
| 142 | cursor = i |
| 143 | kimiVariant = 2 |
| 144 | break |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | fi := textinput.New() |
| 150 | fi.Placeholder = "filter..." |
| 151 | fi.CharLimit = 64 |
| 152 | fi.Width = 30 |
| 153 | |
| 154 | return Model{ |
| 155 | cfg: cfg, |
| 156 | cache: cdb, |
| 157 | entries: entries, |
| 158 | phase: phaseSelect, |
| 159 | cursor: cursor, |
| 160 | ollamaCloud: ollamaCloud, |
| 161 | zaiCoding: zaiCoding, |
| 162 | kimiVariant: kimiVariant, |
| 163 | input: ti, |
| 164 | filterInput: fi, |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | // NewModelPicker creates a Model that goes directly to the copilot model selection phase. |
| 169 | func NewModelPicker(cfg *config.Config, cdb *cache.DB, models []copilot.CopilotModel) Model { |
| 170 | families, grouped := copilot.VendorFamilies(models) |
| 171 | items := make(map[string][]PickerItem) |
| 172 | for fam, cms := range grouped { |
| 173 | for _, cm := range cms { |
| 174 | items[fam] = append(items[fam], PickerItem{ID: cm.ID, Name: cm.DisplayName(), ContextLength: cm.ContextWindow}) |
| 175 | } |
| 176 | } |
| 177 | m := Model{ |
| 178 | cfg: cfg, |
| 179 | cache: cdb, |
| 180 | phase: phaseSelectModel, |
| 181 | pickerProvider: "copilot", |
| 182 | families: families, |
| 183 | familyModels: items, |
| 184 | } |
| 185 | m.preSelectModel() |
| 186 | return m |
| 187 | } |
| 188 | |