Skip to content

feat: self-update banner and restart - #42

Merged
stanlyzoolo merged 1 commit into
mainfrom
feat/self-update-restart
Jul 25, 2026
Merged

feat: self-update banner and restart#42
stanlyzoolo merged 1 commit into
mainfrom
feat/self-update-restart

Conversation

@stanlyzoolo

Copy link
Copy Markdown
Owner

keeptui watches its own releases and offers to update from inside the TUI. Plan: docs/plans/completed/20260725-self-update-restart.md.

What it does

  • Self-check at startup — one release-only request to stanlyzoolo/keeptui, cached 24h. Works out of the box for an untracked keeptui, which is the main case.
  • Non-modal bannerkeeptui v0.5.0 available — [U] update [X] dismiss replaces the hints. [X] collapses it into a compact cell next to the API gauge, so the update stays reachable all session. Dismiss is session-only.
  • Update through the existing pipelineupdater.DetectmodeConfirmUpdate → streaming log. The self log owns panel [3] regardless of the selection (keeptui may not be tracked).
  • Restart[U] restart re-execs the new binary via syscall.Exec (unix). Windows degrades to a hint.

Off entirely on working-copy builds: selfCheckEnabled rejects dev, Go pseudo-versions and +dirty, so go build . neither fetches nor renders anything.

Design notes worth reviewing

ReleaseMissing is the part with the widest blast radius. The release tuple (Latest/Body/HtmlUrl/PublishedAt) belongs to the shared repo card, so a release-only pass must not wipe it on a 404 — one drafted release would strip a tracked keeptui's latest: line, date, , changelog body and release URL, with CheckedAt deliberately not advanced so nothing could recover. The 404 is recorded in a new CacheEntry.ReleaseMissing flag instead, maintained by all three release writers (SelfLatest, getRepoData, getChangelog) through one applyReleaseOutcome helper. getChangelog's write is load-bearing: with a fresh CheckedAt and an empty Body it is the only pass that reaches /releases/latest.

Behavior changes for non-keeptui tools (three, all in the update pipeline):

  1. [u] while an update runs now reports another update is running instead of doing nothing.
  2. A detect result landing while an editor/search is open is dropped instead of opening a confirm and stealing the keystroke.
  3. The confirm no longer reads selectedMeta() — the target comes from updateTarget, so a selection that moved during detection cannot retarget the plan.

[u] on a tracked keeptui row is now a self-update, same as [U]. The plan originally kept them separate; that left a banner advertising an update the card already showed as installed, with [U] restart reachable only by running the whole update again.

Out of the plan's stated scope: the internal/version cache changes above, a testability split of runTUI into newRootModel/restartIfRequested (mutation showed both wiring lines could be deleted with the suite still green), a CI cross-compile step for windows/darwin, and a test-fixture consolidation in internal/version that also fixed a pre-existing leak — one httptest server was not resetting the token, so it sent a real GITHUB_TOKEN to a local server.

internal/updater, internal/launcher, internal/proc and the meta.yaml schema are untouched.

Testing

go build, go vet, go test -race ./... (10/10 packages), golangci-lint run (0 issues), cross build+vet for GOOS=windows/GOOS=linux.

Not covered by tests, needs a manual pass before release:

  • The restart itself — syscall.Exec is never called by any test. Specifically Linux with go install, where /proc/self/exe is symlink-resolved and the argv0/LookPath ordering is what stops a "restart → same banner" loop.
  • restart_windows.go has never executed anywhere; it is only cross-compiled.

Known caveat

updater.Detect resolves the target with LookPath(name) while the restart resolves argv0-first. Run ./keeptui with a different keeptui on PATH and the update hits the PATH one while the restart returns the local binary. Documented in README/CLAUDE.md rather than fixed — the fix belongs in internal/updater, well outside this branch.

keeptui watches its own releases and offers to update from inside the TUI.

- `version.SelfLatest` — a release-only pass to `stanlyzoolo/keeptui` with its
  own `ReleaseCheckedAt` freshness stamp, so it never marks a tracked keeptui's
  repo card fresh-but-blank. A 404 is recorded in the new `ReleaseMissing` flag
  instead of wiping the release tuple the card shares; all three release writers
  maintain the flag.
- A non-modal banner in the hints bar: `keeptui v0.5.0 available — [U] update
  [X] dismiss`. `[X]` collapses it into a compact cell next to the API gauge.
  Session-only, never written to disk.
- The update runs through the existing detect/confirm/stream pipeline. The self
  log owns panel `[3]` regardless of the selection — keeptui may not be tracked —
  via the single `showsUpdateLog()` predicate at all six former call sites.
- After a successful update `[U] restart` re-execs the new binary (`syscall.Exec`
  on unix, an honest hint on Windows). Path resolution follows argv0 semantics
  and prefers `LookPath` for a bare name, so Linux's symlink-resolved
  `/proc/self/exe` cannot loop the restart into the pre-upgrade binary.

The feature is off entirely on working-copy builds: `selfCheckEnabled` rejects
`dev`, Go pseudo-versions and `+dirty`, so `go build .` neither fetches nor
shows anything.

Also: `[u]` on a tracked keeptui row is now the same self-update as `[U]`, both
update keys report when they are blocked instead of doing nothing, a late detect
result no longer opens a confirm under an open editor, and CI cross-compiles for
windows/darwin so the build-tagged files are checked.
@stanlyzoolo
stanlyzoolo merged commit bb3e06b into main Jul 25, 2026
3 checks passed
@stanlyzoolo
stanlyzoolo deleted the feat/self-update-restart branch July 25, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant