From e538f383560413298499f3aa3af14678d0283426 Mon Sep 17 00:00:00 2001 From: yyyr Date: Thu, 16 Jul 2026 12:51:19 +0800 Subject: [PATCH 1/5] feat(web): CliSnippet Claude Code fable/opus tiers via settings.json env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the dashboard's Claude Code setup helper from three tiers to four (Fable / Opus / Sonnet / Haiku) and switch the emitted format from shell exports to a JSON `env` fragment for `~/.claude/settings.json`. Fable is Claude Code's new top tier above Opus; missing it left users without a way to route the CLI's Fable slot through Floway. Missing the Opus slot meant the CLI fell back to its built-in default opus id instead of a Floway-catalogued one. Shell exports also don't reach background agents reliably — the supervisor only forwards them when it was started from the same gateway environment and dispatched into the current cwd; @repo / --cwd and the SDK / -p paths drop them. settings.json's `env` block is the only channel that covers every execution context. Emit only the `env` sub-object so users paste it into an existing settings file without clobbering unrelated top-level keys. Also collapse the three ad-hoc tier sort helpers into one distance-to-target factory used for all four buckets, and drop ANTHROPIC_MODEL (redundant with the per-tier defaults). --- apps/web/src/components/keys/CliSnippet.vue | 77 ++++++++++++--------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/keys/CliSnippet.vue b/apps/web/src/components/keys/CliSnippet.vue index 245b1133f..a7c207b14 100644 --- a/apps/web/src/components/keys/CliSnippet.vue +++ b/apps/web/src/components/keys/CliSnippet.vue @@ -13,26 +13,22 @@ const baseUrl = computed(() => window.location.origin); // Picker buckets — Claude Code only accepts claude-* generation ids, Codex's // Floway integration is the gpt-5 family only. Backend already collapses -// dated / variant suffixes; dedupe by id and sort by family tier so the -// picker defaults land on the canonical Opus / Sonnet / Haiku per slot. -const CLAUDE_TIER: Record = { opus: 0, sonnet: 1, haiku: 2 }; +// dated / variant suffixes; dedupe by id and sort by family tier so each +// slot's default lands on the canonical Fable / Opus / Sonnet / Haiku. +const CLAUDE_TIER: Record = { fable: 0, opus: 1, sonnet: 2, haiku: 3 }; const claudeTier = (id: string) => { for (const t of Object.keys(CLAUDE_TIER)) if (id.includes(t)) return CLAUDE_TIER[t]!; return 99; }; -const sortClaudeBig = (a: string, b: string) => { - const ta = claudeTier(a), tb = claudeTier(b); - return ta !== tb ? ta - tb : b.localeCompare(a); -}; -const sortClaudeSmall = (a: string, b: string) => { - const ta = claudeTier(a), tb = claudeTier(b); - return ta !== tb ? tb - ta : b.localeCompare(a); -}; -const sortClaudeSonnet = (a: string, b: string) => { - const da = Math.abs(claudeTier(a) - CLAUDE_TIER.sonnet!); - const db = Math.abs(claudeTier(b) - CLAUDE_TIER.sonnet!); +const sortByTierDistance = (target: number) => (a: string, b: string) => { + const da = Math.abs(claudeTier(a) - target); + const db = Math.abs(claudeTier(b) - target); return da !== db ? da - db : b.localeCompare(a); }; +const sortClaudeFable = sortByTierDistance(CLAUDE_TIER.fable!); +const sortClaudeOpus = sortByTierDistance(CLAUDE_TIER.opus!); +const sortClaudeSonnet = sortByTierDistance(CLAUDE_TIER.sonnet!); +const sortClaudeSmall = sortByTierDistance(CLAUDE_TIER.haiku!); const sortCodex = (a: string, b: string) => { const am = a.includes('mini') ? 1 : 0; const bm = b.includes('mini') ? 1 : 0; @@ -51,12 +47,14 @@ const CODEX_RE = /(^|\/)gpt-5/; const claudeIds = computed(() => dedupe(props.models.filter(m => CLAUDE_RE.test(m.id) && isChat(m)).map(m => m.id))); const codexIds = computed(() => dedupe(props.models.filter(m => CODEX_RE.test(m.id) && isChat(m)).map(m => m.id))); -const claudeModelsBig = computed(() => [...claudeIds.value].sort(sortClaudeBig)); +const claudeModelsFable = computed(() => [...claudeIds.value].sort(sortClaudeFable)); +const claudeModelsOpus = computed(() => [...claudeIds.value].sort(sortClaudeOpus)); const claudeModelsSonnet = computed(() => [...claudeIds.value].sort(sortClaudeSonnet)); const claudeModelsSmall = computed(() => [...claudeIds.value].sort(sortClaudeSmall)); const codexModelsList = computed(() => [...codexIds.value].sort(sortCodex)); -const claudeModel = ref(''); +const claudeFableModel = ref(''); +const claudeOpusModel = ref(''); const claudeSonnetModel = ref(''); const claudeSmallModel = ref(''); const codexModel = ref(''); @@ -64,14 +62,16 @@ const codexModel = ref(''); // Keep the selection valid as the model lists rehydrate: if the current pick // disappears (e.g. an upstream toggled off), fall back to the bucket head. watchEffect(() => { - if (!claudeModelsBig.value.includes(claudeModel.value)) claudeModel.value = claudeModelsBig.value[0] ?? ''; + if (!claudeModelsFable.value.includes(claudeFableModel.value)) claudeFableModel.value = claudeModelsFable.value[0] ?? ''; + if (!claudeModelsOpus.value.includes(claudeOpusModel.value)) claudeOpusModel.value = claudeModelsOpus.value[0] ?? ''; if (!claudeModelsSonnet.value.includes(claudeSonnetModel.value)) claudeSonnetModel.value = claudeModelsSonnet.value[0] ?? ''; if (!claudeModelsSmall.value.includes(claudeSmallModel.value)) claudeSmallModel.value = claudeModelsSmall.value[0] ?? ''; if (!codexModelsList.value.includes(codexModel.value)) codexModel.value = codexModelsList.value[0] ?? ''; }); -// Per-id context-window lookup so Claude Code's ANTHROPIC_MODEL line can -// append the `[1m]` suffix when the upstream supports a 1M context. +// Per-id context-window lookup so the fable/opus/sonnet slots can append the +// `[1m]` suffix when the upstream advertises a 1M context window. Haiku stays +// plain — background-task slot, 1M cost isn't warranted. const contextById = computed(() => { const map = new Map(); for (const m of props.models) { @@ -85,13 +85,22 @@ const contextById = computed(() => { const addCtx = (id: string) => (contextById.value.get(id) ?? 0) >= 1_000_000 ? `${id}[1m]` : id; -const claudeSnippet = computed(() => [ - `export ANTHROPIC_BASE_URL=${baseUrl.value}`, - `export ANTHROPIC_AUTH_TOKEN=${props.apiKey}`, - `export ANTHROPIC_MODEL=${addCtx(claudeModel.value)}`, - `export ANTHROPIC_DEFAULT_SONNET_MODEL=${addCtx(claudeSonnetModel.value)}`, - `export ANTHROPIC_DEFAULT_HAIKU_MODEL=${claudeSmallModel.value}`, -].join('\n')); +// JSON fragment for `settings.json`'s `env` block, not shell exports: Claude +// Code's background-agent supervisor doesn't reliably inherit shell env +// (dispatching into a different cwd drops it, and the SDK / -p paths don't +// see it either) — settings.json is the only channel that reaches every +// execution context. Emit only the `env` sub-object so the user pastes it +// into their existing settings without clobbering unrelated fields. +const claudeSnippet = computed(() => JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: baseUrl.value, + ANTHROPIC_AUTH_TOKEN: props.apiKey, + ANTHROPIC_DEFAULT_FABLE_MODEL: addCtx(claudeFableModel.value), + ANTHROPIC_DEFAULT_OPUS_MODEL: addCtx(claudeOpusModel.value), + ANTHROPIC_DEFAULT_SONNET_MODEL: addCtx(claudeSonnetModel.value), + ANTHROPIC_DEFAULT_HAIKU_MODEL: claudeSmallModel.value, + }, +}, null, 2)); const codexBaseUrl = computed(() => `${baseUrl.value}/azure-api.codex`); @@ -166,9 +175,15 @@ const selectClass = 'max-w-full text-xs font-mono bg-surface-800 text-gray-300 b
- - + + +
+
+ +
@@ -185,8 +200,8 @@ const selectClass = 'max-w-full text-xs font-mono bg-surface-800 text-gray-300 b
-

Add to ~/.bashrc, ~/.zshrc, or equivalent

- +

Merge the env block into ~/.claude/settings.json (user-scope) or .claude/settings.json (project-scope)

+
From 750fd5c2b2f7bd53259fcd9bc77b7a7d2e81535c Mon Sep 17 00:00:00 2001 From: yyyr Date: Thu, 16 Jul 2026 12:52:53 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(web):=20rename=20CliSnippet=20'sma?= =?UTF-8?q?ll'=20=E2=86=92=20'haiku'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Haiku slot's identifiers were still named 'small' from when the picker only distinguished big/sonnet/small. Now that Fable / Opus / Sonnet / Haiku are four peer tiers, the odd-one-out naming is misleading — align on the tier name. --- apps/web/src/components/keys/CliSnippet.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/keys/CliSnippet.vue b/apps/web/src/components/keys/CliSnippet.vue index a7c207b14..84d23acf8 100644 --- a/apps/web/src/components/keys/CliSnippet.vue +++ b/apps/web/src/components/keys/CliSnippet.vue @@ -28,7 +28,7 @@ const sortByTierDistance = (target: number) => (a: string, b: string) => { const sortClaudeFable = sortByTierDistance(CLAUDE_TIER.fable!); const sortClaudeOpus = sortByTierDistance(CLAUDE_TIER.opus!); const sortClaudeSonnet = sortByTierDistance(CLAUDE_TIER.sonnet!); -const sortClaudeSmall = sortByTierDistance(CLAUDE_TIER.haiku!); +const sortClaudeHaiku = sortByTierDistance(CLAUDE_TIER.haiku!); const sortCodex = (a: string, b: string) => { const am = a.includes('mini') ? 1 : 0; const bm = b.includes('mini') ? 1 : 0; @@ -50,13 +50,13 @@ const codexIds = computed(() => dedupe(props.models.filter(m => CODEX_RE.test(m. const claudeModelsFable = computed(() => [...claudeIds.value].sort(sortClaudeFable)); const claudeModelsOpus = computed(() => [...claudeIds.value].sort(sortClaudeOpus)); const claudeModelsSonnet = computed(() => [...claudeIds.value].sort(sortClaudeSonnet)); -const claudeModelsSmall = computed(() => [...claudeIds.value].sort(sortClaudeSmall)); +const claudeModelsHaiku = computed(() => [...claudeIds.value].sort(sortClaudeHaiku)); const codexModelsList = computed(() => [...codexIds.value].sort(sortCodex)); const claudeFableModel = ref(''); const claudeOpusModel = ref(''); const claudeSonnetModel = ref(''); -const claudeSmallModel = ref(''); +const claudeHaikuModel = ref(''); const codexModel = ref(''); // Keep the selection valid as the model lists rehydrate: if the current pick @@ -65,7 +65,7 @@ watchEffect(() => { if (!claudeModelsFable.value.includes(claudeFableModel.value)) claudeFableModel.value = claudeModelsFable.value[0] ?? ''; if (!claudeModelsOpus.value.includes(claudeOpusModel.value)) claudeOpusModel.value = claudeModelsOpus.value[0] ?? ''; if (!claudeModelsSonnet.value.includes(claudeSonnetModel.value)) claudeSonnetModel.value = claudeModelsSonnet.value[0] ?? ''; - if (!claudeModelsSmall.value.includes(claudeSmallModel.value)) claudeSmallModel.value = claudeModelsSmall.value[0] ?? ''; + if (!claudeModelsHaiku.value.includes(claudeHaikuModel.value)) claudeHaikuModel.value = claudeModelsHaiku.value[0] ?? ''; if (!codexModelsList.value.includes(codexModel.value)) codexModel.value = codexModelsList.value[0] ?? ''; }); @@ -98,7 +98,7 @@ const claudeSnippet = computed(() => JSON.stringify({ ANTHROPIC_DEFAULT_FABLE_MODEL: addCtx(claudeFableModel.value), ANTHROPIC_DEFAULT_OPUS_MODEL: addCtx(claudeOpusModel.value), ANTHROPIC_DEFAULT_SONNET_MODEL: addCtx(claudeSonnetModel.value), - ANTHROPIC_DEFAULT_HAIKU_MODEL: claudeSmallModel.value, + ANTHROPIC_DEFAULT_HAIKU_MODEL: claudeHaikuModel.value, }, }, null, 2)); @@ -194,8 +194,8 @@ const selectClass = 'max-w-full text-xs font-mono bg-surface-800 text-gray-300 b
- +
From 9a653ae6d88066660139e05082d3640f539572de Mon Sep 17 00:00:00 2001 From: yyyr Date: Thu, 16 Jul 2026 13:09:19 +0800 Subject: [PATCH 3/5] refactor(web): inline single-use sortClaude* tier comparators --- apps/web/src/components/keys/CliSnippet.vue | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/keys/CliSnippet.vue b/apps/web/src/components/keys/CliSnippet.vue index 84d23acf8..c64dfc503 100644 --- a/apps/web/src/components/keys/CliSnippet.vue +++ b/apps/web/src/components/keys/CliSnippet.vue @@ -25,10 +25,6 @@ const sortByTierDistance = (target: number) => (a: string, b: string) => { const db = Math.abs(claudeTier(b) - target); return da !== db ? da - db : b.localeCompare(a); }; -const sortClaudeFable = sortByTierDistance(CLAUDE_TIER.fable!); -const sortClaudeOpus = sortByTierDistance(CLAUDE_TIER.opus!); -const sortClaudeSonnet = sortByTierDistance(CLAUDE_TIER.sonnet!); -const sortClaudeHaiku = sortByTierDistance(CLAUDE_TIER.haiku!); const sortCodex = (a: string, b: string) => { const am = a.includes('mini') ? 1 : 0; const bm = b.includes('mini') ? 1 : 0; @@ -47,10 +43,10 @@ const CODEX_RE = /(^|\/)gpt-5/; const claudeIds = computed(() => dedupe(props.models.filter(m => CLAUDE_RE.test(m.id) && isChat(m)).map(m => m.id))); const codexIds = computed(() => dedupe(props.models.filter(m => CODEX_RE.test(m.id) && isChat(m)).map(m => m.id))); -const claudeModelsFable = computed(() => [...claudeIds.value].sort(sortClaudeFable)); -const claudeModelsOpus = computed(() => [...claudeIds.value].sort(sortClaudeOpus)); -const claudeModelsSonnet = computed(() => [...claudeIds.value].sort(sortClaudeSonnet)); -const claudeModelsHaiku = computed(() => [...claudeIds.value].sort(sortClaudeHaiku)); +const claudeModelsFable = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.fable!))); +const claudeModelsOpus = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.opus!))); +const claudeModelsSonnet = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.sonnet!))); +const claudeModelsHaiku = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.haiku!))); const codexModelsList = computed(() => [...codexIds.value].sort(sortCodex)); const claudeFableModel = ref(''); From 6b57906889128048b7056bfbf2d01dcb378c4fc3 Mon Sep 17 00:00:00 2001 From: yyyr Date: Thu, 16 Jul 2026 13:11:45 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(web):=20rename=20codexModelsList?= =?UTF-8?q?=20=E2=86=92=20codexModels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/components/keys/CliSnippet.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/keys/CliSnippet.vue b/apps/web/src/components/keys/CliSnippet.vue index c64dfc503..8c9fb51e2 100644 --- a/apps/web/src/components/keys/CliSnippet.vue +++ b/apps/web/src/components/keys/CliSnippet.vue @@ -47,7 +47,7 @@ const claudeModelsFable = computed(() => [...claudeIds.value].sort(sortByTierDis const claudeModelsOpus = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.opus!))); const claudeModelsSonnet = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.sonnet!))); const claudeModelsHaiku = computed(() => [...claudeIds.value].sort(sortByTierDistance(CLAUDE_TIER.haiku!))); -const codexModelsList = computed(() => [...codexIds.value].sort(sortCodex)); +const codexModels = computed(() => [...codexIds.value].sort(sortCodex)); const claudeFableModel = ref(''); const claudeOpusModel = ref(''); @@ -62,7 +62,7 @@ watchEffect(() => { if (!claudeModelsOpus.value.includes(claudeOpusModel.value)) claudeOpusModel.value = claudeModelsOpus.value[0] ?? ''; if (!claudeModelsSonnet.value.includes(claudeSonnetModel.value)) claudeSonnetModel.value = claudeModelsSonnet.value[0] ?? ''; if (!claudeModelsHaiku.value.includes(claudeHaikuModel.value)) claudeHaikuModel.value = claudeModelsHaiku.value[0] ?? ''; - if (!codexModelsList.value.includes(codexModel.value)) codexModel.value = codexModelsList.value[0] ?? ''; + if (!codexModels.value.includes(codexModel.value)) codexModel.value = codexModels.value[0] ?? ''; }); // Per-id context-window lookup so the fable/opus/sonnet slots can append the @@ -208,7 +208,7 @@ const selectClass = 'max-w-full text-xs font-mono bg-surface-800 text-gray-300 b
From 3defd1bac460176d837f04febf7b789c4aa9941d Mon Sep 17 00:00:00 2001 From: yyyr Date: Thu, 16 Jul 2026 13:40:58 +0800 Subject: [PATCH 5/5] refactor(web): collapse Claude tier picker into table-driven records Replace four per-tier `claudeModels{Fable,Opus,Sonnet,Haiku}` computeds, four `claude{...}Model` refs, four inline watchEffect fallback lines, and four near-identical `
` slots with a `CLAUDE_TIER_KEYS` tuple, a single `claudeModelsByTier: Record` computed, a `claudeSelection` reactive record, and one `v-for` template block. This mirrors the peer `v-for="key in PATH_KEYS"` + bracket-indexed `Record` shape already in use in `upstream-edit/CustomConfigPanel.vue` (with its `PATH_KEYS as const` tuple in `customConfig.ts`). The four `ANTHROPIC_DEFAULT__MODEL` env keys stay explicit for grep-ability, and Haiku's no-`addCtx` asymmetry is preserved. --- apps/web/src/components/keys/CliSnippet.vue | 56 +++++++-------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/apps/web/src/components/keys/CliSnippet.vue b/apps/web/src/components/keys/CliSnippet.vue index 8c9fb51e2..37698ac1d 100644 --- a/apps/web/src/components/keys/CliSnippet.vue +++ b/apps/web/src/components/keys/CliSnippet.vue @@ -1,5 +1,5 @@