From 973bf4eb2c885291f079cd1b1126569dfd4bd734 Mon Sep 17 00:00:00 2001 From: yetone Date: Fri, 2 Oct 2026 05:30:11 +0800 Subject: [PATCH] =?UTF-8?q?profiles:=20a=20profile's=20details=20list=20al?= =?UTF-8?q?l=20four=20of=20Claude=20Code's=20tiers,=20one=20left=20to=20fo?= =?UTF-8?q?llow=20the=20main=20model=20saying=20so=20(#480:=20opus=20and?= =?UTF-8?q?=20fable,=20following=20it,=20were=20missing=20from=20the=20det?= =?UTF-8?q?ails,=20so=20they=20read=20as=20not=20set)=20=E2=80=94=20a=20ti?= =?UTF-8?q?er=20is=20a=20quiet=20field,=20empty=20while=20it=20follows=20t?= =?UTF-8?q?he=20main=20model,=20and=20the=20details=20left=20every=20empty?= =?UTF-8?q?=20quiet=20field=20out.=20A=20field=20now=20says=20which=20one?= =?UTF-8?q?=20it=20follows=20(Follows,=20"model"=20for=20the=20tiers),=20a?= =?UTF-8?q?nd=20the=20details=20keep=20such=20a=20field,=20empty,=20with?= =?UTF-8?q?=20it;=20the=20panel=20shows=20it=20as=20"follows=20the=20main?= =?UTF-8?q?=20model=20()"=20(=E8=B7=9F=E9=9A=8F=E4=B8=BB=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=88=E2=80=A6=EF=BC=89),=20or=20"follows=20the=20?= =?UTF-8?q?main=20model"=20when=20the=20main=20is=20the=20agent's=20defaul?= =?UTF-8?q?t.=20Other=20quiet=20fields=20(Codex's=20sign-in,=20subagents)?= =?UTF-8?q?=20are=20still=20left=20out=20while=20empty,=20and=20what=20app?= =?UTF-8?q?lying=20a=20profile=20writes=20is=20unchanged.=20TestDetails=20?= =?UTF-8?q?and=20profile-details.test.cjs=20(Chromium,=20WebKit,=20en,=20z?= =?UTF-8?q?h)=20fail=20without=20the=20change.=20go=20vet,=20go=20build=20?= =?UTF-8?q?-tags=20nogui=20(darwin,=20GOOS=3Dlinux),=20GOOS=3Dwindows=20go?= =?UTF-8?q?=20build,=20go=20test=20-tags=20nogui=20./...=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/agent/agent.go | 3 +++ internal/agent/claude.go | 2 +- internal/gui/assets/app.js | 3 +++ internal/gui/assets/i18n.js | 2 ++ internal/gui/tests/README.md | 5 +++-- internal/gui/tests/profile-details.test.cjs | 19 +++++++++++----- internal/profile/details.go | 25 ++++++++++++++------- internal/profile/details_test.go | 25 ++++++++++++++++++--- 8 files changed, 65 insertions(+), 19 deletions(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a8c80e22..2ac9bce0 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -53,6 +53,9 @@ type Field struct { // Quiet fields are left out of listings while empty: they follow // another field until set (Claude Code's per-tier models). Quiet bool + // Follows is the key of the field a Quiet one takes after while empty + // ("model" for Claude Code's tiers), for a profile's details to say so. + Follows string } // Agent is one supported coding agent. diff --git a/internal/agent/claude.go b/internal/agent/claude.go index 2e1b8996..327af72a 100644 --- a/internal/agent/claude.go +++ b/internal/agent/claude.go @@ -573,7 +573,7 @@ func claudeIn(at place) *Agent { }} for _, tier := range claudeTiers { fields = append(fields, Field{ - Key: tier, Label: tier, Quiet: true, + Key: tier, Label: tier, Quiet: true, Follows: "model", // empty while the tier follows the main model Get: func() string { if w := env(tierEnv(tier)); routed() && w != env("ANTHROPIC_MODEL") { diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index de0c0d0d..76c3d5d5 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -589,7 +589,10 @@ function profileDetail(p, footed) { }; for (const f of g.fields || []) { const effort = f.key === "effort" || f.label === "effort" || f.label === "thinking"; + // a Claude Code tier left to follow the main model (#480), and to what + const main = f.follows && g.fields.find((x) => x.key === f.follows && !x.hidden)?.value; if (f.hidden) row(t(f.label), "••••••", "pd-hidden"); + else if (f.follows) row(t(f.label), main ? t("follows the main model ({model})", { model: main }) : t("follows the main model"), "pd-default"); else if (!f.value) row(t(f.label), t("agent default"), "pd-default"); else row(t(f.label), effort ? effortName({ value: f.value }) : f.value); } diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index 7f694bbd..7c269be2 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -295,6 +295,8 @@ const I18N = { "Apply": "应用", "Apply {name} to the agents": "将 {name} 应用到各 Agent", "No agent settings saved": "没有保存任何 Agent 的设置", + "follows the main model": "跟随主模型", + "follows the main model ({model})": "跟随主模型({model})", "•••••• looks like a key or a token, and is not shown": "•••••• 看起来是密钥或令牌,不予显示", "Updated {name} to the current setup": "已用当前配置更新 {name}", "Profile name": "方案名称", diff --git a/internal/gui/tests/README.md b/internal/gui/tests/README.md index 6ef09c16..0ee5224c 100644 --- a/internal/gui/tests/README.md +++ b/internal/gui/tests/README.md @@ -347,8 +347,9 @@ field; Escape closes it; in English and Chinese. window and in the tray panel with its list scrolled to its end: the details open, applying nothing and leaving the chip where it was on the screen; they list each agent by name, its fields under their labels (an empty one as the -agent's default, one that reads as a key as dots) and what the Library gives -it; a second click closes them; their Apply applies the profile, once; ↻ and +agent's default, one that reads as a key as dots, all four of Claude Code's +tiers, one left empty as following the main model and which that is, #480) +and what the Library gives it; a second click closes them; their Apply applies the profile, once; ↻ and × stay on the chip. In Chromium and WebKit, English and Chinese: ```sh diff --git a/internal/gui/tests/profile-details.test.cjs b/internal/gui/tests/profile-details.test.cjs index cd375bc5..dc55a77e 100644 --- a/internal/gui/tests/profile-details.test.cjs +++ b/internal/gui/tests/profile-details.test.cjs @@ -4,7 +4,9 @@ // could be seen only by applying it over the setup in use). A click on the // chip opens its details: the agents' names, each one's fields under their // labels (an empty one the agent's default, one that reads as a key shown as -// dots), what the Library gives it; nothing is applied, and the chip clicked +// dots, each of Claude Code's four tiers, one left to follow the main model +// saying so and what that is: #480, they were missing), what the Library +// gives it; nothing is applied, and the chip clicked // stays where it is on the screen. Apply in the details applies it, once; a // second click on the chip closes them; ↻ and × are still on the chip. In the // window and in the tray panel (its list scrolled to its end), in English @@ -23,7 +25,14 @@ const work = { name: "work", summary: "codex gpt-5", library: { servers: 1, skills: 1, instructions: true }, agents: [ - { id: "claude", name: "Claude Code", fields: [{ key: "model", label: "model", value: "" }], servers: ["github"], skills: ["review"] }, + { id: "claude", name: "Claude Code", fields: [ + { key: "model", label: "model", value: "magpie/a/main" }, + { key: "opus", label: "opus", value: "", follows: "model" }, + { key: "sonnet", label: "sonnet", value: "magpie/a/s" }, + { key: "haiku", label: "haiku", value: "", follows: "model" }, + { key: "fable", label: "fable", value: "", follows: "model" }, + { key: "subagent", label: "subagents", value: "" }, + ], servers: ["github"], skills: ["review"] }, { id: "codex", name: "Codex", fields: [ { key: "model", label: "model", value: "magpie/openai/gpt-5" }, { key: "effort", label: "effort", value: "high" }, @@ -57,8 +66,8 @@ function server(lang, calls, count) { } const words = { - en: { model: "model", effort: "effort", high: "high", def: "agent default", servers: "MCP servers", skills: "Skills", instructions: "Instructions", on: "on", apply: "Apply" }, - zh: { model: "模型", effort: "推理强度", high: "高", def: "Agent 默认值", servers: "MCP 服务器", skills: "技能", instructions: "指令", on: "开启", apply: "应用" }, + en: { model: "model", effort: "effort", high: "high", def: "agent default", follows: "follows the main model (magpie/a/main)", subagents: "subagents", servers: "MCP servers", skills: "Skills", instructions: "Instructions", on: "on", apply: "Apply" }, + zh: { model: "模型", effort: "推理强度", high: "高", def: "Agent 默认值", follows: "跟随主模型(magpie/a/main)", subagents: "子 agent", servers: "MCP 服务器", skills: "技能", instructions: "指令", on: "开启", apply: "应用" }, }; for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) { @@ -125,7 +134,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", assert.equal(read.apply, w.apply); assert(read.inView, "the details must be drawn"); assert.deepEqual(read.groups, [ - { name: "Claude Code", rows: [`${w.model}=${w.def}`, `${w.servers}=github`, `${w.skills}=review`] }, + { name: "Claude Code", rows: [`${w.model}=magpie/a/main`, `opus=${w.follows}`, "sonnet=magpie/a/s", `haiku=${w.follows}`, `fable=${w.follows}`, `${w.subagents}=${w.def}`, `${w.servers}=github`, `${w.skills}=review`] }, { name: "Codex", rows: [`${w.model}=magpie/openai/gpt-5`, `${w.effort}=${w.high}`, "token=••••••", `${w.instructions}=${w.on}`] }, ]); diff --git a/internal/profile/details.go b/internal/profile/details.go index c2ae9c0f..b03bebd9 100644 --- a/internal/profile/details.go +++ b/internal/profile/details.go @@ -24,13 +24,16 @@ type Group struct { Instructions bool `json:"instructions,omitempty"` } -// Item is one saved field. Value "" is the agent's own default. Hidden is a -// value that looked like a key or a token: it is left out, never shown. +// Item is one saved field. Value "" is the agent's own default, or, with +// Follows, the key of the field it takes after (a Claude Code tier that +// follows the main model, #480). Hidden is a value that looked like a key or +// a token: it is left out, never shown. type Item struct { - Key string `json:"key"` - Label string `json:"label"` - Value string `json:"value"` - Hidden bool `json:"hidden,omitempty"` + Key string `json:"key"` + Label string `json:"label"` + Value string `json:"value"` + Follows string `json:"follows,omitempty"` + Hidden bool `json:"hidden,omitempty"` } // secretName is a field named for a credential; secretValue a value that @@ -47,8 +50,11 @@ func secret(key, label, value string) bool { } // Details is what p holds, by agent: the agents magpie knows in the order it -// lists them, then any it no longer does by id. A quiet field left empty (it -// follows another one) is left out, as the agent's row leaves it out. +// lists them, then any it no longer does by id. A quiet field left empty is +// left out, as the agent's row leaves it out, unless it says which field it +// follows: Claude Code's four tiers are all there, one that follows the main +// model saying so (#480: opus and fable, following it, were missing, and +// read as not set). What applying the profile writes is unchanged. func Details(p Profile) []Group { known := map[string]*agent.Agent{} var order []string @@ -117,6 +123,9 @@ func Details(p Profile) []Group { } seen[f.Key] = true if f.Quiet && v == "" { + if f.Follows != "" { + g.Fields = append(g.Fields, Item{Key: f.Key, Label: f.Label, Follows: f.Follows}) + } continue } add(f.Key, f.Label, v) diff --git a/internal/profile/details_test.go b/internal/profile/details_test.go index 59b516d5..637bc3eb 100644 --- a/internal/profile/details_test.go +++ b/internal/profile/details_test.go @@ -11,7 +11,9 @@ import ( // A profile's details are its fields by agent, in the order magpie lists the // agents and their fields, under the agents' names and the fields' labels, -// with what the library gives each; an empty quiet field is left out, an +// with what the library gives each; an empty quiet field is left out, but +// Claude Code's four tiers are all there, an empty one following the main +// model (#480: opus and fable, following it, were missing); an // agent magpie no longer knows comes last by id, and nothing that reads as a // key or a token is in them (#467). func TestDetails(t *testing.T) { @@ -23,6 +25,9 @@ func TestDetails(t *testing.T) { "codex.login": "", // quiet: follows the model "claude.model": "opus", "claude.haiku": "", + "claude.opus": "", + "claude.sonnet": "magpie/a/s", + "claude.fable": "", "gone.model": "m", "gone.apiKey": "plain-looking", "codex.extra": "sk-live-abcdef", @@ -67,8 +72,22 @@ func TestDetails(t *testing.T) { if _, ok := item(codex, "login"); ok { t.Error("an empty quiet field is left out") } - if _, ok := item(claude, "haiku"); ok { - t.Error("an empty tier is left out") + var tiers []string + for _, it := range claude.Fields { + switch it.Key { + case "opus", "sonnet", "haiku", "fable": + tiers = append(tiers, it.Key) + want := Item{Key: it.Key, Label: it.Key, Follows: "model"} + if it.Key == "sonnet" { + want = Item{Key: "sonnet", Label: "sonnet", Value: "magpie/a/s"} + } + if it != want { + t.Errorf("tier %+v, want %+v", it, want) + } + } + } + if !slices.Equal(tiers, []string{"opus", "sonnet", "haiku", "fable"}) { + t.Errorf("tiers %v, want all four in Claude Code's order, an empty one following the model", tiers) } for _, c := range []struct { g Group