Edit a preset's tiers from the bar panel

The gear on every preset row now opens an editor: the four tiers with the
model each maps to, and for each one field that filters the provider's
catalogue as you type (every word must match; arrows and Enter work), listed
inline with context length and price, and that also takes any id typed by
hand. LM Studio's server form moves one click inside the editor.

The panel card moves from PopupCard to KeyboardPanel. PopupCard is an
xdg-popup, which only receives keys after focus is routed through its parent
surface, so no text field in it could ever be typed into - the existing
server URL form included. KeyboardPanel primes layer-shell keyboard focus on
open, which is why every shell panel with a text field uses it. Esc now
closes the panel.

The picker reads ~/.claude-mode/models-cache.json, which or_catalogue and
lms_catalogue now write as a side effect, so models, doctor, setup and the
menu's picker all keep it fresh and the panel never hits the network itself.
One node per provider with its own fetchedAt/ok; a failed fetch keeps the
old list, an LM Studio list is tied to its server, and no key or key name is
ever stored. `models` gains --preset, --refresh and --json, and the Z.AI list
now lives in one place.

A tier edit to the active preset re-applies without the running-sessions
prompt: that prompt guards against the endpoint or key moving, and a tier
edit moves neither (preset url/auth still ask). A failed re-apply now says
the edit was saved. `preset set` on an unknown name no longer creates it.

Panel edits run through one chain that stops on the first failure and
refreshes health.json at the end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
smoido
2026-09-15 00:14:30 +03:00
co-authored by Claude Opus 5
parent cbd3b1d8a9
commit 83b59f34a1
5 changed files with 797 additions and 47 deletions
+72
View File
@@ -16,6 +16,7 @@ Subcommands:
presets <dir> "name<TAB>provider<TAB>desc"
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)
"""
import glob
@@ -313,6 +314,76 @@ def cmd_or_models(argv):
print("%s\t%s\t%s\t%s" % (m.get("id", ""), m.get("context_length", ""), per_m("prompt"), per_m("completion")))
def cmd_cache_models(argv):
"""cache-models <cache.json> <provider> <1|0> [baseUrl] TSV catalogue on stdin
Keeps the last catalogue each provider returned, for a reader that cannot
afford a network round trip - the bar panel's model picker. One node per
provider with its own fetchedAt and ok, so a failed OpenRouter fetch never
invalidates a fresh LM Studio list. A failed fetch keeps the previous list
(a stale list beats none when the network blinks) and marks it ok=false.
Model ids, context lengths and prices only: never a key reference or token.
Prints the number of models stored by this call.
"""
path, provider, ok = argv[0], argv[1], argv[2] == "1"
base = argv[3].rstrip("/") if len(argv) > 3 else ""
def num(s, cast):
try:
return cast(s)
except (TypeError, ValueError):
return None
rows = []
for line in sys.stdin.read().splitlines():
parts = line.split("\t")
mid = parts[0].strip() if parts else ""
if not mid:
continue
m = {"id": mid}
if provider == "openrouter":
ctx, pin, pout = (parts[1:] + ["", "", ""])[:3]
m["contextTokens"] = num(ctx, int)
m["priceIn"] = num(pin, float)
m["priceOut"] = num(pout, float)
elif provider == "lmstudio":
state, ctx = (parts[1:] + ["", ""])[:2]
m["state"] = state
m["contextTokens"] = num(ctx, int)
else:
m["note"] = parts[1] if len(parts) > 1 else ""
rows.append(m)
try:
data = load(path, {})
except (OSError, ValueError):
data = {}
providers = data.get("providers") if isinstance(data, dict) else None
if not isinstance(providers, dict):
providers = {}
node = providers.get(provider)
if not isinstance(node, dict):
node = {}
now = __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
if ok:
node = {"fetchedAt": now, "ok": True, "models": rows}
if provider == "lmstudio":
node["baseUrl"] = base
else:
# A list from a different LM Studio server is not stale, it is wrong.
if provider == "lmstudio" and node.get("baseUrl", "") != base:
node = {"baseUrl": base, "models": []}
node["ok"] = False
node["failedAt"] = now
providers[provider] = node
save(path, {"schema": 1, "providers": providers})
print(len(rows) if ok else 0)
def cmd_lms_models(argv):
"""LM Studio /api/v0/models on stdin -> 'id<TAB>state<TAB>ctx' lines."""
data = json.loads(sys.stdin.read())
@@ -1095,6 +1166,7 @@ COMMANDS = {
"strip-tags": cmd_strip_tags,
"or-models": cmd_or_models,
"lms-models": cmd_lms_models,
"cache-models": cmd_cache_models,
"apply": cmd_apply,
"summary": cmd_summary,
"models": cmd_models,