Compare commits

...
2 Commits
Author SHA1 Message Date
smoidoandClaude Opus 5 a54f7bcb55 1.11.0: preset editor in the bar panel
Tier editing with the cached model catalogue, preset create / duplicate /
rename / delete, and a choosable default preset per provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 00:39:12 +03:00
smoidoandClaude Opus 5 c5c3fc57d9 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>
2026-09-15 00:34:14 +03:00
6 changed files with 215 additions and 15 deletions
+16 -2
View File
@@ -185,6 +185,7 @@ claude-mode preset new <name> [from] copy an existing preset (default: `defaul
claude-mode preset new <name> --provider <p> [--blank]
copy that provider's default, or start empty
claude-mode preset rename <name> <new-name> repoints state.json if it is in use
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> point every tier at one model
claude-mode preset rm <name>
@@ -204,8 +205,21 @@ claude-mode sessions --dry-run show what either would do, and do nothing
claude-mode <mode> --force switch even when preflight says no
```
Omitting the preset uses a **fixed** per-provider default, not "most recently
used" — so `claude-mode openrouter` always means `default`.
Omitting the preset uses a per-provider default, never "most recently used".
Out of the box that is a fixed name — `claude-mode openrouter` means `default`,
`zai` means `zai`, `lmstudio` means `lmstudio` — and it can be changed:
```bash
claude-mode preset default # what each provider picks, and why
claude-mode preset default openrouter cheap # `claude-mode openrouter` now means `cheap`
claude-mode preset default openrouter --clear # back to the built-in name
```
The choice lives in `~/.claude-mode/defaults.json`. If the chosen preset is
deleted the choice is cleared with it, renaming moves it along, and a choice
whose file has gone some other way simply falls back to the built-in name. If
that is gone too, the provider's first preset by name is used. The Windows build
does not read this file yet, and keeps the built-in names.
Editing the active preset re-applies it straight away. For `preset set` and
`preset all` that happens **without** asking about running sessions: a switch
+1 -1
View File
@@ -1 +1 @@
1.10.0
1.11.0
+79 -5
View File
@@ -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
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,
+49 -2
View File
@@ -317,7 +317,27 @@ Panel {
property string editTier: ""
property string editModel: ""
// Which preset `claude-mode <provider>` picks when none is named. The CLI
// publishes it now that it can be chosen; the built-in names in Modes.js
// are only the fallback for a health.json written by an older CLI.
readonly property var defaultPresets: {
var out = {}
var builtin = Modes.DEFAULT_PRESET
for (var b in builtin) out[b] = builtin[b]
var pub = health && health.defaultPresetFor ? health.defaultPresetFor : null
if (pub) for (var p in pub) if (pub[p]) out[p] = pub[p]
return out
}
readonly property var chosenDefaults: health && health.defaultPresetChosen ? health.defaultPresetChosen : ({})
readonly property var editEntry: root.editPreset !== "" ? root.presetEntry(root.editPreset) : null
readonly property bool editIsDefault: root.editPreset !== "" && root.defaultPresets[root.editProvider] === root.editPreset
readonly property bool editIsChosenDefault: root.editPreset !== "" && root.chosenDefaults[root.editProvider] === root.editPreset
function setDefault(on) {
root.runOps("saving", [on ? ["preset", "default", root.editProvider, root.editPreset]
: ["preset", "default", root.editProvider, "--clear"]], "preset")
}
readonly property string editProvider: editEntry ? String(editEntry.provider) : ""
readonly property bool editIsActive: root.mode !== "anthropic" && root.editPreset !== "" && root.editPreset === root.preset
@@ -847,7 +867,7 @@ Panel {
readonly property string thisMode: String(modelData)
readonly property bool isCurrent: thisMode === root.mode
readonly property bool isExpanded: root.expandedMode === thisMode
readonly property var presetList: Modes.presetsFor(root.health, thisMode, Modes.DEFAULT_PRESET)
readonly property var presetList: Modes.presetsFor(root.health, thisMode, root.defaultPresets)
Item {
width: parent.width
@@ -968,10 +988,24 @@ Panel {
font.bold: presetRow.isActive
}
// Only worth saying when there is a choice to tell apart.
Text {
id: defaultTag
visible: modeEntry.presetList.length > 1
&& String(presetRow.modelData.name) === root.defaultPresets[modeEntry.thisMode]
anchors.left: presetName.right
anchors.leftMargin: Style.space(6)
anchors.verticalCenter: parent.verticalCenter
text: "default"
color: Color.muted
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(9)
}
// The opus mapping is the one that tells you what you are
// about to be talking to; the rest is in the tooltip.
Text {
anchors.left: presetName.right
anchors.left: defaultTag.visible ? defaultTag.right : presetName.right
anchors.leftMargin: Style.space(8)
anchors.right: gearButton.visible ? gearButton.left : parent.right
anchors.rightMargin: Style.space(4)
@@ -1311,6 +1345,17 @@ Panel {
font.pixelSize: Style.space(10)
}
Text {
width: parent.width
visible: root.editIsDefault
text: "claude-mode " + root.editProvider + " picks this one when no preset is named"
+ (root.editIsChosenDefault ? "." : " (the built-in choice).")
color: Color.muted
wrapMode: Text.WordWrap
font.family: root.bar ? root.bar.fontFamily : Style.font.family
font.pixelSize: Style.space(10)
}
Column {
width: parent.width
spacing: Style.space(1)
@@ -1392,6 +1437,8 @@ Panel {
visible: root.editProvider === "lmstudio"
onTriggered: root.openServerSettings(root.editPreset, "preset")
}
PillButton { label: "Make default"; visible: !root.editIsDefault; onTriggered: root.setDefault(true) }
PillButton { label: "Clear default"; visible: root.editIsChosenDefault; onTriggered: root.setDefault(false) }
PillButton { label: "Duplicate…"; onTriggered: root.openNewPreset(root.editProvider, root.editPreset) }
PillButton { label: "Rename…"; onTriggered: root.openRename() }
// The CLI refuses to delete the preset in use. A button that always
+1 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"id": "smoido.claude-mode",
"name": "Claude Mode",
"version": "1.10.0",
"version": "1.11.0",
"author": "smoido",
"description": "Which provider Claude Code is pointed at, and a one-click switch between them",
"kinds": ["bar-widget"],