// PicoClaw - Ultra-lightweight personal AI agent // License: MIT // // Copyright (c) 2026 PicoClaw contributors package ui import ( "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" tuicfg "github.com/sipeed/picoclaw/cmd/picoclaw-launcher-tui/config" ) type modelsAPIResponse struct { Data []modelEntry `json:"data"` } type modelEntry struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } func (a *App) newModelsPage(schemeName, userName, baseURL string) tview.Primitive { table := tview.NewTable(). SetBorders(false). SetSelectable(true, false). SetFixed(0, 0) table.SetBorder(true).SetTitle(fmt.Sprintf(" Models %s / %s ", schemeName, userName)) var modelIDs []string status := tview.NewTextView(). SetTextAlign(tview.AlignCenter). SetDynamicColors(true). SetText("[yellow]Fetching models…[-]") footer := hintBar(" Enter: select ESC: back ") flex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(status, 1, 0, false). AddItem(table, 0, 1, false). AddItem(footer, 1, 0, false) apiKey := a.resolveKey(schemeName, userName) go func() { entries, err := fetchModels(baseURL, apiKey) a.tapp.QueueUpdateDraw(func() { if err != nil { status.SetText(fmt.Sprintf("[red]Error: %s[-]", err.Error())) table.SetCell(0, 0, tview.NewTableCell("(failed to load models)")) a.tapp.SetFocus(table) return } if len(entries) == 0 { status.SetText("[yellow]No models returned[-]") table.SetCell(0, 0, tview.NewTableCell("(no models available)")) a.tapp.SetFocus(table) return } status.SetText(fmt.Sprintf("[green]%d model(s) loaded[-]", len(entries))) for i, m := range entries { modelIDs = append(modelIDs, m.ID) table.SetCell(i, 0, tview.NewTableCell(fmt.Sprintf("%3d", i+1)). SetAlign(tview.AlignRight). SetTextColor(tcell.ColorGray). SetSelectable(false), ) table.SetCell(i, 1, tview.NewTableCell(" "+m.ID). SetAlign(tview.AlignLeft). SetExpansion(1), ) } a.tapp.SetFocus(table) }) }() table.SetSelectedFunc(func(row, _ int) { if row < 0 || row >= len(modelIDs) { return } a.cfg.Provider.Current = tuicfg.ProviderCurrent{ Scheme: schemeName, User: userName, Model: modelIDs[row], } a.save() a.goBack() }) return flex } func (a *App) resolveKey(schemeName, userName string) string { for _, u := range a.cfg.Provider.Users { if u.Scheme == schemeName && u.Name == userName { return u.Key } } return "" } func fetchModels(baseURL, apiKey string) ([]modelEntry, error) { url := strings.TrimRight(baseURL, "/") + "/models" client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("build request: %w", err) } if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } var result modelsAPIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) } return result.Data, nil }