Choose which preset claude-mode <provider> picks

Omitting the preset used a fixed name per provider (default, zai,
lmstudio). It can now be chosen:

  claude-mode preset default                     what each picks, and why
  claude-mode preset default openrouter cheap
  claude-mode preset default openrouter --clear

The choice is stored in ~/.claude-mode/defaults.json and read first by
default_preset_for, while its file still exists. A choice whose file is gone
falls back to the built-in name, and if that is gone too, to the first
preset by name, as before. Renaming a chosen preset moves the choice with it,
and deleting it clears the choice. The terminal menu marks what
resolve_preset would actually pick.

health.json publishes defaultPresetFor (what each provider resolves to, in
the CLI's order) and defaultPresetChosen (the explicit choices only). The
panel sorts and tags from it, and the editor gains Make default / Clear
default. preset new, rename and rm now refresh health.json, so the bar
follows edits made in a terminal too.

This changes existing CLI behaviour only once a default is chosen. The
Windows build does not read defaults.json yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
smoido
2026-09-15 00:34:14 +03:00
co-authored by Claude Opus 5
parent de9f10a8c2
commit c5c3fc57d9
4 changed files with 213 additions and 13 deletions
+69 -4
View File
@@ -17,8 +17,9 @@ Subcommands:
scan-sessions <projects> [max-age-days] [ignored] broken transcripts, as JSON
ignore-session <ignored> <projects> add|remove|clear|list [id]
cache-models <cache> <provider> <1|0> [baseUrl] store a TSV catalogue (stdin)
preset-rename <presets-dir> <old> <new> <state> rename, repointing state.json
preset-rename <dir> <old> <new> <state> [defaults] rename, repointing state + default
auth-of <preset> "mode<TAB>keyRef"; fails if missing
set-default <defaults> <provider> [name] choose (or clear) a provider default
"""
import glob
@@ -62,6 +63,10 @@ BASE_MANAGED = [
TIERS = ["opus", "sonnet", "haiku", "fable"]
# Mirrors builtin_default_preset() in the CLI: what `claude-mode <provider>`
# picks when no preset is named and none has been chosen with `preset default`.
BUILTIN_DEFAULT_PRESET = {"openrouter": "default", "zai": "zai", "lmstudio": "lmstudio"}
def load(path, default=None):
if not os.path.exists(path):
@@ -269,15 +274,36 @@ def cmd_auth_of(argv):
print("%s\t%s" % (auth.get("mode") or "vault", auth.get("keyRef") or "openrouter"))
def cmd_set_default(argv):
"""set-default <defaults.json> <provider> [name] - an empty name clears it."""
path, prov = argv[0], argv[1]
name = argv[2] if len(argv) > 2 else ""
try:
d = load(path, {})
except (OSError, ValueError):
d = {}
if not isinstance(d, dict):
d = {}
if name:
d[prov] = name
else:
d.pop(prov, None)
save(path, d)
print(json.dumps(d))
def cmd_preset_rename(argv):
"""preset-rename <presets-dir> <old> <new> <state.json>
"""preset-rename <presets-dir> <old> <new> <state.json> [defaults.json]
Every running session's key helper resolves the active preset through
state.json on each key fetch, so at no instant may state.json name a file
that is not there. The new name is linked in first, state.json repointed,
and only then does the old name go. Prints {"renamed", "active"}.
and only then does the old name go. A provider default chosen with
`preset default` follows the rename too. Prints {"renamed", "active",
"default"}.
"""
d, old, new, state_path = argv[:4]
defaults_path = argv[4] if len(argv) > 4 else ""
src = os.path.join(d, old + ".json")
dst = os.path.join(d, new + ".json")
if not os.path.isfile(src):
@@ -295,8 +321,21 @@ def cmd_preset_rename(argv):
state["preset"] = new
save(state_path, state)
moved = []
if defaults_path:
try:
defaults = load(defaults_path, {})
except (OSError, ValueError):
defaults = {}
if isinstance(defaults, dict):
moved = [p for p, n in defaults.items() if n == old]
if moved:
for p in moved:
defaults[p] = new
save(defaults_path, defaults)
os.unlink(src)
print(json.dumps({"renamed": True, "active": active}))
print(json.dumps({"renamed": True, "active": active, "default": bool(moved)}))
def cmd_get(argv):
@@ -610,6 +649,31 @@ def cmd_health(argv):
})
h["presets"] = catalogue
# Which preset `claude-mode <provider>` picks when none is named, in the
# CLI's own order (default_preset_for, then resolve_preset): the chosen one
# while its file exists, else the built-in name if that exists, else the
# first by name. `defaultPresetChosen` holds only the explicit choices, so a
# reader can tell a choice it could clear from the built-in fallback.
names = {c["name"] for c in catalogue}
try:
chosen_raw = load(os.path.join(root, "defaults.json"), {})
except (OSError, ValueError):
chosen_raw = {}
if not isinstance(chosen_raw, dict):
chosen_raw = {}
effective, chosen = {}, {}
for prov, builtin in BUILTIN_DEFAULT_PRESET.items():
pick = chosen_raw.get(prov)
if pick and pick in names:
effective[prov] = chosen[prov] = pick
elif builtin in names:
effective[prov] = builtin
else:
mine = sorted(c["name"] for c in catalogue if c["provider"] == prov)
effective[prov] = mine[0] if mine else ""
h["defaultPresetFor"] = effective
h["defaultPresetChosen"] = chosen
save(os.path.join(root, "health.json"), h)
@@ -1227,6 +1291,7 @@ COMMANDS = {
"scaffold": cmd_scaffold,
"preset-rename": cmd_preset_rename,
"auth-of": cmd_auth_of,
"set-default": cmd_set_default,
"get": cmd_get,
"presets": cmd_presets,
"managed": cmd_managed,