Compare commits
2
Commits
de9f10a8c2
...
a54f7bcb55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54f7bcb55 | ||
|
|
c5c3fc57d9 |
@@ -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
|
||||
|
||||
+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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user