release: v1.2.0 — About+update checker, log viewer, restart button, dup-name fix, stale-file hardening #26

Merged
the_og merged 34 commits from release/v1.2.0 into main 2026-07-08 11:34:35 -04:00
Owner

Integration of the five reviewed feature branches for the v1.2.0 release, merged with real git 3-way merges, conflicts resolved, and verified green locally (ruff format --check, ruff check, pytest ~138 tests pass).

Closes #6
Closes #8
Closes #9
Closes #17
Closes #18
Closes #19

Included

  • #18 + #19 — About dialog (Help menu, repo/issues/license links) + notify-only update checker (__version__ single source, numeric version compare, fail-quiet tokenless releases fetch, throttled once/day startup check). (PR #25)
  • #9 — Restart Claude Desktop button after save, gated to Desktop profiles only. (PR #24)
  • #6 — In-app MCP server log viewer (server_log_path() + read-only auto-tailing LogViewerDialog). (PR #23)
  • #17 — Stale-file detection hardened with a mtime+size fingerprint (ConfigStat/config_fingerprint). (PR #22)
  • #8 — Duplicate-name collision fix: closes a real silent-overwrite on drag-and-drop import; shared _import_server prompt + pure resolve_name_collision. (PR #21)

Conflict resolutions

  • _normalize_unicode NBSP line — kept the single out.replace(chr(0xA0), " ") form (all branches were equivalent).
  • bcc.pyLogViewerDialog (#6) and AboutDialog (#18/19) both inserted a class at the same anchor; kept both. Imports and MainWindow hooks merged cleanly.
  • tests/test_core.py — each branch appended tests at EOF; concatenated all.
  • Version bumped 1.1.0 → 1.2.0 in __version__, pyproject.toml, and test_dunder_version_matches_pyproject.

Supersedes the individual feature PRs #21–#25 (to be closed on merge).

Integration of the five reviewed feature branches for the **v1.2.0** release, merged with real git 3-way merges, conflicts resolved, and verified green locally (`ruff format --check`, `ruff check`, `pytest` ~138 tests pass). Closes #6 Closes #8 Closes #9 Closes #17 Closes #18 Closes #19 ## Included - **#18 + #19** — About dialog (Help menu, repo/issues/license links) + notify-only update checker (`__version__` single source, numeric version compare, fail-quiet tokenless releases fetch, throttled once/day startup check). *(PR #25)* - **#9** — Restart Claude Desktop button after save, gated to Desktop profiles only. *(PR #24)* - **#6** — In-app MCP server log viewer (`server_log_path()` + read-only auto-tailing `LogViewerDialog`). *(PR #23)* - **#17** — Stale-file detection hardened with a mtime+size fingerprint (`ConfigStat`/`config_fingerprint`). *(PR #22)* - **#8** — Duplicate-name collision fix: closes a real silent-overwrite on drag-and-drop import; shared `_import_server` prompt + pure `resolve_name_collision`. *(PR #21)* ## Conflict resolutions - `_normalize_unicode` NBSP line — kept the single `out.replace(chr(0xA0), " ")` form (all branches were equivalent). - `bcc.py` — `LogViewerDialog` (#6) and `AboutDialog` (#18/19) both inserted a class at the same anchor; kept both. Imports and `MainWindow` hooks merged cleanly. - `tests/test_core.py` — each branch appended tests at EOF; concatenated all. - Version bumped 1.1.0 → 1.2.0 in `__version__`, `pyproject.toml`, and `test_dunder_version_matches_pyproject`. Supersedes the individual feature PRs #21–#25 (to be closed on merge).
the_og added 34 commits 2026-07-08 11:33:55 -04:00
Bare mtime equality can miss a concurrent external write that lands
within the filesystem's mtime resolution (same-second writes), or
where the writer restores the original mtime. Add config_fingerprint()
returning a (mtime, size) ConfigStat pair; the stale-file check in
bcc.py now compares both fields instead of mtime alone. config_mtime()
is kept as-is (still used/tested independently).
The previous commit accidentally normalized the non-breaking space
(U+00A0) in _normalize_unicode's replace() call to a regular space
during a copy/paste, turning that replace() into a no-op. Use an
explicit   escape instead of the literal character so it can't
be silently corrupted again.
- Add `__version__ = "1.1.0"` as the single source of truth (matches pyproject.toml).
- Add parse_version()/is_newer_version() for numeric (never lexical) version
  comparison, handling v-prefix, pre-release suffixes, and malformed input.
- Add fetch_latest_release(): reads the public Gitea releases API
  (anonymous, no token) and returns {version, url} or None on any failure.
  Never downloads or touches a binary — metadata only.
The previous commit message said this was fixed but the content still
had the literal (unescaped) line. Apply the   escape for real
this time.
Platform-abstracted restart: macOS uses pkill -x Claude + open -a Claude,
Windows uses taskkill + relaunch via the Start-menu shortcut, Linux uses
pkill claude + a detached Popen relaunch. Not finding a running process is
not an error -- only a failed relaunch is reported as failure.

Also adds profile_targets_claude_desktop() to distinguish Claude Desktop
profiles (claude_desktop_config.json) from Claude Code profiles, used to
gate the restart button in the GUI.
Two prior attempts to fix this line via a literal or  -escaped
non-breaking space both silently reverted back to a no-op regular-space
replace during transcription. Using chr(0xA0) instead removes any
non-ASCII or backslash-escape character from the source line entirely.
tests/test_core.py: add resolve_name_collision unit tests
CI / Lint (ruff) (pull_request) Failing after 8s
CI / Tests (py3.10) (pull_request) Failing after 8s
CI / Tests (py3.12) (pull_request) Failing after 8s
0843c51c7d
After a successful save, shows a 'Restart Claude Desktop' button next to
the status line. Only shown when the saved profile targets Claude Desktop
(core.profile_targets_claude_desktop) -- never for Claude Code, which has
no GUI process to bounce. Clicking it calls core.restart_claude_desktop()
and reports success/failure. The button hides again on further edits or
when switching/reloading profiles.
MainWindow._loaded_mtime -> _loaded_stat (core.ConfigStat), populated
via core.config_fingerprint() at load/save time. The save-path stale
check now compares the full fingerprint (mtime AND size) instead of
bare mtime equality.
test: add coverage for config_fingerprint mtime+size hardening (#17)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 11s
CI / Tests (py3.12) (pull_request) Successful in 7s
f9752211a2
Includes the required regression case: rewrite a file with different
content, force the original mtime back via os.utime, and assert the
fingerprint still differs (because size changed).
feat: add View logs button + LogViewerDialog for in-app MCP log viewer (#6)
CI / Lint (ruff) (pull_request) Successful in 6s
CI / Tests (py3.10) (pull_request) Successful in 7s
CI / Tests (py3.12) (pull_request) Successful in 8s
2b3843a714
test: cover restart_claude_desktop() and profile_targets_claude_desktop() (#9)
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
16961a5cc8
Mocks subprocess.run/Popen and patches sys.platform per-OS branch (darwin,
win32, linux) -- never actually kills or launches anything. Covers: correct
command sequence per platform, pkill/taskkill exiting non-zero (nothing to
kill) is NOT treated as failure, a failed relaunch IS reported as failure,
and profile_targets_claude_desktop() correctly distinguishes Claude Desktop
configs from Claude Code / legacy settings.json profiles.
- Add a real QMenuBar (Help -> About...).
- AboutDialog: app icon, name, version (core.__version__), and links to the
  repo/issues/license opened via QDesktopServices.openUrl (system browser,
  never in-app navigation). macOS unsigned-app note.
- "Check for updates" button + UpdateCheckWorker (QThread) runs
  core.fetch_latest_release() off the UI thread; shows "up to date" or
  "vX.Y.Z available" with a button to open the releases page. No binary
  download, ever.
- Optional quiet startup auto-check, throttled to once/day via a QSettings
  timestamp, off-thread, silent on failure, toggle lives in the About dialog.
test: cover __version__, parse_version/is_newer_version, and fetch_latest_release (#19)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 9s
CI / Tests (py3.12) (pull_request) Successful in 7s
3cd18392c9
All network calls are monkeypatched (urllib.request.urlopen) — no live
network in tests. Covers older/newer/equal comparisons, v-prefix,
differing-length tuples, malformed input on both sides, and
fetch_latest_release success/timeout/network-failure/malformed-response
paths.
Fix: rebuild from clean main (previous push included unrelated contaminated content)
CI / Lint (ruff) (pull_request) Failing after 6s
CI / Tests (py3.10) (pull_request) Successful in 7s
CI / Tests (py3.12) (pull_request) Successful in 8s
a811e323e6
Fix: rebuild from clean main (previous push included unrelated contaminated content)
CI / Lint (ruff) (pull_request) Failing after 6s
CI / Tests (py3.10) (pull_request) Successful in 7s
CI / Tests (py3.12) (pull_request) Successful in 7s
3e07b51134
Fix: rebuild from clean main (previous push included unrelated contaminated content)
CI / Lint (ruff) (pull_request) Failing after 8s
CI / Tests (py3.10) (pull_request) Failing after 8s
CI / Tests (py3.12) (pull_request) Failing after 7s
4ab3c3b00a
Fix: correct smart-quote/nbsp characters mangled in previous push
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
2d9fb083dc
# Conflicts:
#	bcc_core.py
#	tests/test_core.py
# Conflicts:
#	bcc_core.py
#	tests/test_core.py
# Conflicts:
#	bcc.py
#	bcc_core.py
#	tests/test_core.py
chore: bump version to 1.2.0 for release
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10) (pull_request) Successful in 8s
CI / Tests (py3.12) (pull_request) Successful in 8s
8c718387c0
the_og merged commit 4c6fe7c5aa into main 2026-07-08 11:34:35 -04:00
the_og deleted branch release/v1.2.0 2026-07-08 11:34:35 -04:00
Sign in to join this conversation.