Files
magpie/visible_cli.go
T
yetone 5da6c24e80 agents, gui, cli: an agent can be shown only the models picked for it, so a model added later stays out of its list until ticked (#1337, nianlee-official)
nianlee-official asked for an agent whose model list grows only by hand:
today the Agents page's per-agent list hides models one at a time, and
every new model, of a new provider or an old one, shows up in the
agent's picker and config on its own ("New models are shown").

What changed:
- settings: PickedModels, per agent. An agent named in it is shown only
  the catalog entries listed for it (after Visible); HiddenModels is not
  read for it.
- provider: ModelOff(agent) is the one test CatalogFor, the Agents page
  count and Codex's own ChatGPT models (CodexNativeHidden) use, so the
  gateway's /v1/models for that agent's key, the lists magpie writes into
  its config and Codex's /model all narrow the same way hidden models do.
  SetOnlyPicked switches it on or off keeping what is shown as it is
  (on: what is shown becomes the picks; off: the listed models not
  picked become hidden). SetPickedModels keeps a pick whose provider is
  missing for now. renameModelPrefs moves picks with a provider rename.
  A model asked for by name still resolves against the full catalog.
- GUI: the list's foot "New models are shown" note is now a switch
  "Only models I pick", with a tooltip saying what each side does. On,
  a click sends what is ticked ({shown}), not what is hidden, so a model
  that arrived after the list was opened isn't picked by accident; the
  server still reads {hidden} from an older page. The model in use is
  always kept. Hide all / Show all's armed state takes the switch's room.
  zh, zh-TW, ja and de strings; the old note string is gone.
- CLI: magpie visible <agent> --only-picked | --show-new; magpie visible
  lists such an agent as "only models picked"; magpie models <agent>
  says how many are not picked, and what to do when none is.
- /api/settings keeps PickedModels like HiddenModels.
- docs/reference.md: the per-agent list bullet.
Not touched: #1302 (bulk agent visibility) and its files.

Run:
- go test -tags nogui ./... in a temp HOME: 44 packages ok.
- New: provider TestOnlyPickedModels, gui TestAgentModelsOnlyPicked,
  gateway TestOnlyPickedModelsList (opencode's key: a new provider's and
  an old provider's new model are left out of /v1/models, cursor's key
  has them, and a chat request naming fresh/x1 reaches the upstream) and
  TestCodexModelListOnlyPicked. With ModelOff made to ignore picks, all
  four fail.
- GUI: new agent-models-only-picked.test.cjs, Chromium and WebKit, en/zh/de,
  420 and 1100 wide (12/12). With save() sending {hidden} in pick mode it
  fails. agent-model*, agent-models*, codex-model-order, cursor-local-models,
  desktop-model-menu, click-scroll, gui-ja, gui-de, gui-zh-tw: 85/85.
- gofmt -l clean; go vet -tags nogui ./... on darwin, GOOS=linux and
  GOOS=windows; go build, and linux/windows builds.
- The built CLI in a sandbox HOME: visible codex --only-picked, a model
  added after it is left out of `magpie models codex` and kept in claude's;
  --show-new keeps it out and new ones show again.
Not tried: the real window against a live gateway, or a real agent
reading the narrowed config.
2026-10-09 08:50:14 +08:00

199 lines
6.8 KiB
Go

package main
import (
"fmt"
"maps"
"slices"
"strings"
"github.com/yetone/magpie/internal/agent"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
stats "github.com/yetone/magpie/internal/usage"
)
// Which models each agent is shown, from the terminal: magpie visible.
const visibleUsage = `usage:
magpie visible which models each agent is shown
magpie visible <agent> <name>[,<name>…] show the agent only these: families (a tag set with
magpie provider set <id> family=relay, or magpie group set),
provider ids and group ids
magpie visible <agent> all show the agent every model again
magpie visible <agent> --only-picked show the agent only the models ticked for it now; a model
that comes later, of any provider, stays off until it is
ticked in its list on the Agents page
magpie visible <agent> --show-new show it the models that come later again (the default)
An agent not narrowed is shown every model. The gateway's model list and the model lists
magpie writes into the agents' files are narrowed alike; a model kept from an agent still
answers when the agent asks for it by name. magpie models <agent> shows what it is shown,
and why the others aren't. --only-picked and --show-new keep the models shown as they are,
and keep the providers, families and groups the agent is narrowed to.
e.g. magpie provider set opencode-go family=ocgo
magpie group set gpt-plus-auto family=relay
magpie visible zcode relay,ocgo`
func agentIDs() []string {
var ids []string
for _, a := range agent.All() {
ids = append(ids, a.ID)
}
return ids
}
// agentID is the agent a typed name is, by its id or an alias.
func agentOf(name string) string {
return stats.AgentOf(strings.ToLower(strings.TrimSpace(name)))
}
func knownAgent(id string) bool { return slices.Contains(agentIDs(), id) }
// names is every name a visibility can hold: the families, and the
// providers' and groups' ids.
func visibleNames() []string {
names := provider.Families()
for _, p := range provider.All() {
names = append(names, p.ID)
}
for _, g := range provider.Groups() {
names = append(names, g.ID)
}
return names
}
func visibleCmd(args []string) error {
if len(args) > 0 && slices.Contains([]string{"help", "-h", "--help"}, args[0]) {
fmt.Println(visibleUsage)
return nil
}
s := settings.Load()
if len(args) == 0 {
if len(s.Visible) == 0 && len(s.PickedModels) == 0 {
fmt.Println(muted.Render(" every agent is shown every model · magpie visible <agent> <family>,… narrows one"))
}
ids := slices.Collect(maps.Keys(s.Visible))
for id := range s.PickedModels {
if !slices.Contains(ids, id) {
ids = append(ids, id)
}
}
slices.Sort(ids)
for _, id := range ids {
shown, hidden := provider.CatalogFor(id)
var says []string
if names, ok := s.Visible[id]; ok {
says = append(says, strings.Join(names, ", "))
}
if _, ok := s.PickedModels[id]; ok {
says = append(says, "only models picked")
}
fmt.Printf(" %s %s %s\n", pad(id, 12), strings.Join(says, " · "),
muted.Render(fmt.Sprintf("%d shown, %d not", len(shown), len(hidden))))
}
if fs := provider.Families(); len(fs) > 0 {
fmt.Println(faint.Render(" families: " + strings.Join(fs, ", ")))
}
return nil
}
id := agentOf(args[0])
if !knownAgent(id) {
return fmt.Errorf("no agent %q (%s)", args[0], strings.Join(agentIDs(), ", "))
}
if len(args) == 1 {
return models([]string{id})
}
if len(args) == 2 && (args[1] == "--only-picked" || args[1] == "--show-new") {
if err := provider.SetOnlyPicked(id, args[1] == "--only-picked"); err != nil {
return err
}
fmt.Println(green.Render("✓"), "saved")
return models([]string{id})
}
list := splitList(strings.Join(args[1:], ","))
if len(list) == 1 && strings.EqualFold(list[0], "all") {
delete(s.Visible, id)
} else {
known := visibleNames()
for i, n := range list {
n = strings.TrimPrefix(n, provider.GroupPrefix)
if !slices.ContainsFunc(known, func(k string) bool { return strings.EqualFold(k, n) }) {
return fmt.Errorf("%s is no family, provider or group (families: %s; magpie provider set <id> family=%s makes one)",
n, orNone(provider.Families()), n)
}
list[i] = n
}
if s.Visible == nil {
s.Visible = map[string][]string{}
}
s.Visible[id] = list
}
if err := settings.Save(s); err != nil {
return err
}
if catalog.Changed != nil {
catalog.Changed() // the agents' files follow
}
fmt.Println(green.Render("✓"), "saved")
return models([]string{id})
}
func orNone(xs []string) string {
if len(xs) == 0 {
return "none yet"
}
return strings.Join(xs, ", ")
}
// explainHidden says what is kept from an agent, and why: what the kept
// models go by, none of which its visibility names, and how many its list
// on the Agents page leaves out besides — the ones not picked, for an
// agent shown only the models picked for it.
func explainHidden(id string, hidden []provider.Entry) {
_, kept := provider.ListedFor(id)
if n := len(hidden) - len(kept); n > 0 {
if _, only := provider.PickedModels(id); only {
fmt.Println(faint.Render(" " + id + " is shown only the models picked for it: " + fmt.Sprint(n) + " not picked, and a new one stays off until it is ticked · magpie visible " + id + " --show-new shows new ones"))
} else {
fmt.Println(faint.Render(" " + fmt.Sprint(n) + " taken out of " + id + "'s list on the Agents page"))
}
} else if _, only := provider.PickedModels(id); only {
fmt.Println(faint.Render(" " + id + " is shown only the models picked for it; a new one stays off until it is ticked · magpie visible " + id + " --show-new shows new ones"))
}
hidden = kept
names, ok := provider.VisibleTo(id)
if !ok {
fmt.Println(faint.Render(" " + id + " is shown every provider's models · magpie visible " + id + " <family>,… narrows it"))
return
}
fmt.Println(faint.Render(" " + id + " is shown " + strings.Join(names, ", ")))
if len(hidden) == 0 {
return
}
by := map[string][]string{}
var order []string
for _, e := range hidden {
k := strings.Join(e.Names()[:1], "")
if e.Family == "" {
k = "no family · " + k
} else {
k = "family " + k
}
if _, seen := by[k]; !seen {
order = append(order, k)
}
by[k] = append(by[k], e.ID)
}
fmt.Println(amber.Render(" ! "+fmt.Sprint(len(hidden))) + muted.Render(" kept from "+id+", in none of those:"))
for _, k := range order {
ids := by[k]
more := ""
if len(ids) > 4 {
ids, more = ids[:4], fmt.Sprintf(" +%d", len(by[k])-4)
}
fmt.Println(muted.Render(" " + k + ": " + strings.Join(ids, ", ") + more))
}
}