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:
+79
-5
@@ -18,6 +18,7 @@ CM_BACKUPS="$CM_ROOT/backups"
|
||||
CM_STATE="$CM_ROOT/state.json"
|
||||
CM_IGNORED="$CM_ROOT/ignored-sessions.json"
|
||||
CM_MODELS_CACHE="$CM_ROOT/models-cache.json"
|
||||
CM_DEFAULTS="$CM_ROOT/defaults.json"
|
||||
CM_SETTINGS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
CM_SETTINGS="$CM_SETTINGS_DIR/settings.json"
|
||||
CM_HELPER="$CM_BIN/claude-key-helper.sh"
|
||||
@@ -269,6 +270,8 @@ claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
|
||||
claude-mode preset new <name> --provider <p> [--blank]
|
||||
copy that provider's default, or start empty
|
||||
claude-mode preset rename <name> <new-name>
|
||||
claude-mode preset default [provider] [name|--clear]
|
||||
which preset 'claude-mode <provider>' picks
|
||||
claude-mode preset set <name> <tier> <model-id>
|
||||
claude-mode preset all <name> <model-id>
|
||||
claude-mode preset url <name> <base-url> point a preset at another server
|
||||
@@ -359,7 +362,12 @@ term_cols() {
|
||||
printf '%s' "$c"
|
||||
}
|
||||
|
||||
default_preset_for() {
|
||||
# The preset `claude-mode <provider>` uses when none is named: the one chosen
|
||||
# with `preset default`, while its file still exists, else the built-in name.
|
||||
# The existence test is what lets a stale choice degrade instead of break.
|
||||
# cm-json.py's BUILTIN_DEFAULT_PRESET and the widget's Modes.DEFAULT_PRESET
|
||||
# mirror the built-in names.
|
||||
builtin_default_preset() {
|
||||
case "$1" in
|
||||
openrouter) printf 'default' ;;
|
||||
zai) printf 'zai' ;;
|
||||
@@ -367,6 +375,15 @@ default_preset_for() {
|
||||
esac
|
||||
}
|
||||
|
||||
default_preset_for() {
|
||||
local chosen
|
||||
chosen="$(jget "$CM_DEFAULTS" "$1")"
|
||||
if [ -n "$chosen" ] && valid_preset_name "$chosen" && [ -f "$(preset_path "$chosen")" ]; then
|
||||
printf '%s' "$chosen"; return 0
|
||||
fi
|
||||
builtin_default_preset "$1"
|
||||
}
|
||||
|
||||
resolve_preset() {
|
||||
local provider="$1" requested="${2:-}" fallback first
|
||||
if [ -n "$requested" ]; then
|
||||
@@ -1571,6 +1588,7 @@ cmd_preset() {
|
||||
rm -f "$(preset_path "$name")"; return 1; }
|
||||
ok "created blank $provider preset '$name'"
|
||||
say 'every tier is empty: set them with claude-mode preset set, or the bar panel'
|
||||
write_health "$(state_mode)" "$(state_preset)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -1588,6 +1606,7 @@ cmd_preset() {
|
||||
fi
|
||||
cp "$(preset_path "$from")" "$(preset_path "$name")"
|
||||
ok "created $(preset_path "$name") from '$from'"
|
||||
write_health "$(state_mode)" "$(state_preset)"
|
||||
;;
|
||||
rename)
|
||||
local new="${3:-}" out
|
||||
@@ -1596,14 +1615,17 @@ cmd_preset() {
|
||||
[ -f "$(preset_path "$name")" ] || { err "preset '$name' not found"; return 1; }
|
||||
[ "$name" = "$new" ] && { ok "already called '$new'"; return 0; }
|
||||
[ -e "$(preset_path "$new")" ] && { err "preset '$new' already exists"; return 1; }
|
||||
out="$("$PY" "$JSON" preset-rename "$CM_PRESETS" "$name" "$new" "$CM_STATE" 2>&1)" || {
|
||||
out="$("$PY" "$JSON" preset-rename "$CM_PRESETS" "$name" "$new" "$CM_STATE" "$CM_DEFAULTS" 2>&1)" || {
|
||||
err "$out"; return 1; }
|
||||
ok "renamed '$name' -> '$new'"
|
||||
case "$out" in
|
||||
*'"active": true'*)
|
||||
say 'it is the active preset; state.json now names it, and running sessions keep working'
|
||||
write_health "$(state_mode)" "$new" ;;
|
||||
say 'it is the active preset; state.json now names it, and running sessions keep working' ;;
|
||||
esac
|
||||
case "$out" in
|
||||
*'"default": true'*) say 'it was chosen as its provider'"'"'s default, and that follows the new name' ;;
|
||||
esac
|
||||
write_health "$(state_mode)" "$(state_preset)"
|
||||
;;
|
||||
rm)
|
||||
[ -n "$name" ] || { err 'usage: claude-mode preset rm <name>'; return 1; }
|
||||
@@ -1613,12 +1635,62 @@ cmd_preset() {
|
||||
fi
|
||||
local prov; prov="$(jget "$(preset_path "$name")" provider)"; [ -z "$prov" ] && prov=openrouter
|
||||
rm -f "$(preset_path "$name")"; ok "deleted preset '$name'"
|
||||
if [ "$(jget "$CM_DEFAULTS" "$prov")" = "$name" ]; then
|
||||
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "" >/dev/null
|
||||
say "it was the chosen default for $prov; 'claude-mode $prov' means '$(resolve_preset "$prov" 2>/dev/null || printf 'nothing')' now"
|
||||
fi
|
||||
# Allowed, but not quietly: with no preset left, `claude-mode <provider>`
|
||||
# has nothing to resolve to.
|
||||
if [ -z "$(presets_for_provider "$prov")" ]; then
|
||||
warn "that was the last $prov preset - 'claude-mode $prov' has nothing to switch to now"
|
||||
say "create one with: claude-mode preset new <name> --provider $prov [--blank]"
|
||||
fi
|
||||
write_health "$(state_mode)" "$(state_preset)"
|
||||
;;
|
||||
default)
|
||||
# Which preset `claude-mode <provider>` means when none is named.
|
||||
local prov="$name" target="${3:-}" p cur got
|
||||
if [ -z "$prov" ]; then
|
||||
head_ 'default preset per provider'
|
||||
for p in openrouter zai lmstudio; do
|
||||
cur="$(jget "$CM_DEFAULTS" "$p")"
|
||||
if [ -n "$cur" ] && [ -f "$(preset_path "$cur")" ]; then
|
||||
printf ' %-11s %s (chosen)\n' "$p" "$cur"
|
||||
else
|
||||
# "built-in" only when it really is the built-in name;
|
||||
# with that preset gone, resolve_preset takes the first.
|
||||
local got_p why
|
||||
got_p="$(resolve_preset "$p" 2>/dev/null)" || got_p=''
|
||||
if [ -z "$got_p" ]; then why='no preset left'
|
||||
elif [ "$got_p" = "$(builtin_default_preset "$p")" ]; then why='built-in'
|
||||
else why='first by name'; fi
|
||||
printf ' %-11s %s (%s%s)\n' "$p" "${got_p:--}" "$why" \
|
||||
"$([ -n "$cur" ] && printf "; chosen '%s' no longer exists" "$cur")"
|
||||
fi
|
||||
done
|
||||
printf '\n %sclaude-mode preset default <provider> <name> (or --clear)%s\n' "$C_DIM" "$C_RESET"
|
||||
return 0
|
||||
fi
|
||||
case "$prov" in
|
||||
openrouter|zai|lmstudio) ;;
|
||||
*) err "unknown provider '$prov' - openrouter, zai or lmstudio"; return 1 ;;
|
||||
esac
|
||||
if [ -z "$target" ]; then
|
||||
resolve_preset "$prov" && printf '\n'
|
||||
return
|
||||
fi
|
||||
if [ "$target" = "--clear" ]; then
|
||||
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "" >/dev/null || return 1
|
||||
ok "$prov : no longer chosen; 'claude-mode $prov' means '$(resolve_preset "$prov" 2>/dev/null || printf 'nothing - no preset left')'"
|
||||
else
|
||||
valid_preset_name "$target" || { err "invalid preset name '$target'"; return 1; }
|
||||
[ -f "$(preset_path "$target")" ] || { err "preset '$target' not found"; return 1; }
|
||||
got="$(jget "$(preset_path "$target")" provider)"; [ -z "$got" ] && got=openrouter
|
||||
[ "$got" = "$prov" ] || { err "'$target' is for $got, not $prov"; return 1; }
|
||||
"$PY" "$JSON" set-default "$CM_DEFAULTS" "$prov" "$target" >/dev/null || return 1
|
||||
ok "$prov : 'claude-mode $prov' now means '$target'"
|
||||
fi
|
||||
write_health "$(state_mode)" "$(state_preset)"
|
||||
;;
|
||||
set)
|
||||
local tier="${3:-}" model="${4:-}"
|
||||
@@ -1913,7 +1985,9 @@ UI_PICKED=''
|
||||
ui_pick_preset() {
|
||||
local provider="$1" def names name
|
||||
UI_PICKED=''
|
||||
def="$(default_preset_for "$provider")"
|
||||
# resolve_preset, not default_preset_for: the mark should land on whatever
|
||||
# `claude-mode <provider>` would actually pick, fallbacks included.
|
||||
def="$(resolve_preset "$provider" 2>/dev/null)"
|
||||
names=()
|
||||
while IFS= read -r name; do names+=("$name"); done < <(presets_for_provider "$provider")
|
||||
[ "${#names[@]}" -gt 0 ] || { err "no presets for '$provider'"; return 1; }
|
||||
|
||||
+69
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user