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>
A broken session that will never be repaired kept the bar's warning dot lit
forever. repair-session now takes --ignore/--unignore/--unignore-all/--ignored,
recorded in ~/.claude-mode/ignored-sessions.json, and the scan hides broken
transcripts older than --max-age days (default 7, 0 disables,
CM_IGNORE_AGE_DAYS sets it). Hidden sessions move to a separate ignored[]
list with a reason, and --all always names how many it held back.
A repair clears the session's dismissal, and entries whose transcript is gone
are pruned, so a session that breaks again is never silently hidden.
The panel gets an Ignore button beside Repair and a collapsed "hidden (N)"
section with Restore. The dot still counts broken sessions only.
Bumps to 1.10.0 and fixes the widget manifest, which still said 1.8.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vault is written with ConvertFrom-SecureString -SecureString, so the
inline branch has to produce a SecureString as well - it only set the
plain text, and under Set-StrictMode the vault write then failed with
'the variable $secure cannot be retrieved'. Caught by running the new
form on the Windows box rather than assuming it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
`claude-mode set-key openrouter sk-or-...` did the wrong thing on the
POSIX port: the dispatcher loop kept the *last* positional, so the key
became the ref name and the prompt asked for "the key for ref
'sk-or-...'". Storing to that ref succeeds, so nothing looks wrong until
the switch later reports no key for 'openrouter' - which is exactly what
happened on macOS.
Both ports now take `set-key [ref] [key]`: the first positional is the
ref unless it is key-shaped (sk-* or longer than a ref name would
plausibly be), in which case it is the key for the default ref, and a
second positional is always the key. An inline key warns that it is now
in the shell history, since the hidden prompt leaves no trace. Extra
arguments are refused instead of ignored. The Windows dispatch already
read the first positional correctly, so it only gains the inline form.
The shipped default preset moves to the models asked for: DeepSeek 4.1
Flash on opus, GLM Flash on sonnet. Existing installs keep their own
presets - the installer never overwrites one without --force - so an
upgrade needs `claude-mode preset set default opus ...` and the same for
sonnet, or a --force install.
Verified in the bash 3.2 container: ref+key stores under the ref, a bare
key stores under openrouter, an empty prompt aborts, a custom ref still
works, the stored value is byte-identical, and four positionals are
refused.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude Code's project slug is not "slashes become dashes". Every
non-alphanumeric character becomes one dash, nothing collapsed - verified
against 2.1.269 by running a session in a directory named `slug._test x`,
which was filed as `-tmp-cmtest-slug--test-x`: the dot, the underscore and
the space each became a dash of their own. Paths with dots are ordinary on
macOS (iCloud Drive lives under `Mobile Documents`), and the old slash-only
rule pointed at a directory that does not exist, so repair-session could
not find a single transcript there.
It is also the physical directory that gets slugged. Claude Code asks the
OS for its working directory, and symlinks come back resolved: a session
started in /tmp/x is filed under -private-tmp-x on macOS, where /tmp is a
symlink, while the shell's $PWD still reads /tmp/x. Verified the same way -
a session started in /tmp/cmtest/linkdir landed under -tmp-cmtest-realdir.
cm_project_dir now applies the real rule and falls back to the resolved
path, which also covers the walk-up search in repair-session. Both cases
were re-tested end to end afterwards: from the dotted-and-spaced directory
and from the symlinked one, the transcripts are found.
The bar widget still reverses a slug for display ("-home-smoido-Work" ->
"home/smoido/Work"), which is lossy in the same way but is only a label,
and only ever sees Linux paths.
Co-Authored-By: Claude Code <noreply@anthropic.com>
macOS ships bash 3.2, and the port used three things it does not have:
mapfile, fractional `read -t`, and GNU `stat -c`. The first two took the
interactive menu down - `mapfile: command not found` in the preset
picker, and an arrow key that parsed as a bare Escape, leaving `[C` in
the tty for zsh to report as a bad pattern.
read_key now reads the escape tail through the terminal itself: icanon
off with min 0 / time 1, so a plain read returns the moment a byte
arrives and gives up after ~0.1s. bash 3.2's `read -t 0` cannot be used
to poll for this - it reports nothing even with bytes buffered (checked
against 3.2.57). CSI sequences are consumed whole, so modified keys like
Ctrl+Right no longer leak their tail either. Verified interactively
under bash 3.2.57 in a pty: arrows, pgup/pgdn, home/end, enter,
backspace, and a bare Escape all parse, with no stray bytes in the tty.
Session listing reads /proc, which macOS does not have; it now says so
rather than silently reporting no running sessions - the answer that
gets people to switch out from under a live session. install.sh swaps
GNU `find -print -quit` for a glob, and the README notes both.
Co-Authored-By: Claude Code <noreply@anthropic.com>
apiKeyHelper is a shell command line, not a path, so the raw value written
into settings.json was split at the first space. A Windows profile named
"Mohammed Ahmed" produced an attempt to run C:\Users\Mohammed, surfacing as
"your apiKeyHelper script is failing" with nothing to go on. Quote the value
when it contains anything a shell cares about, and leave it bare otherwise so
no existing settings.json churns on the next switch. The POSIX port had the
same bug against a /Users/First Last home; shlex.quote has exactly the wanted
"leave ordinary paths alone" behaviour.
doctor could not see any of this. It quoted the path itself before running it,
so it exercised a command line Claude Code never uses and passed while the
real one failed. It now reads the string out of settings.json, reports it when
it is not what a switch would write, and runs that string through a shell.
The helper itself exited 1 in silence on four distinct faults - no state, no
preset, no key, undecryptable key - collapsing them into one indistinguishable
message. Each now names itself on stderr, which is what /status displays. The
DPAPI case says what it actually means: a key stored by a different Windows
account than the one Claude Code runs as. Success paths stay silent, so stdout
still carries the key and nothing else.
Also make install.ps1 survive a Restricted execution policy: piped through
iex it is not subject to the policy, but invoking the installed script for the
key prompt is, which is where a fresh install died. Set Process scope for the
install, offer to set CurrentUser to RemoteSigned, and clear the
mark-of-the-web that Expand-Archive can leave on the extracted scripts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Piped in via irm | iex there is no checkout next to the script, so the
installer now notices that and fetches the repository archive to %TEMP%
itself. README documents the one-liner, the download-and-run variant for
switches, and the clone path.
Co-Authored-By: Claude Code <noreply@anthropic.com>