# 18 — Install Helper

## AI grep hints

- Keys: `installer.title`, `installer.subtitle`, `installer.railLabel`, `installer.refresh`, `installer.lastChecked`, `installer.checkingDots`, `installer.checkingForUpdates`, `installer.updateAll`, `installer.section.installed`, `installer.section.available`, `installer.section.essentials`, `installer.section.aiAgents`, `installer.section.aiPlatforms`, `installer.section.development`, `installer.section.design`, `installer.section.windowsPowerUser`, `installer.section.remoteAccess`, `installer.section.packageManagers`, `installer.section.utilities`, `installer.tile.latest`, `installer.tile.installed`, `installer.actions.install`, `installer.actions.update`, `installer.actions.uninstall`, `installer.actions.run`, `installer.actions.stop`, `installer.actions.openWebUi`, `installer.actions.addToWorkspace`, `installer.actions.installService`, `installer.actions.registerService`, `installer.actions.removeService`, `installer.actions.cancel`, `installer.options.scope`, `installer.options.scopeUser`, `installer.options.scopeMachine`, `installer.options.scopeSelfElevatingHint`, `installer.options.provider`, `installer.options.version`, `installer.options.location`, `installer.options.addToPath`, `installer.options.pinVersion`, `installer.status.installing`, `installer.status.uninstalling`, `installer.status.completed`, `installer.status.failed`, `installer.status.cancelled`, `installer.status.running`, `installer.status.stopped`, `installer.status.unknown`, `installer.status.serviceInstalled`, `installer.status.addedToWorkspace`, `installer.status.serviceRemoved`, `installer.status.partial`, `installer.status.noVersion`, `installer.status.web`, `installer.status.notInstalled`, `installer.status.retrieving`, `installer.status.neverChecked`, `installer.status.scanning`, `installer.empty.loading`, `installer.confirm.installTitle`, `installer.confirm.installWithPrereqsBody`, `installer.confirm.uacFooter`, `installer.confirm.uninstallTitle`, `installer.confirm.uninstallSimpleBody`, `installer.confirm.uninstallDependentsBody`, `installer.confirm.uninstallDependentsFooter`, `installer.confirm.updateAllTitle`, `installer.confirm.updateAllBody`, `installer.confirm.updateAllConfirm`, `installer.wslReboot`, `installer.dialog.installLocation`, `installer.dialog.provider`, `installer.dialog.installedVersion`, `installer.dialog.latestVersion`, `installer.dialog.lastChecked`, `installer.dialog.homepage`, `installer.dialog.releaseNotes`, `installer.dialog.webUi`, `installer.dialog.windowsService`, `installer.dialog.runtimeStatus`, `installer.dialog.serviceStartup`, `installer.dialog.runHints`, `installer.dialog.quickLaunch`, `installer.quickLaunch.search`, `installer.quickLaunch.openTerminal`, `installer.quickLaunch.cli`, `installer.quickLaunch.launch`, `installer.quickLaunch.noMatches`, `installer.dialog.prerequisites`, `installer.dialog.prereqInstalled`, `installer.dialog.prereqMissing`, `installer.dialog.checkNow`, `installer.dialog.checkingDots`, `installer.dialog.updateAvailable`, `installer.dialog.installingTitle`, `installer.dialog.uninstallingTitle`, `installer.dialog.installedTitle`, `installer.dialog.failedTitle`, `installer.dialog.cancelledTitle`, `installer.stepper.failedBadge`, `installer.steps.resolve`, `installer.steps.download`, `installer.steps.verifyChecksum`, `installer.steps.extract`, `installer.steps.placeFiles`, `installer.steps.updatePath`, `installer.steps.install`, `installer.steps.verify`, `installer.steps.enable`
- Topics: Install Helper Module, bundled catalog (compile-time embedded), ADR 0008 supersedes ADR 0007, winget recipes, Chocolatey recipes and Chocolatey alternate providers, npm recipes, uv pip recipes, download-installer recipes, github-release recipes and GitHub-release download providers, Windows feature recipes (DISM), WSL distro recipes, bundles (node-bundle, python-bundle), dependency resolution, UAC prompts, WSL reboot gating, pin version, in-flight cancellation, tutorial targets `app.activityRailInstaller`, `installer.updateAll`, `installer.toolOptions`
- Synonyms: "install nvm", "install Node", "install Python", "install Docker", "install Claude Code", "set up dev tools", "developer tools installer", "package manager"

## What it is

The **Install Helper Module** is a built-in Activity Rail destination that installs, updates, and uninstalls a curated catalog of Windows AI tool stacks — git, node, python, docker, AI coding CLIs, local AI platforms, and supporting utilities. It is grouped with the other built-in Module buttons near the top of the rail, with its own icon (`Package`).

The catalog **ships with the KKTerm release**: the JSON list of supported tools is compiled into the binary at build time. Updates to the catalog ride with each release. See `docs/ADR/0008-installer-helper-bundled-catalog.md` for the rationale and what it supersedes.

**Liveness rule.** Install Helper work must not block KKTerm's UI thread. Catalog load, detection, cache reads, latest-version lookup, SQLite preference updates, install/uninstall, managed web UI launch/status/stop, and NSSM service registration/removal run on background workers or stream progress over `installer://progress`.

## Module layout

The page has a header and categorized sections:

- **Header.** Title (`installer.title`), subtitle (`installer.subtitle`), a page-level last checked status (`installer.lastChecked`) that switches to `installer.checkingForUpdates` while the background sweep runs, and two buttons:
  - `installer.refresh` — re-runs detection for every catalog tool, then queries the latest available version for every catalog tool whose provider can report one and caches the result.
  - `installer.updateAll` — installs every available update sequentially. Tutorial target: `installer.updateAll`.
- **Essentials** (`installer.section.essentials`) — recommended Windows setup patterns for Node, Python, and Git. The visible Node entry (`Node (nvm-windows)`) installs nvm-windows, then installs and activates the latest Node LTS release with nvm-windows. The visible Python entry (`Python (uv)`) installs uv, then installs the latest stable Python 3.13 patch release and pins Python 3.13 globally. Power users who want direct Node.js or CPython installer packages can install those outside Install Helper. winget itself lives in the **Package Managers** section, but all winget-backed catalog entries still declare `winget` as a prerequisite.
- **AI Agents** (`installer.section.aiAgents`) — coding-agent CLIs and desktop agent apps such as Claude Code CLI, Codex CLI, Antigravity CLI, OpenCode CLI, OpenClaw, Codex Desktop, Claude Desktop, Hermes AI Agent, and Hermes Desktop. Installed Hermes AI Agent and OpenClaw entries expose terminal launch and Add to Workspace shortcuts with tool-specific setup hints.
- **AI Platforms** (`installer.section.aiPlatforms`) — local or self-hosted AI platforms such as Ollama, n8n, Open WebUI, Flowise, Langflow, ComfyUI, and LM Studio. Managed entries live under `%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\` rather than ordinary global command-line tools.
- **Development** (`installer.section.development`) — editors, containers, API tools, base WSL, WSL distribution shortcuts, Rustup, and Bun. Bun installs through WinGet (`Oven-sh.Bun`), Chocolatey (`bun`), or, via the provider option, a direct GitHub-release zip (`oven-sh/bun`, `bun-windows-x64.zip`, extracted with `bun.exe` under the `bun-windows-x64` subdirectory); it is the all-in-one JavaScript runtime and toolkit with a bundler, test runner, and Node.js-compatible package manager.
- **Design** (`installer.section.design`) — diagramming, whiteboard, and graphics tools such as Excalidraw, OpenFlowKit, Draw.IO, Krita, Inkscape, and Blender.
- **Windows Power User** (`installer.section.windowsPowerUser`) — Windows-focused productivity and administration tools such as Microsoft PowerToys, PowerShell 7, psmux, Sysinternals Suite, Everything Search, and Ditto Clipboard Manager. psmux installs through WinGet (`marlocarlo.psmux`) and provides native Windows terminal multiplexing with tmux-style sessions, windows, and split panes. The PowerShell 7 entry installs cross-platform PowerShell 7 (`pwsh.exe`) through WinGet (`Microsoft.PowerShell`), Chocolatey (`powershell-core`), or, via the provider option, a direct `.msi` download; pwsh 7 keeps its own `$PROFILE` separate from Windows PowerShell 5.1, so oh-my-posh and module setup placed in the pwsh profile load only under pwsh. After installing it, pick `settings.powerShell7` as the default shell or from the Quick Connect local-shell menu to open pwsh terminals. The installed Sysinternals Suite dialog exposes a searchable `installer.dialog.quickLaunch` mini launcher covering the full suite: GUI tools (Process Explorer, Process Monitor, Autoruns, TCPView, ZoomIt, RAMMap, VMMap, WinObj, …) launch elevated by default (UAC), while command-line PsTools are list-only with an `installer.quickLaunch.openTerminal` button that opens an elevated PowerShell — all resolved against the refreshed PATH (WinGet/Store execution aliases or Chocolatey shims).
- **Remote Access** (`installer.section.remoteAccess`) — private-network and remote-desktop access tools such as Tailscale and RustDesk.
- **Package Managers** (`installer.section.packageManagers`) — the Windows package managers KKTerm drives, ordered above Utilities: winget and Chocolatey. The winget entry installs Microsoft App Installer / Windows Package Manager from `microsoft/winget-cli` for Windows versions that do not already ship `winget.exe`; before staging the App Installer MSIX bundle, KKTerm also installs the Desktop VCLibs and Microsoft.UI.Xaml 2.8 framework packages that Microsoft documents as required for Store-less/LTSC-style WinGet installs. Chocolatey bootstraps through WinGet (`Chocolatey.Chocolatey`), then uses the installed `choco` CLI for self-updates and as an alternate provider on curated rows that also have verified Chocolatey package ids.
- **Utilities** (`installer.section.utilities`) — Notepad++, NSSM, VcXsrv, Oh My Posh, ripgrep, jq, fzf, Coreutils for Windows, 7-Zip, ShareX, FFmpeg, scrcpy, and BentoPDF. Winget-installed command-line utilities add the winget links directory (`%LOCALAPPDATA%\Microsoft\WinGet\Links`) to the user PATH so new shells can find their commands.

Each tool surface is a **tile** in the section grid. The alternate list view keeps its Name, Installed, Latest, Status, and Action columns within a bounded readable width on wide displays instead of stretching the Name column across the full window. Installed tools show an `installer.section.installed` badge. Tile version metadata is shown on separate `installer.tile.latest` and `installer.tile.installed` rows, rather than as provider kind text. Direct `download-installer` entries render `installer.status.web` as the latest value and link it to the download URL because those providers have no structured latest-version source. Windows feature, WSL distro, and multi-step bundle entries do not render `installer.tile.latest`. Runtime bundles add a third row: `installer.tile.node` for the Node runtime version, and `installer.tile.python` for the Python runtime version. Their `installer.tile.installed` row remains the manager version (nvm-windows or uv), so update checks compare like with like. While the startup or manual refresh sweep is running, version values show `installer.status.retrieving` and install/update actions are disabled. When `latestVersionSeen` has higher version precedence than `installedVersion`, the tool stays in its normal category section, the latest version value is highlighted, and the tile action uses `installer.actions.update`; equivalent trailing-zero build segments such as `26.01` and `26.01.00.0` are treated as equal. If a latest-version lookup for a supported provider errors, the tile's `installer.tile.latest` row shows that error instead of the generic unknown-version fallback. The page-level header status carries the most recent check time. One-step bundles usually inherit latest/detected version metadata from their child recipe; Node and Python bundles additionally show the detected runtime version once nvm-windows/uv and the managed runtime are present. The tile action button switches between `installer.actions.install` and `installer.actions.update` based on detected install/update state, and opens the app-owned popup dialog — `InstallerToolDialog` — that owns the detail surface (the previous inline expansion has been removed). Tutorial target on the options form inside the dialog: `installer.toolOptions`.

The dialog has three rendering modes:

- **Installed info** — version, install location (when known), detected management-provider summary, detected winget install mode (`installer.options.scopeUser` or `installer.options.scopeMachine`, when known), and, for providers with a structured latest-version source, latest version + last checked timestamp, pin-version switch, and an Update banner when `latestVersionSeen` compares newer than `installedVersion`. Direct download entries show `installer.status.web` as a link in the latest-version row instead. Installed tools that expose a local browser interface may also show `installer.dialog.webUi`, `installer.dialog.runtimeStatus`, and `installer.actions.run` / `installer.actions.stop` plus `installer.actions.openWebUi`; n8n, Flowise, Open WebUI, Langflow, Excalidraw, BentoPDF, OpenFlowKit, and Ollama use app-local commands and localhost URLs. BentoPDF prefers `localhost:3022` and OpenFlowKit prefers `localhost:3023`, but their app-local servers record and report a different free port when the preferred port is busy, so Open web UI follows the actual URL. Run commands for managed web UI apps start hidden from their `%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\` directory so app-local config, secrets, and generated files stay with the managed app without opening a terminal window. Managed apps that expose upstream data-directory settings point them at each tool's `data` directory, including n8n's `N8N_USER_FOLDER`, Flowise's database/secret/log/blob paths, Open WebUI's `DATA_DIR`, Langflow's `LANGFLOW_CONFIG_DIR`, and Ollama's model directory; browser-only state such as Excalidraw imported libraries remains in the browser's site storage for the localhost origin. After install/update completes, managed web UI apps are started automatically in normal run mode. Service registration is explicit: service-capable managed apps may show `installer.dialog.windowsService`, `installer.dialog.serviceStartup`, and `installer.actions.registerService`; this stops a recorded normal localhost run when one exists, then installs NSSM on demand if needed, uses NSSM to register and start the service, sets startup to automatic, writes service stdout/stderr logs under `%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\logs\`, and may show UAC. Stop on a running service also asks NSSM to stop it and then cleans up the recorded listening process. Tool suites that ship many standalone utilities show a searchable `installer.dialog.quickLaunch` mini launcher in the dialog body — a `installer.quickLaunch.search` filter over a scrollable list where each row carries the tool name, executable, and a one-line description. Sysinternals Suite populates this with its full set of GUI and command-line tools. **GUI tools** (Process Explorer, Process Monitor, Autoruns, TCPView, ZoomIt, RAMMap, VMMap, WinObj, and more) get a `installer.quickLaunch.launch` button that starts them **elevated by default** — most Sysinternals GUI tools embed a `requireAdministrator` manifest, so a plain launch fails with `os error 740`; KKTerm resolves the executable against the refreshed PATH and runs it through `Start-Process -Verb RunAs`, which shows the normal UAC consent prompt. **Command-line tools** (PsExec, handle, sigcheck, strings, sdelete, and the rest of the PsTools) are list-only with a `installer.quickLaunch.cli` badge, since launching them with no arguments is useless; a single `installer.quickLaunch.openTerminal` button opens a standard **elevated Windows PowerShell** window (`Start-Process powershell -Verb RunAs`) so the user can run them with their own arguments at admin integrity. The launch set is a closed per-tool allow-list in the backend (`installer_list_quick_launch`); `installer_launch_quick_command` validates the requested executable against it and refuses CLI entries, and `installer_open_quick_launch_terminal` opens the elevated prompt. Footer: `Uninstall` (danger) · `Run`/`Stop` and `Open web UI` (when available) · `Register as service` or `Remove service` (when available) · `Update` (when available) · `Close`.
- **Not-installed info** — official website (`recipe.homepage`), release notes (`recipe.releaseNotesUrl`, with a provider-derived fallback for github/npm/winget when absent), latest version for providers with a structured latest-version source (with an inline `installer.dialog.checkNow` action when no latest has been queried yet), `installer.status.web` latest-version links for direct download entries, provider summary, selected winget install mode, prerequisites list with installed/missing badges, and the options form. Footer: `Install` (primary) · `Cancel`.
- **Stepper** — opened when the user presses Install/Update, or when reopening the dialog while an install/uninstall is in flight or has just terminated. The body renders the n8n-style step list with status dots (`pending`/`running`/`done`/`failed`), per-step duration / active-step ratio, and a click-to-expand per-step log panel. Winget progress is best-effort: KKTerm shows determinate progress when winget emits carriage-return percentage or downloaded/total frames, and falls back to elapsed-time heartbeats when the underlying installer is silent. Terminal states swap the dialog title (`installer.dialog.installedTitle` / `installer.dialog.failedTitle` / `installer.dialog.cancelledTitle`). Failed installs leave the stepper dialog open on the failed package, including prerequisite packages and Update all entries, so the user can read or copy the streamed stdout/stderr logs instead of being bounced back to an info-style dialog. The failure message also includes the most useful recent child-process output line when one is available, so package-manager causes are visible without expanding the full log.

## Tool detection

Detection renders in two phases on Module entry. First, KKTerm loads the local Windows Registry detection cache from `HKCU\Software\Ryan Tsai\KKTerm\InstallerDetectionCache` on a worker and paints any remembered states immediately. Then the backend starts a bounded streaming revalidation sweep in the background without blocking the Module. Each completed tool emits a `detectResult` event over `installer://progress`, updates the tile, and writes the fresh state back to the registry cache. After detection finishes, KKTerm runs an interval-gated latest-version check: it queries latest versions for every catalog tool whose provider can report one only when the configured interval has elapsed since the last successful check (see Update flow). `installer.refresh` re-runs the same detection and forces a latest-version check on demand regardless of the interval.

Per-provider detection methods:

- **winget** — detection reads a local Add/Remove Programs registry snapshot once per fresh detection sweep and matches catalog aliases (`registryKeys`, `displayNames`, `displayNamePrefixes`) so existing installs can be recognized even when winget would report an `ARP\...` identifier. The snapshot is cached in memory until the next explicit detection call. Fresh results are persisted to the Install Helper registry cache. Latest-version checks prefer the structured Microsoft `winget-pkgs` manifest repository so they do not depend on localized `winget show` labels; KKTerm only treats version-shaped manifest directories as latest-version candidates and ignores sibling channel folders such as prerelease or alternate-edition branches. When that lookup is unavailable, KKTerm falls back to `winget show`, accepts winget source agreements for first-run Windows machines, and uses `CREATE_NO_WINDOW` so no `cmd.exe` window flashes on Module entry.
- **Chocolatey** — curated winget rows may expose Chocolatey in `installer.options.provider` when the package id was verified in the Chocolatey community feed. Selecting Chocolatey requires the `chocolatey` recipe; if it is missing, KKTerm opens that recipe's install dialog first. Detection uses local `choco list --local-only --exact --limit-output`. Once a row is detected as installed through Chocolatey, KKTerm uses `choco search --exact --limit-output` for latest-version checks, `choco upgrade` for updates, and `choco uninstall` for uninstall. Every Chocolatey **install, update, and uninstall runs elevated** — Chocolatey writes to `C:\ProgramData\chocolatey` and always requires Administrator, so each op raises exactly one UAC prompt via `Start-Process -Verb RunAs` (KKTerm itself stays non-elevated) and applies machine-wide for all users. Because an elevated child cannot share pipes across the UAC boundary, the elevated command redirects its output to a temp log that KKTerm tails into the stepper. The dialog states the requirement up front via `installer.dialog.adminRequiredChocolatey`, and the update and uninstall confirmations carry an `installer.confirm.adminChocolateyFooter` notice. Declining the UAC prompt fails the op cleanly (Windows `ERROR_CANCELLED` / 1223).
- **npm** — `npm ls -g --json --depth=0`. Looks up the package in the top-level `dependencies`. Managed npm apps such as n8n, Flowise, and OpenClaw use an app-local marker instead.
- **uv pip** — app-local Python virtual environments created with uv. Open WebUI, Langflow, and Hermes AI Agent use this provider and are detected by their app-local marker after KKTerm installs them.
- **download-installer** — downloads a vendor-declared installer URL to a temp file and runs the downloaded installer. `.appx`, `.appxbundle`, `.msix`, and `.msixbundle` downloads are installed with PowerShell `Add-AppxPackage`; the winget/App Installer recipe also requests `Microsoft.DesktopAppInstaller_8wekyb3d8bbwe` family registration before and after installing the downloaded bundle so first-login Windows 11 images can register `winget.exe`. Other downloads launch with the normal Windows installer UI. Codex Desktop uses this path because its canonical install surface is a vendor download page rather than a package-manager recipe. Some winget desktop-app recipes also expose this as an optional provider when a canonical vendor installer URL exists.
- **GitHub-release download provider** — selected winget recipes can offer a direct GitHub release fallback when the vendor publishes portable archives instead of a conventional installer. Zip fallbacks extract under `%LOCALAPPDATA%\KKTerm\installer\bin\<tool_id>\`; catalog metadata can point Add to PATH at a nested executable directory inside the archive. Once KKTerm detects the marker for one of these managed fallback installs, latest-version checks and updates keep using the GitHub-release provider instead of switching back to winget.
- **github-release / managed app** — a marker file under `%LOCALAPPDATA%\KKTerm\installer\bin\<tool_id>\` or `%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\` written at install time. Only installs we performed count as "managed".
- **windows-feature** — `dism /online /get-featureinfo /featurename:<X>`. Parses the `State :` line.
- **wsl-distro** — `wsl --list --quiet`. Matches the distro name declared in the catalog.
- **bundle** — installed iff every step's detection is installed; otherwise the row shows `installer.status.partial` with an `installed/total` count. One-step bundles inherit the child version. Installing or updating a bundle skips already-current installed steps, but re-runs an installed step when that step's latest-version check reports a newer manager version.

Runtime command probes such as `node --version`, `uv python find 3.13`, global npm package detection, managed app version probes, and managed web UI launchers use or retry with the refreshed persisted Windows PATH when the current KKTerm process cannot resolve the command. This lets a fresh nvm-windows, uv, or npm install re-detect and launch related npm-backed tools correctly without requiring a KKTerm restart.

## Install flow

1. The user clicks `installer.actions.install` on a row.
2. The frontend resolves the install plan: transitive `needs` are walked, and prerequisites already detected as installed are skipped. If a winget-backed recipe is installed with `installer.options.provider` set to direct download, the winget prerequisite is skipped for that install because the selected provider does not call `winget.exe`.
3. If the plan has unresolved prerequisites OR a non-zero UAC-prompt estimate, a confirm dialog (`installer.confirm.installTitle`) lists the prerequisites and a footer (`installer.confirm.uacFooter`) names the estimated prompt count.
4. On confirm, the backend dispatches the install in a worker thread. The dialog flips to stepper mode for the package currently being installed, including prerequisites. Progress streams to the frontend on the `installer://progress` Tauri event channel as a discriminated union: `plan` (declared step list, emitted once before any work begins), `stepStarted` / `stepFinished` (per-step lifecycle), `stdout` / `stderr` / `progress` (each carrying an optional `stepId` so the UI can route lines and ratios to the active step row), `step` (legacy free-form label retained for unmigrated providers), `completed`, `failed`, `cancelled`. A `failed` or `cancelled` event stops the queue and keeps the dialog in stepper mode on that package with its streamed logs visible.
   Runtime bundle follow-up commands and npm package installs refresh PATH plus relevant persisted Windows user and machine environment variables before spawning. This lets freshly installed managers such as nvm-windows and uv run their immediate `nvm install lts`, `uv python install ...`, or npm-based AI-agent install steps without restarting KKTerm or Windows. The refreshed child environment also includes existing Git for Windows command directories so later child installers can resolve `git` in the same KKTerm session. The uv recipe is a WinGet portable package; before updating it, KKTerm checks for running `uv.exe` processes and asks the user to close uv-based tools when they would block replacing the portable executable. The base WSL recipe runs `wsl --install --no-distribution`, so Docker can depend on WSL without silently installing Ubuntu or launching Linux first-run setup. WSL distribution shortcuts are separate recipes that run `wsl --install --distribution <name> --no-launch`; if base WSL was installed as their prerequisite in the same queue, the queue stops at the reboot boundary and the distro install is retried after restart.
5. After a `completed` event, the row re-runs detection automatically and moves to the Installed section.

## Update flow

1. KKTerm evaluates the update sweep each time the user switches to the Install Helper Module from another Module. The automatic sweep runs only when the configured check interval (Settings → Install Helper → `settings.installerCheckInterval`; one of hour / day / week / month, default daily) has elapsed since the last successful check; otherwise the persisted check state is reused without a network fetch. The user can also force a check by clicking `installer.refresh` (header) / `installer.dialog.checkNow` for one tool inside the detail dialog.
2. The backend launches a **streaming** worker and returns after the worker starts. It emits `checkStarted` with the id list, then per-tool `checkResult` events as each lookup lands (latest-version queries run in parallel — `winget show`, npm registry, PyPI, GitHub releases API — bounded to a small pool to hide slow legs behind fast ones), and finally `checkFinished`. The frontend only sends recipes whose providers can report a structured latest version. It writes each successful result into `toolState` as it arrives so rows light up incrementally instead of after one large blocking await. Per-tool lookup errors stay in frontend session state and render in the `installer.tile.latest` / `installer.dialog.latestVersion` row; they do not overwrite the persisted latest-version cache. Results are persisted in the `installer_tool_state` SQLite table.
3. Rows whose `latestVersionSeen` compares newer than `installedVersion` stay in their normal category section, highlight the latest version value, and show their per-row `installer.actions.update` button.
4. Clicking `installer.updateAll` lists every pending update in a dialog (`installer.confirm.updateAllTitle`), warns about the likely UAC prompt count, and on confirm runs the queue sequentially. Pinned tools (see Pin version below) are skipped.

The automatic sweep is triggered only by switching to the Install Helper Module, and at most once per configured interval; there is no background timer polling while the Module is inactive. The last-check timestamp is persisted per tool in `installer_tool_state` (`last_check_at`) so the interval is honored across app launches.

## Uninstall flow

1. The user clicks `installer.actions.uninstall` on an installed row.
2. The frontend runs a reverse-dependency check: every catalog recipe whose `needs` includes this row's id AND whose detection says installed is collected.
3. If dependents exist, the confirm dialog (`installer.confirm.uninstallTitle`) lists them with the warning `installer.confirm.uninstallDependentsBody` and the footer `installer.confirm.uninstallDependentsFooter`. Otherwise the dialog shows the simple body `installer.confirm.uninstallSimpleBody`.
4. On confirm, the backend dispatches per-provider uninstall: `winget uninstall`, `npm uninstall -g`, `uv pip uninstall`, `Remove-Item -Recurse` on the github-release or managed-app install directory, `dism /online /disable-feature`, or `wsl --unregister <distro>` for WSL distro shortcuts. Download-installer desktop apps that do not expose a safe structured uninstall path tell the user to uninstall from Windows Settings.

There is intentionally no "Uninstall all" action.

## Cancellation

Per row, an in-flight install/uninstall surfaces an `installer.actions.cancel` button. Cancel raises an `AtomicBool` flag the worker thread polls; on the next check it kills the child process and emits `cancelled`. Partial installs are **not rolled back** — the underlying installer's transactionality is what it is.

For "Update all", cancel stops the **queue** (the next-up tools), not the in-flight install.

## Options

Each recipe declares a subset of these options (the set is closed):

- **`installer.options.provider`** — winget (default) vs direct download or Chocolatey provider. Shown only for recipes whose catalog entry declares a canonical `downloadProvider` or verified `chocolateyProvider`. Choosing download hides winget-only options, removes the winget prerequisite for that install, and shows download-provider options such as GitHub-release location/Add to PATH when applicable. Choosing Chocolatey uses the `chocolatey` recipe as the package-manager prerequisite and then manages that row with `choco`.
- **`installer.options.scope`** — `user` (default, `installer.options.scopeUser`) vs `machine` (`installer.options.scopeMachine`). Winget recipes only. KKTerm passes `--scope user` by default for scoped winget recipes, including prerequisite installs inside bundles. User Mode means KKTerm asks winget for a current-user install; it is no-UAC only when the package's own installer supports a non-elevating user install. Known self-elevating user-mode installers show `installer.options.scopeSelfElevatingHint`; for those recipes, and for known machine-only winget recipes such as Docker Desktop, KKTerm launches the winget operation through the elevated helper so the user gets the expected UAC prompt even when KKTerm itself is not running as administrator. System Mode(UAC) always triggers UAC.
- **`installer.options.version`** — specific version string, or empty = `installer.options.versionLatest`.
- **`installer.options.location`** — install location override. Winget: `--location`. github-release: extraction directory.
- **`installer.options.addToPath`** — github-release recipes only. Appends the cataloged executable directory (or the install directory when no nested path is declared) to user PATH via PowerShell after extraction.
- **`installer.options.pinVersion`** — row-level switch. When enabled, this tool is **excluded** from `installer.updateAll`. Pinning does not prevent a per-row update click.

## WSL distro manager

Once base WSL is installed, its installed-tool dialog exposes an `installer.wsl.manageDistros` action that opens the WSL distro manager. Unlike the static `wslDistro` catalog shortcuts, the manager reflects live host state derived from `wsl.exe` rather than the catalog, so it sees every distribution — including ones installed outside KKTerm:

- **List** — `wsl --list --verbose` (decoded from UTF-16LE) yields each distro's name, default marker, WSL version, and running state. The parser keys off the `*` default marker and the trailing version digit instead of the localized column headers.
- **Set default** — `wsl --set-default <distro>` retargets bare `wsl`/`wsl --install`.
- **Unregister** — `wsl --unregister <distro>` permanently deletes the distro and all of its files; the manager requires a danger-tone confirmation first.
- **Install** — the install picker is populated from `wsl --list --online` (installed distros filtered out) and installs the selected one with `wsl --install --distribution <distro> --no-launch`.

Distro names passed to the state-changing commands are validated to be plain identifiers (alphanumerics plus `-`, `_`, `.`, no leading dash) so a name can never smuggle extra `wsl` arguments. The backend commands (`installer_wsl_list_distros`, `installer_wsl_list_online`, `installer_wsl_set_default`, `installer_wsl_unregister`, `installer_wsl_install`) run the blocking `wsl.exe` calls off the UI thread.

## WSL reboot gating

When the user installs base WSL in the current app session, the Install Helper sets a session-only flag `wslJustEnabled`. While that flag is set, any recipe that transitively `needs` WSL (Docker Desktop and WSL distro shortcuts in the v1 catalog) shows the hint `installer.wslReboot` and its install button is disabled. The flag resets only when KKTerm is restarted — which is what happens during a real Windows reboot.

## Persistence

- **SQLite** — table `installer_tool_state(tool_id PK, pinned, latest_version_seen, last_check_at)` (added in schema migration 17). Pinning and the cached latest available version survive restarts.
- **Windows Registry** — per-tool detection cache under `HKCU\Software\Ryan Tsai\KKTerm\InstallerDetectionCache`. This cache is local Windows state, not portable settings data.
- **Debug log** — debug builds write Install Helper catalog, detection, latest-version, install, uninstall, cache, progress-event, and child-process output records to `installer.helper.debug.log` beside `kkterm.log`. Release builds write the same JSONL log only when `settings.advancedDebugging` is enabled. The same setting also enables `kkterm-heartbeat.debug.log` in release builds for frontend/native liveness timing while troubleshooting Install Helper stalls.
- **In-memory only** — the active detection sweep, the in-flight install queue, the WSL reboot flag, and the per-tool log buffer.
- **On-disk install dirs** — `%LOCALAPPDATA%\KKTerm\installer\bin\<tool_id>\` holds installed `githubRelease`-provider tools, including winget recipes installed through a GitHub-release `downloadProvider`, and their `.kkterm-installer.json` marker file. `%LOCALAPPDATA%\KKTerm\installer\apps\<tool_id>\` holds managed server apps such as n8n and Ollama, plus their app-local marker and data folders. These are installed-tool state, not catalog cache.

## Catalog source

The catalog `installer/catalog.v1.json` is **embedded into the KKTerm binary at compile time** via `include_str!`. Updates to the catalog ship with the next KKTerm release — there is no network fetch, no on-disk cache, and no signature verification. The trust anchor is the app binary itself (eventually backed by Windows code-signing of the KKTerm installer).

Adding, editing, or removing a tool is a normal commit to `installer/catalog.v1.json` followed by a release. The `shipped_catalog_parses_and_validates` test embeds the same JSON via `include_str!` and runs `Catalog::validate()`, so malformed edits fail `cargo test` before they can ship.

See [`docs/ADR/0008-installer-helper-bundled-catalog.md`](../ADR/0008-installer-helper-bundled-catalog.md) for the design rationale and what it supersedes from [ADR 0007](../ADR/0007-installer-helper-remote-catalog.md).

## Tutorial

The Module is tutorial-capable in v1 via two targets:

- `app.activityRailInstaller` — the rail button (navigation: workspace).
- `installer.updateAll` — the header button (navigation: installer).
- `installer.toolOptions` — the per-row options form (navigation: installer).

## Limits

- AI Assistant integration is deferred. The AI does **not** have a Tauri command to trigger installs in v1.
- "Latest version" detection for multi-step `bundle`, `windowsFeature`, and `wslDistro` recipes returns null — the rows do not surface update suggestions. One-step bundles inherit latest-version checks from their child recipe.
- The UAC prompt count shown in confirm dialogs is a best-effort estimate; some installers (including Git for Windows, nvm-windows, Docker Desktop, and system MSIs) self-escalate even when `--scope user` is passed.
