From 73272ad4c35f7ed7c839d1150d1b5f3a1ebf17d6 Mon Sep 17 00:00:00 2001 From: agrechenkov Date: Wed, 1 Jul 2026 11:23:07 -0400 Subject: [PATCH] Fix: unify autostart to one per-user Run entry (was 3 divergent keys) Three independent autostart registrations had drifted out of sync: - daemon (ffp_daemon._act_set_autostart): HKCU\...\Run\FastFlowPrompt -- what the dashboard's "Launch Flowkey when I sign in" toggle reads/writes. - installer/install.ps1 (source install): HKCU\...\Run\Flowkey -- same key path, DIFFERENT value name, so the dashboard toggle couldn't see or manage it, and toggling it on added a second, redundant entry. - installer/installer.iss (packaged installer): optional machine-wide HKLM\...\Run\Flowkey via an install-time "autostart" task -- a third, independent mechanism in a different hive entirely. Any combination of these could double-launch grammarFix.ahk + the daemon at logon. Fix: unify everyone on the one HKCU\...\Run\FastFlowPrompt entry the daemon owns. - install.ps1: $RunKeyName "Flowkey" -> "FastFlowPrompt" (matches ffp_daemon._AUTOSTART_VALUE_NAME exactly). - installer.iss: removed the "autostart" Task and its HKLM [Registry] entry entirely (the installer no longer offers machine-wide autostart); added an [UninstallRun] step that removes the per-user HKCU value on uninstall. - first_run.py: removed the dead, now-inaccurate var_autostart_hint (declared, never rendered/read; its comment claimed "installer owns Run key", which is no longer even conceptually true). - installer/README.md: corrected the uninstaller description. New test_installer_autostart.py cross-references ffp_daemon's actual constant (not a hardcoded string) so install.ps1 and installer.iss can never silently drift from the daemon again. Also: removed the Serena MCP server (unregistered globally + deleted the project's local .serena/ cache) and a stale, long-superseded codex/add-autohotkey-runtime branch on origin -- both were artifacts of an external agent that had been operating on this project. Gates: ruff clean, 323 pytest passed (+3), node --check, AHK parse-check. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + SPEC.md | 7 +++-- installer/README.md | 2 +- installer/install.ps1 | 7 ++++- installer/installer.iss | 22 +++++++++------ scripts/first_run.py | 1 - tests/test_installer_autostart.py | 47 +++++++++++++++++++++++++++++++ 7 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 tests/test_installer_autostart.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1f29e..5e1a206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - The Meetings results list is now scrollable and shows a meeting counter. - **Packaging now ships all runtime modules.** `ffp_meetings`, `ffp_notifications`, and `ffp_quill` were missing from `pyproject.toml` `py-modules` and the PyInstaller spec `hiddenimports`, so a wheel / frozen installer could omit them and crash on import even though source-tree tests passed. All three are now declared, and a new test (`test_packaging_modules`) asserts `py-modules` and the spec stay in sync with `scripts/*.py`. - The Telemetry time-of-day chart now renders only **active hours** — zero-activity hours are dropped instead of drawn as empty bars (and an empty history shows "No activity yet"). +- **Autostart unified to a single per-user entry.** Three independent autostart registrations had drifted out of sync: the daemon wrote `HKCU\...\Run\FastFlowPrompt` (what the dashboard toggle reads/writes), a source install (`install.ps1`) wrote a *different* value name (`HKCU\...\Run\Flowkey`) the toggle couldn't see, and the packaged installer optionally wrote a third, machine-wide `HKLM\...\Run\Flowkey` entry — enabling the dashboard toggle after either install path could add a redundant entry and launch the app twice at logon. All three now agree on the one per-user `HKCU\...\Run\FastFlowPrompt` entry the daemon owns; the packaged installer no longer offers a machine-wide autostart option, and its uninstaller now removes the per-user entry. Guarded by a new `test_installer_autostart` regression test. ### Internal diff --git a/SPEC.md b/SPEC.md index ab3dca9..596ee47 100644 --- a/SPEC.md +++ b/SPEC.md @@ -16,7 +16,7 @@ Caveman-encoded (compression, not amputation). Paths / ids / action names / numb - LLM: FastFlowLM NPU @ `:52625` | Ollama @ `:11434`, OpenAI-compat `POST /v1/chat/completions` - dashboard: daemon-served `scripts/ui/web/{index.html,app.js,styles.css}`, CSP `default-src 'self'` - paths: `scripts/paths.py` → USER_ROOT/{config,data,logs}; `_version.py` = version src of truth -- version: current `2.1.0` (held `release/v2.1.0`); public `2.0.0`; repo `agr77one/Fastflow` +- version: `2.1.0` merged to `main`, tag/installer/Release NOT yet cut; public `2.0.0`; repo `agr77one/Fastflow` - run tree = `flowkey-pub2` (worktree, branch `live`=origin/main). old `FastFlowPrompt_Local_Setup`=1.5.0 stale. ## §I interfaces @@ -81,13 +81,13 @@ T6|x|git autosync: `sync.ps1` + daily task + autostart→flowkey-pub2|V21,V22 T7|~|2.1.0 release held on `release/v2.1.0` → land after user test|V18,V19 T8|.|installer clean-VM smoke test|— T9|.|[AUDIT] dead-code: unused daemon helper + 2 AHK wrappers + stale chat-popup config key + obsolete settings ref in test fixture + deprecated install shims|— -T10|.|[AUDIT-P1] autostart: unify 3 divergent Run keys (daemon/src-installer/pkg-installer) → single HKCU entry; fix UI autostart status reporting|V20 +T10|x|[AUDIT-P1] autostart: unify 3 divergent Run keys (daemon/src-installer/pkg-installer) → single HKCU entry|V20,B6 T11|.|[AUDIT] old open_chat default `^+t` still appears in first-run + web config fallback → replace with `^!c`|B5 T12|.|[AUDIT] first-run seed thinner than DEFAULT_CONFIG schema → add seed-vs-schema drift guard (compare keys on first-run copy)|— T13|.|[AUDIT] installer bootstrap wrapper hardcodes old installer filename → derive from `_version.py`|V18 T14|.|[AUDIT] quality-gate gaps: installer policy drift, autostart reg-name drift, bootstrap output name, README/dashboard tab count|V20 T15|.|[DOCS] dashboard docs: 7 tabs listed, live = 8 (add Benchmark)|— -T16|.|[DOCS] autostart docs conflict: main says no machine-wide entry; installer docs+impl still describe it → align on HKCU-only|— +T16|x|[DOCS] autostart docs conflict: main says no machine-wide entry; installer docs+impl still describe it → align on HKCU-only|B6 T17|.|[DOCS] installer layout: build script says flattened, installer.md still shows nested layout|— T18|.|[DOCS] provider roadmap marks selector/status UX incomplete → update to reflect it exists|— T19|.|[DOCS] first-run wizard text: "chat popup" + retired hotkey → update to current|B5 @@ -103,4 +103,5 @@ B2|2026-06|bench history blank row from 0-point result file|skip `rows==[]` in ` B3|2026-06|autostart → stale tree / empty `flowkey-public`|repoint HKCU Run → flowkey-pub2 + bundled AHK B4|2026-06|install launch: AHK called `.py`, shipped only `.exe`|flatten bundle to {app} + AHK→exe bridge (PR #19) B5|2026-06|`Ctrl+Shift+T` open_chat collided w/ browser reopen-tab|default → `^!c`; tray label = configured hotkey +B6|2026-07|3 divergent autostart Run keys: daemon HKCU\Run\FastFlowPrompt, `install.ps1` HKCU\Run\Flowkey (different name!), `installer.iss` optional HKLM\Run\Flowkey → toggle blind to other 2, could double-launch|unify on HKCU\Run\FastFlowPrompt everywhere; drop installer.iss HKLM task; uninstall now cleans the HKCU value; guarded by `test_installer_autostart.py` ``` diff --git a/installer/README.md b/installer/README.md index 900cfdb..469b964 100644 --- a/installer/README.md +++ b/installer/README.md @@ -101,7 +101,7 @@ Debug flags: 3. Asks (default = No) whether to wipe per-user data at `%LOCALAPPDATA%\FastFlowPrompt\`. The user can decline and keep their notes / config / logs across reinstalls. -4. Removes the HKLM `Run` autostart entry. +4. Removes the per-user `HKCU\...\Run\FastFlowPrompt` autostart entry, if one was set via the dashboard's "Launch Flowkey when I sign in" toggle. The installer itself never sets machine-wide (HKLM) autostart — that's a single per-user entry the daemon owns. ## End-user SmartScreen note diff --git a/installer/install.ps1 b/installer/install.ps1 index c68f53a..e241897 100644 --- a/installer/install.ps1 +++ b/installer/install.ps1 @@ -72,7 +72,12 @@ $venvDir = Join-Path $scriptsDir ".venv" $venvPythonw = Join-Path $venvDir "Scripts\pythonw.exe" $RunKeyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -$RunKeyName = "Flowkey" +# Must match ffp_daemon._AUTOSTART_VALUE_NAME exactly -- that daemon action is +# the single source of truth the dashboard checkbox reads/writes. A source +# install used to register a DIFFERENT value name ("Flowkey") here, so the +# dashboard toggle couldn't see it (and enabling the toggle added a second, +# redundant Run entry -> double launch at logon). See SPEC.md B6 / T10. +$RunKeyName = "FastFlowPrompt" function Info($m) { Write-Host "[FFP] $m" -ForegroundColor Cyan } function Ok($m) { Write-Host "[ok] $m" -ForegroundColor Green } diff --git a/installer/installer.iss b/installer/installer.iss index d05d643..909f1d1 100644 --- a/installer/installer.iss +++ b/installer/installer.iss @@ -84,8 +84,11 @@ MinVersion=10.0.17763 Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] -Name: "autostart"; Description: "Launch {#AppName} when Windows starts (all users)"; \ - GroupDescription: "Additional options:" +; NOTE: autostart is intentionally NOT an install-time task. The daemon owns a +; single per-user HKCU\...\Run\FastFlowPrompt entry (Dashboard -> Config -> +; "Launch Flowkey when I sign in") -- that's the one source of truth. A prior +; machine-wide HKLM task here used a different value name and could run +; alongside the per-user one, double-launching the app at logon. See T10/B6. Name: "desktopicon"; Description: "Create a desktop shortcut"; \ GroupDescription: "Additional options:"; Flags: unchecked @@ -160,13 +163,6 @@ Name: "{commondesktop}\{#AppName}"; Filename: "{app}\ahk\AutoHotkey64 Parameters: """{app}\scripts\grammarFix.ahk"""; WorkingDir: "{app}"; \ IconFilename: "{app}\ffp-daemon.exe"; Tasks: desktopicon -[Registry] -; --- Autostart (per-machine HKLM Run) — controlled by the autostart task ----- -Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \ - ValueType: string; ValueName: "{#AppName}"; \ - ValueData: """{app}\ahk\AutoHotkey64.exe"" ""{app}\scripts\grammarFix.ahk"""; \ - Flags: uninsdeletevalue; Tasks: autostart - [UninstallRun] ; --- 1. Stop our processes before removing files ----------------------------- ; CloseApplications=force handles in-use files but a windowless daemon @@ -177,6 +173,14 @@ Filename: "{sys}\taskkill.exe"; \ Parameters: "/F /IM AutoHotkey64.exe /FI ""WINDOWTITLE eq grammarFix*"""; \ RunOnceId: "KillAhk"; Flags: runhidden waituntilterminated +; --- 1b. Remove the per-user autostart entry, if the dashboard toggle or a +; source install set it (installer itself never sets it — see [Tasks]). +; reg.exe exits non-zero when the value is absent; that's fine, Inno +; doesn't treat a nonzero [UninstallRun] exit as fatal. +Filename: "{sys}\reg.exe"; \ + Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""FastFlowPrompt"" /f"; \ + RunOnceId: "RemoveAutostart"; Flags: runhidden waituntilterminated + ; --- 2. Chain FLM uninstaller — but ONLY if we installed it ------------------ ; We tagged it with {app}\.flm_installed_by_us. Pascal helper reads the ; QuietUninstallString out of the registry and runs it silently. diff --git a/scripts/first_run.py b/scripts/first_run.py index 1aea40d..e31d40e 100644 --- a/scripts/first_run.py +++ b/scripts/first_run.py @@ -309,7 +309,6 @@ def __init__(self) -> None: self.var_base_url = tk.StringVar(value=str(llm.get("base_url") or self.cfg.get("flm_base_url") or base_url)) self.var_model = tk.StringVar(value=str(llm.get("model") or self.cfg.get("flm_model") or default_model)) self.var_license_accept = tk.BooleanVar(value=False) - self.var_autostart_hint = tk.BooleanVar(value=True) # display-only — installer owns Run key hk = self.cfg.get("hotkeys") or {} self.var_hotkeys: dict[str, tk.StringVar] = { diff --git a/tests/test_installer_autostart.py b/tests/test_installer_autostart.py new file mode 100644 index 0000000..b980fc5 --- /dev/null +++ b/tests/test_installer_autostart.py @@ -0,0 +1,47 @@ +"""Drift guard: autostart must have exactly ONE mechanism — the daemon's +per-user HKCU\\...\\Run entry (ffp_daemon._AUTOSTART_VALUE_NAME), managed by the +dashboard's "Launch Flowkey when I sign in" toggle. + +Regression: three independent Run-key registrations had drifted out of sync — +the daemon wrote HKCU\\Run\\FastFlowPrompt, installer/install.ps1 (source +install) wrote a DIFFERENT value HKCU\\Run\\Flowkey, and installer/installer.iss +(packaged installer) optionally wrote a separate machine-wide HKLM\\Run\\Flowkey +via an install-time task. The dashboard toggle only knew about the first, so a +source or packaged install could register autostart the toggle couldn't see or +control, and enabling the toggle afterwards added a second, redundant entry — +launching the app twice at logon. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" + +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import ffp_daemon # noqa: E402 + + +def test_install_ps1_uses_the_same_value_name_as_the_daemon(): + text = (ROOT / "installer" / "install.ps1").read_text(encoding="utf-8") + m = re.search(r'\$RunKeyName\s*=\s*"([^"]+)"', text) + assert m, "install.ps1 no longer declares $RunKeyName" + assert m.group(1) == ffp_daemon._AUTOSTART_VALUE_NAME + + +def test_installer_iss_has_no_machine_wide_autostart(): + text = (ROOT / "installer" / "installer.iss").read_text(encoding="utf-8") + assert "Tasks: autostart" not in text, "a separate install-time autostart task reappeared" + assert not re.search(r"Root:\s*HKLM.*\n.*CurrentVersion\\Run", text), \ + "installer.iss writes an HKLM Run entry — autostart must be per-user (HKCU) only" + + +def test_installer_iss_uninstall_cleans_the_same_value_name(): + text = (ROOT / "installer" / "installer.iss").read_text(encoding="utf-8") + assert f'/v ""{ffp_daemon._AUTOSTART_VALUE_NAME}""' in text, \ + "uninstaller doesn't clean up the daemon's actual per-user autostart value"