All notable changes to this project are documented in this file.
- Screen-share privacy: a "Hide hosts and addresses" toggle in Settings → Remote Gateways masks hostnames, tailnet names, IPs, SSH destinations, and the host part of endpoint URLs across the dashboard, Remote Hosts panel, and Settings (ports, paths, model names, and metrics stay visible; endpoint copy keeps working). For demos, streams, and shared screens.
- sparkDash-derived host visibility for every remote gateway (agent 1.2.0): uptime on the gateway header, storage and network rate readouts, tailnet self-health (online state, backend state, warning reasons - catches the "healthy on LAN but off-tailnet" blind spot), and GPU process names next to running models.
- Live LLM serving rates on model rows and the Remote Hosts panel: decode/prefill tok/s from llama.cpp
/slots, vLLM Prometheus/metrics, and sglang/server_infocounters, diffed over time so idle reads 0; plus KV-cache usage and request queue depth for vLLM. Loopback-only unlessALLOW_REMOTE_HEALTHCHECK, same SSRF posture as health probes; every probe failure degrades to a hidden label, never an error.
- Gateway agent updates can no longer hang forever on "Pushing agent…": each deploy step is killed at a 120s deadline with a pointed message, and Tailscale SSH re-auth prompts ("To authenticate, visit https://login.tailscale.com/…") are classified into remediation instead of stalling BatchMode ssh silently.
- DIRECT gateways accept an explicit Deploy host (e.g. an ssh-config alias like
spark) used by Update to push the agent - the MagicDNS URL host hangs when the remote sshd is Tailscale SSH; a Tailscale install converting an SSH gateway now carries the proven host over. - Unnamed port-claim folders (no MODEL*/REQUEST_MODEL/SERVER_MODEL_ID hint) are no longer stuck at "WARMING": their synthetic
port-Nidentity is a placeholder, so readiness is proven by the endpoint serving any OpenAI/v1/modelsid, and the row adopts the served id as its visible name (previously "Port 8050" could never count toward the ready tally). - A
SERVER_MODEL_ID=flag in a claim'sflags.envnow reaches the profile (it was silently dropped by the claim scanner's flag whitelist), and a bareMODEL_FILE=*.ggufclaim reports thellama.cppruntime family instead of Unknown. - Local
start-model-mac.shhealth-wait andSERVER_ARGS_JSONparse through the bundled Swift controller instead ofjq, so LaunchAgent PATH (no Homebrew) can still start models. - Activate on a remote agent no longer walks claims and live-probes every listener just to decide which managed profiles to stop; idle siblings skip
STOP_COMMAND, and a failed sibling stop cannot abortstart(). - GB10 / DGX Spark VRAM chips no longer report host RAM used as VRAM (the
34/122 GBlie). When nvidia-smi framebuffer is N/A, used is the compute-apps GPU-memory sum (SparkDash's25.4/121.7 GB) and total is the unified DRAM pool. - Remote agent discovery no longer imports the agent module (cycle broken via
agent_core.py); installer, embed, and SSH deploy push the third file. - Gateway configs no longer project empty SSH/URL fields for the inactive kind. Deploy and SSH tunnels take
Connection.SSHinstead of minting a fake SSH gateway from a Direct URL. - Remote gateway refresh no longer sticks on
DIRECT · ERROR/ "Request timed out" when the agent is healthy: the Mac client allows longer status/doctor HTTP deadlines, and the remote agent skips probing internal vLLM EngineCore/worker ports that were serializing/api/statusfor ~15s. - Claim/command profiles that actually launch vLLM/llama.cpp now report the real runtime family (so Mac filters like "vLLM" show live remote servers instead of an empty board).
- Claim profiles no longer mark live HF/vLLM model directories as missing (
MODEL=dirs were stuffed intoMODEL_FILE); Intern-S2-Mobius-style servers now report ready without a false missing-weights warning. - Allowed private-network HTTP for Direct/Tailscale gateways under App Transport Security and shortened transport errors.
- Made ready-count/refresh span all gateways; corrected remote-only empty/local/STALE states and added gateway online dots.
- Remote agent stop now reaps zombie/defunct model processes, treats them as not running, force-kills after a longer vLLM-friendly wait, and exposes
stop --force/kill-all. - Tailscale agent binds require a bearer token by default (
--allow-unauthenticatedopt-out); installer generates and prints the token. - Watchdog no longer auto-starts a model after agent reboot from a leftover
active-profilefile (session-supervised crash recovery only).
- Remote gateways in Base/Plus: named sections for launching, monitoring, and stopping other hosts; ready-count and Stop Everything aggregate them.
- Stdlib-only Python agent (
RemoteAgent/) implementing the controller contract, with vLLM/llama.cpp/SGLang/TGI templates,START_COMMAND, and cross-implementationControllerClientconformance coverage. - Settings-driven SSH deployment without remote downloads;
modelswitchboard-gateway://pairing vialink;curl | bashfallback. - App-managed SSH tunnels using the user's own keys (
BatchMode), with jittered reconnect backoff, classified failure messages, and automatic same-port forwarding of running models' endpoints. - Tailscale
--tailscaletailnet-only binds andmode=directMagicDNS pairing without tunnels. - Per-gateway bearer tokens in the keychain; non-loopback agent binds outside a tailnet require
--unsafe-bindplus a ≥16-byte token, mirroring the local controller. - Profile discovery: remote default
~/model-profiles/;linkscans$HOMEfor.env/.json, confirms/pastes and persists a path (--profiles-dir/MODEL_SWITCHBOARD_PROFILES_DIR); matching Mac--profiles-dir+config.jsonsupport. - Each remote shows a separate Update control (dashboard, Remote Hosts, and Settings). It pushes the bundled agent over SSH and refreshes models. The DIRECT/SSH chip is status only. Settings empty state accepts a pairing paste for any host.
- Keychain token saves now update existing items; previously edits to a saved controller token were silently discarded (
SecItemAddduplicate). Token edits are debounced, not saved per keystroke. - Remote gateway thermos follow-ups: token-only gateway edits sync the live store; direct/Tailscale rows no longer advertise dead loopback rewrites; SSH destinations are option-terminated and reject leading
-; ControlMaster sockets are per-tunnel-instance; embedded RemoteAgent files are verified at pack time.
- Recognized
llama-swapas an external OpenAI-compatible proxy runtime, with an example profile and docs for request-driven model swapping alongside menu-bar Activate. - Optional controller bearer-token field in Settings (and widget AppStorage) so token-protected /
--unsafe-bindcontrollers work from the menu bar.
- Replaced the Python production controller with a native Swift service sharing contracts with
ModelSwitchboardCore. - Embedded the controller and LaunchAgent in app distributions and register it through
SMAppService. - Ported controller unit, conformance, fuzz-boundary, and real-service coverage to Swift Testing.
- Controller HTTP server is menu-bar / API-only: removed the browser dashboard HTML,
/and/index.htmlroutes, andstart-model-dashboard.sh.serve-webstill exposes/api/*for the app. - Extended doctor report contracts to include
findings,next_steps, and schema/version metadata. stop-all-models.shnow stops only PID-file-tracked processes by default; setFORCE_ORPHANS=1for the previous broad pgrep sweep.
- Removed the Python controller, Python benchmark harness, Python Droid sync adapter, and the
/usr/bin/python3runtime dependency. - Browser-local dashboard UI (
Controller/web/dashboard.html) and thestart-model-dashboard.shlauncher that opened it.
- Aligned the menu-bar RAM readout with Activity Monitor by counting used memory as active + wired + compressed pages, so inactive file cache is no longer reported as used and the percentage no longer drifts from other stats apps.
- Kept the footer status badge inline (no wrapping) with a line limit and fixed-size layout, so it stays on one line in both fresh and stale controller states.
- Activated Finder as the frontmost app (
activateFileViewerSelecting) when revealing the Profiles Folder from Settings, so the folder no longer opens behind the menu bar panel. - Pinned controller-root resolution to the canonical
~/Library/Application Support/ModelSwitchboard/Controllerpath so "Open Profiles Folder" reveals that folder, not a stale or relocated root. - Stored the detached controller
Processstrongly for its lifetime so the embedded controller service is not prematurely deallocated and torn down while the app is running. - Serialized start/stop/switch/benchmark mutations with a process-wide lock so concurrent HTTP workers cannot interleave lifecycle actions.
- Cleared the active-profile marker and suppressed the crash-recovery watchdog at the start of Stop so intentional stops are not undone mid-flight.
- Parsed HTTP request paths without query/fragment so
/api/*?…routes match correctly against exact path equality. - Refused to kill port listeners based on health-check success alone; require a strong command match, and ignore short PID markers that over-match.
- Default-denied non-loopback health/model-list probes unless
ALLOW_REMOTE_HEALTHCHECK=1is set. - Constrained
doctor undorun ids to a safe character set under.doctor/runs. - Hardened status-cache file permissions (
0700/0600). - Built Swift controller API URLs without percent-encoding
/in multi-segment paths, and rolled back optimistic profile UI state when an action fails.
- Added the redesigned Switchboard panel with ready-count header, system sparklines, runtime filters, active-model hero card, and compact model rows.
- Added full-width Benchmarks and Settings side panels with design-token styling.
- Added prefill-scaling benchmark data in controller payloads and the Benchmarks panel.
- Refreshed README screenshots and release imagery for the new panel design.
- Let the menu bar item show ready counts and added theme, accent, and side-panel preferences.
- Prevented stale or cached running state from rendering as live active model state.
- Avoided caching stopped state before stop verification succeeds.
- Kept failed benchmark starts from arming the benchmark cooldown.
- Installed
model-switchboardctland bash/zsh/fish completions from the source installer. - Added installer regression coverage for quiet installs, verification, and completion generation.
- Added agent-facing controller and wrapper discovery surfaces for capabilities, triage, robot docs, doctor diagnostics, and mutating-command dry-run plans.
- Hardened the app and controller installers with help flags, quiet/no-gum modes, preflight checks, atomic locks, install verification, and final uninstall guidance.
model-switchboardctl --jsonoutput for mutating commands uses structured result envelopes, not raw controller pass-through.
- Added a launchd
PATHto the controller LaunchAgent so runtime tools installed in~/.local/bin, Homebrew, and system locations can be found without per-profile overrides. - Improved controller doctor findings for missing runtime executables with explicit
SERVER_BINremediation. - Replaced generic profile action timeout errors in the app with action/profile-specific messages that include doctor diagnostics when available.
- Added a terminal demo recording and refreshed screenshot assets for the README.
- Redesigned the README hero and gallery around the current Base, Plus, benchmark, and controller workflows.
- Hardened controller profile parsing so
.envprofiles are declarative key/value data (not shell scripts), with explicit validation shared by the CLI and launcher.
- Preserved llama.cpp chat template arguments and launcher start timeouts when generating managed runtime commands.
- Removed a local filesystem path from the terminal demo source.
- Verified Stop and Stop All results from the app after controller responses so lingering model processes surface as errors, not optimistic UI success.
- Kept the dashboard open after model action clicks so users can see profiles transition through starting, stopping, and running states.
- Hardened controller shutdown so Stop and Stop All terminate child processes, process groups, and stale profile port listeners; model-serving Python processes no longer stay resident.
- Added active-profile recovery in the controller so an activated local model profile is restarted when its process and health check both disappear.
- Fixed overnight local-model runs silently staying down after an MLX or other managed runtime crash.
- Added a release privacy audit that scans tracked source and built app bundles for local user paths, private-key material, and common token-shaped secret values.
- Added README coverage for supported runtimes, providers, external endpoints, and model-level runtime tags.
- Release verification now runs the privacy audit against app bundles before publication, and the installer now installs from the same stripped build output used for distribution.
- Stripped Mach-O debug/source metadata before signing so public DMGs and locally installed apps do not retain local build paths.
- Removed a personal path from a controller test fixture.
- Added first-class runtime labels and tags for current OpenAI-compatible local launchers including Mistral.rs, MLC-LLM, LightLLM, FastChat, OpenLLM, Nexa, MLX Omni Server, MLX Serve, LiteLLM, TensorRT-LLM, and ONNX Runtime GenAI.
- Added explicit dashboard edge resize handles so resizing works from the left and right sides of the menu window.
- Preferred local model directories and files before remote model IDs when a profile provides both, avoiding accidental network fetches for adapter-based launchers.
- Preserved runtime labels, tags, and launch mode through Swift status mutations so profile cards keep displaying the controller's real runtime metadata.
- Added universal launcher support for local model profiles, including llama.cpp, MLX, vLLM, Ollama, OpenAI-compatible servers, custom commands, and runtime-specific metadata tags.
- Added a live RAM utilization badge between CPU and GPU telemetry in the Plus menu header.
- Added safe button-action stress coverage that can simulate 10,000 menu and inspector clicks without starting local models.
- Moved controller-side install guidance and live-controller expectations toward the
~/AI/modelswitchboardroot with profile storage undermodel-profiles. - Expanded release and setup documentation around runtime selection, launcher tags, downloaded-DMG setup, and managed controller paths.
- Hardened model lifecycle shutdown, launcher log alias sanitization, and Droid sync compatibility on Python 3.9.
- Stabilized installed-app verification against the migrated controller root and live Plus app expectations.
- Added regression coverage for loopback endpoint fast-fail cadence, managed-action suppression, and remote-profile probe skipping.
- Reworked the localhost fast-fail path to reuse a dedicated probe session, use a lightweight
HEADprobe against the model base URL, and back off from a 2-second startup window to a steadier cadence over time.
- Dropped dead loopback model endpoints out of the UI faster without waiting for the next controller refresh tick.
- Avoided redundant localhost probe churn immediately after app-driven start, stop, restart, activate, and stop-all actions.
- Added app regression coverage for fresh, stale, and cached controller snapshots so the menu bar does not show live state when localhost models stop out of band.
- Kept controller auto-refresh running for the lifetime of the app, including when the menu is closed.
- Tightened active-runtime refresh cadence from 30 seconds to 10 seconds and documented the always-resident lightweight polling model in
SETUP.md.
- Stopped stale or cached localhost status from showing live running and ready counts in the menu bar.
- Marked stale running profiles as
STALE, notRUNNING, until the next successful refresh, and updated the status-item tooltip to match.
- Added
Scripts/bump-version.pyplus release-automation tests so version bumps update the repo consistently without hand-editing release files.
- Updated
.github/workflows/release.ymlso a version bump pushed tomainnow builds, notarizes, and publishes the GitHub release automatically, while preserving manual and tag-driven releases. - Documented the automated maintainer release flow in
README.mdandSETUP.md.
- Fixed local llama.cpp profile resolution so
MODEL_FILEworks withMODEL_ROOT_HINT,~/AI/models, and../modelsfallbacks without requiringMODEL_ROOT. - Passed through llama.cpp
CHAT_TEMPLATE_KWARGSandCACHE_RAM, which restores Qwen no-think setups without forcingRUNTIME=custom. - Added
--rootsupport to the controller installer so LaunchAgent installs can target any controller checkout without plist hand-edits.
- Added controller doctor validation for duplicate profile endpoints so conflicting
HOST:PORTorBASE_URLassignments are called out before activation. - Added controller regression coverage for loopback endpoint normalization, conflict reporting, and activation refusal.
- Documented the unique-endpoint requirement in the setup docs so profile authors do not accidentally point two models at the same listener.
- Blocked
Activate,Start, andRestartwhen a profile shares an endpoint with another profile. - Prevented conflicted profiles from borrowing the same port listener PID and appearing to co-activate in the UI.
- Added a footer benchmark viewer shortcut in Plus so the latest benchmark panel can be reopened without rerunning a job.
- Added regression coverage for inspector close/reopen timing, benchmark timestamp parsing, profile display ordering, and controller benchmark result validation.
- Sorted profile cards deterministically by live state first, then local endpoint order for inactive profiles, so the app no longer reshuffles idle models across refreshes.
- Reformatted benchmark timestamps into readable local date/time output, not raw ISO strings in the panel.
- Fixed the side-panel lifecycle so closing Benchmark, Settings, or Help no longer requires defocusing the app before reopening another panel.
- Reduced inspector close jitter by deferring host-window refocus until the panel hide transition has actually completed.
- Prevented empty benchmark streams from being scored as fake ultra-high throughput and attached the underlying non-stream error to failed benchmark results for diagnosis.
- Added
Scripts/release-preflight.shto validate release prerequisites, version alignment, script wiring, tests, and build readiness. - Added a documented manual release checklist item for login-item exclusivity when both editions are installed.
- Updated
.github/workflows/release.ymlto useactions/checkout@v5. - Replaced release upload via
softprops/action-gh-releasewithgh releaseto avoid Node runtime churn and support idempotent re-runs. - Hardened
Scripts/verify-installed-app.shwith AppleScript retry behavior and optional headless fallback mode viaMSW_VERIFY_UI=0.
- Reduced flaky menu opening and foreground-app automation failures in installed-app verification.
- Fixed login-item conflict behavior between Base and Plus so enabling one unregisters the companion edition.
- Added login-item companion bundle mapping tests.
- Hardened release compatibility for CI and notarized distribution verification.
- Added ignore patterns for local key artifacts (
*.p8,*.p12,*.pem,*.key,*.cer,*.crt) and environment files.
- Initial public release with Base and Plus editions.
- Signed/notarized DMG release pipeline.
- Controller contract, runtime adapters, benchmark flows, and docs.