Merge remote-tracking branch 'origin/main' into feat/server-list-ux
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10) (pull_request) Successful in 8s
CI / Tests (py3.12) (pull_request) Successful in 8s

This commit is contained in:
Cowork Supervisor
2026-07-08 11:55:36 -04:00
3 changed files with 264 additions and 0 deletions
+16
View File
@@ -1704,6 +1704,22 @@ class MainWindow(QMainWindow):
self.load_profile(self.profiles[0])
else:
self.status.setText('No Claude installs found. Use "Add config…" to point at one.')
self._maybe_warn_msix()
def _maybe_warn_msix(self):
"""
Windows-only, no-op everywhere else: if Claude Desktop looks like an
MSIX/Store install with a virtualized config, append a warning to the
status bar so edits to the plain %APPDATA% path aren't silently lost.
Defensive on purpose -- this must never block startup or profile load.
"""
try:
warning = core.msix_warning_text()
except Exception:
return
if warning:
self.status.setText(f"{self.status.text()}{warning}")
self.status.setToolTip(warning)
def add_custom_config(self):
start = str(core.app_support_base())
+102
View File
@@ -191,6 +191,97 @@ def app_support_base() -> Path:
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
def msix_config_paths(localappdata: str | os.PathLike | None = None) -> list[Path]:
"""
Find MSIX/Store-packaged Claude Desktop configs.
When Claude Desktop is installed from the Microsoft Store (MSIX), Windows
virtualizes its filesystem writes to a per-package folder under
`%LOCALAPPDATA%\\Packages\\<PackageFamilyName>\\LocalCache\\Roaming\\Claude\\`
instead of the normal `%APPDATA%\\Claude\\`. A user (or BCC) editing the
plain %APPDATA% path can end up changing a file the running app never
reads -- see anthropics/claude-code issues #26073, #29100, #38830.
Globs `<localappdata>/Packages/*Claude*/LocalCache/Roaming/Claude/
claude_desktop_config.json` and returns every match that actually exists,
sorted for determinism. `localappdata` defaults to the %LOCALAPPDATA% env
var (falling back to the usual Windows path) but is accepted as a
parameter so this is unit-testable with tmp_path on any platform.
This function itself is platform-independent (it just globs whatever
directory it's given); callers that care about the *current* machine
should gate on sys.platform -- see `detect_msix_claude`.
"""
base = (
Path(localappdata)
if localappdata is not None
else Path(os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local")))
)
packages = base / "Packages"
if not packages.is_dir():
return []
out: list[Path] = []
for pkg_dir in sorted(packages.glob("*Claude*")):
cfg = pkg_dir / "LocalCache" / "Roaming" / "Claude" / CONFIG_FILENAME
if cfg.is_file():
out.append(cfg)
return out
def detect_msix_claude(
appdata: str | os.PathLike | None = None,
localappdata: str | os.PathLike | None = None,
) -> Path | None:
"""
Best-effort detection of an MSIX-virtualized Claude Desktop install.
Returns the first virtualized `claude_desktop_config.json` found (see
`msix_config_paths`), or None when not running on Windows, no matching
package folder exists, or a package folder exists but has no config file
written yet. The sys.platform gate makes this a safe no-op to call
unconditionally from discovery/diagnostics code on macOS/Linux.
`appdata`/`localappdata` are threaded through (rather than read straight
from os.environ) purely so the whole detection path is unit-testable via
tmp_path + monkeypatch without mutating real env vars.
"""
if not sys.platform.startswith("win"):
return None
hits = msix_config_paths(localappdata)
return hits[0] if hits else None
def msix_warning_text(
appdata: str | os.PathLike | None = None,
localappdata: str | os.PathLike | None = None,
) -> str | None:
"""
A one-line, paste-safe warning for the diagnostics/status surface when
Claude Desktop looks like an MSIX/Store install whose real config lives
somewhere other than the plain %APPDATA%\\Claude\\ path. Returns None
when nothing was detected (including on non-Windows platforms) or when
the virtualized path and the plain path happen to coincide -- i.e. there
is nothing surprising to warn about. Contains only filesystem paths, no
env values or secrets.
"""
real = detect_msix_claude(appdata, localappdata)
if real is None:
return None
plain_base = (
Path(appdata)
if appdata is not None
else Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
)
plain_cfg = plain_base / "Claude" / CONFIG_FILENAME
if plain_cfg == real:
return None
return (
"Claude Desktop looks like it's installed from the Microsoft Store (MSIX). "
f"Windows virtualizes its config, so edits to {plain_cfg} may be silently "
f"ignored by the running app. The real config is at: {real}"
)
def discover_profiles() -> list[Profile]:
"""
Find every `Claude*` data directory in the platform's app-support base
@@ -198,6 +289,11 @@ def discover_profiles() -> list[Profile]:
Claude Desktop: scans the platform app-support folder for any `Claude*`
directory (catches `Claude`, `Claude-Work`, etc.).
Windows/MSIX: if Claude Desktop was installed from the Microsoft Store,
its real config lives in a virtualized per-package folder rather than the
plain %APPDATA%\\Claude\\ path above (see `detect_msix_claude`); when
that's detected, it's surfaced here as its own profile so the user can
edit the file the app actually reads.
Claude Code: user-scope MCP servers live in ~/.claude.json (that's what
`claude mcp add` writes; project scope is a per-repo .mcp.json, which can
be opened via 'Add config…'). NOT ~/.claude/settings.json — that file is
@@ -213,6 +309,12 @@ def discover_profiles() -> list[Profile]:
cfg = d / CONFIG_FILENAME
out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file()))
msix_cfg = detect_msix_claude()
if msix_cfg is not None and str(msix_cfg) not in {str(p.path) for p in out}:
out.append(
Profile(label="Claude (Microsoft Store / MSIX)", path=msix_cfg, config_exists=True)
)
home = Path.home()
cc_cfg = home / ".claude.json"
out.append(Profile(label="Claude Code", path=cc_cfg, config_exists=cc_cfg.is_file()))
+146
View File
@@ -758,6 +758,152 @@ def test_server_log_path_unsupported_platform_returns_none(tmp_path, monkeypatch
assert c.server_log_path("brave-search") is None
# --------------------------------------------------------------------------- #
# MSIX-virtualized Claude Desktop config detection (issue #7)
# --------------------------------------------------------------------------- #
def _make_msix_pkg(localappdata, pkg_name="AnthropicClaude_abc123xyz", with_config=True):
cfg_dir = localappdata / "Packages" / pkg_name / "LocalCache" / "Roaming" / "Claude"
cfg_dir.mkdir(parents=True)
cfg_path = cfg_dir / c.CONFIG_FILENAME
if with_config:
cfg_path.write_text('{"mcpServers": {}}')
return cfg_path
def test_msix_config_paths_finds_package_config(tmp_path):
local = tmp_path / "Local"
expected = _make_msix_pkg(local)
assert c.msix_config_paths(local) == [expected]
def test_msix_config_paths_ignores_non_claude_packages(tmp_path):
local = tmp_path / "Local"
(local / "Packages" / "SomeOtherApp_xyz" / "LocalCache" / "Roaming").mkdir(parents=True)
assert c.msix_config_paths(local) == []
def test_msix_config_paths_empty_when_package_dir_has_no_config_yet(tmp_path):
local = tmp_path / "Local"
_make_msix_pkg(local, with_config=False)
assert c.msix_config_paths(local) == []
def test_msix_config_paths_empty_when_no_packages_dir(tmp_path):
assert c.msix_config_paths(tmp_path / "Local") == []
def test_msix_config_paths_uses_localappdata_env_by_default(tmp_path, monkeypatch):
local = tmp_path / "Local"
expected = _make_msix_pkg(local)
monkeypatch.setenv("LOCALAPPDATA", str(local))
assert c.msix_config_paths() == [expected]
def test_detect_msix_claude_returns_none_on_non_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "darwin")
local = tmp_path / "Local"
_make_msix_pkg(local)
assert c.detect_msix_claude(localappdata=local) is None
def test_detect_msix_claude_finds_virtualized_config_on_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
local = tmp_path / "Local"
expected = _make_msix_pkg(local)
assert c.detect_msix_claude(localappdata=local) == expected
def test_detect_msix_claude_none_when_nothing_present_on_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
local = tmp_path / "Local"
assert c.detect_msix_claude(localappdata=local) is None
def test_msix_warning_text_none_on_non_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "darwin")
local = tmp_path / "Local"
_make_msix_pkg(local)
assert c.msix_warning_text(localappdata=local) is None
def test_msix_warning_text_none_when_nothing_detected(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
local = tmp_path / "Local"
assert c.msix_warning_text(localappdata=local) is None
def test_msix_warning_text_warns_when_virtualized_path_differs(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
roaming = tmp_path / "Roaming"
local = tmp_path / "Local"
real = _make_msix_pkg(local)
warning = c.msix_warning_text(appdata=roaming, localappdata=local)
assert warning is not None
assert "Microsoft Store" in warning
assert str(real) in warning
assert str(roaming / "Claude" / c.CONFIG_FILENAME) in warning
def test_msix_warning_text_none_when_plain_and_virtualized_paths_match(tmp_path, monkeypatch):
# Degenerate case: if the "plain" APPDATA base is pointed at the exact
# same tree the MSIX scan found (e.g. a symlink setup), there's nothing
# surprising to warn about.
monkeypatch.setattr(c.sys, "platform", "win32")
local = tmp_path / "Local"
pkg_claude_dir = local / "Packages" / "AnthropicClaude_abc123xyz" / "LocalCache" / "Roaming"
pkg_claude_dir.mkdir(parents=True)
(pkg_claude_dir / "Claude").mkdir()
(pkg_claude_dir / "Claude" / c.CONFIG_FILENAME).write_text("{}")
warning = c.msix_warning_text(appdata=pkg_claude_dir, localappdata=local)
assert warning is None
def test_discover_profiles_adds_msix_profile_on_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
base = tmp_path / "AppSupport"
base.mkdir()
monkeypatch.setattr(c, "app_support_base", lambda: base)
monkeypatch.setattr(c.Path, "home", lambda: tmp_path / "home")
(tmp_path / "home" / ".claude").mkdir(parents=True)
local = tmp_path / "Local"
real = _make_msix_pkg(local)
monkeypatch.setenv("LOCALAPPDATA", str(local))
profs = c.discover_profiles()
msix = [p for p in profs if "MSIX" in p.label]
assert len(msix) == 1
assert msix[0].path == real
assert msix[0].config_exists
def test_discover_profiles_no_msix_profile_when_nothing_detected(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "win32")
base = tmp_path / "AppSupport"
base.mkdir()
monkeypatch.setattr(c, "app_support_base", lambda: base)
monkeypatch.setattr(c.Path, "home", lambda: tmp_path / "home")
(tmp_path / "home" / ".claude").mkdir(parents=True)
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "Local"))
profs = c.discover_profiles()
assert not any("MSIX" in p.label for p in profs)
def test_discover_profiles_no_msix_profile_on_non_windows(tmp_path, monkeypatch):
monkeypatch.setattr(c.sys, "platform", "darwin")
base = tmp_path / "AppSupport"
base.mkdir()
monkeypatch.setattr(c, "app_support_base", lambda: base)
monkeypatch.setattr(c.Path, "home", lambda: tmp_path / "home")
(tmp_path / "home" / ".claude").mkdir(parents=True)
local = tmp_path / "Local"
_make_msix_pkg(local)
monkeypatch.setenv("LOCALAPPDATA", str(local))
profs = c.discover_profiles()
assert not any("MSIX" in p.label for p in profs)
# --------------------------------------------------------------------------- #
# restart_claude_desktop / profile_targets_claude_desktop (issue #9)
# --------------------------------------------------------------------------- #