Skip to content

Keep Radar Desktop on the same port so preferences survive restarts - #1907

Merged
nadaverell merged 5 commits into
mainfrom
fix/desktop-stable-port
Sep 26, 2026
Merged

nadaverell merged 5 commits into
mainfrom
fix/desktop-stable-port

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Radar Desktop forgot every browser-stored preference each time it restarted. That covers theme, favorites, the pinned nav rail, recent resources, log viewer settings, AI panel width/model/effort, timeline pinned lanes, and dismissed prompts.

The cause: Desktop's webview loads http://localhost:<port>, and browser storage is kept per origin, which includes the port. Desktop picked a random port on every launch, so every launch started with empty storage.

What changed

  • Desktop reuses its last port. After the server starts, Desktop records the port it bound in ~/.radar/desktop-port. On the next launch it asks for that port again, so the webview keeps the same origin and the same storage.

  • Falling back is safe. The server has a new PortFallback option (server.Config, wired through app.AppConfig). When the requested port is taken, it binds an OS-assigned port instead of failing. The fallback happens inside the bind, so there's no window where another process takes the port between a check and the listen. Desktop records whichever port it actually got.

  • The port is recorded only once the window has loaded, on the first DOM-ready, so a launch that fails before its window appears changes nothing.

  • The port moves only when it's clearly lost. When a launch falls back, it checks the remembered port once the window is up:

    On the remembered port Result
    Radar answers /api/connection?contexts=0 (a second Desktop window) keep it
    no answer in time (maybe a busy Radar) keep it
    nothing answers, and it binds again (it was only briefly busy) keep it
    nothing answers, and it still can't be bound adopt the new port
    something other than Radar answers adopt the new port

    The unbindable row covers Windows' reserved port ranges: Hyper-V, WSL2 and Docker Desktop reserve blocks, which a reboot can move. Dial errors are distinguished only by timeout, because refusal error codes differ between Windows and Unix.

  • The window loads 127.0.0.1:<port>, the address actually bound, instead of localhost:<port>. localhost can resolve to [::1], where another service may hold the same port. This changes Desktop's origin once, which costs nothing, since storage was already reset on every launch.

  • A port configured in config.json is unchanged: used as-is, with no fallback and nothing recorded.

  • The CLI is unaffected. It keeps 9280 by default and never falls back.

When the remembered port is taken (rare), that one session starts with empty storage, which is today's behavior every time. The next launch goes back to the recorded port.

Platform notes

  • Storage: webview storage persists on all three platforms and is keyed by origin.
    • macOS: WKWebView's default store under the bundle id.
    • Windows: WebView2 under %AppData%\<exe name>. Renaming the exe would reset storage; the self-updater keeps the name.
    • Linux: WebKitGTK's default context under radar.
  • Rebinding after a quick restart: Go sets SO_REUSEADDR on Linux and macOS. Windows reuses recently closed ports by default. If a quick restart still misses the port, that one session falls back, and the next launch reclaims it.
  • Only macOS was exercised at runtime. Windows and Linux were reviewed from source; the code is plain Go with nothing platform-specific.

Testing

  • Unit:
    • rememberDesktopPort: every row of the table above.
    • loopbackPortBindable: held and free ports.
    • portOwner: recognizes Radar; treats an unrelated HTTP server and a non-HTTP service as foreign; reports a free port as silent and a server too slow to answer as unknown.
    • listenPreferringPort: a taken port fails without fallback and binds a different port with it; a free port is used as preferred.
    • desktop-port round-trip, garbage values ignored, no temp files left.
  • go test ./internal/server ./internal/app ./cmd/desktop all pass.
  • End to end, two concurrent windows: A bound 51528 and recorded it; its webview requests went to 127.0.0.1:51528 (none to localhost). B, started while A ran, fell back to 51555 and left the record at 51528. After both quit, C bound 51528 again.
  • End to end: the Desktop build, three launches with a throwaway HOME:
    1. first launch bound 63239 and recorded it
    2. second launch bound 63239 again
    3. with 63239 held by another process, the third launch logged Port 63239 is unavailable …; using an OS-assigned port, bound 63266 and recorded that

The webview's origin includes the server's port and browser storage is
per origin, so a random port each launch emptied every browser-stored
preference on restart. Desktop now records its port in
~/.radar/desktop-port and asks for it next time; the server falls back
to an OS-assigned port inside the bind when it's taken. A port set in
config.json is still used as-is.
@nadaverell
nadaverell requested a review from hisco as a code owner September 26, 2026 12:39
@cursor

cursor Bot commented Sep 26, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Preserve Desktop preferences with a stable server port

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Reuses Desktop’s previous port to preserve browser-stored preferences across restarts.
• Falls back atomically when a remembered port is unavailable.
• Keeps configured Desktop ports and CLI port behavior unchanged.
Diagram

graph TD
  C["config.json"] -->|"configured port"| D["Desktop startup"] -->|"preferred port"| S["Server bind"] -->|"actual port"| W["Webview origin"] -->|"stable origin"| P[("Browser storage")]
  R["desktop-port file"] -->|"remembered port"| D
  S -->|"port unavailable"| O["OS port allocator"] -->|"fallback port"| W
  S -->|"record actual port"| R
Loading
High-Level Assessment

The current approach is appropriately scoped and preserves existing CLI and explicit-configuration semantics. A fixed Desktop port would create avoidable conflicts, while moving preferences away from origin-scoped browser storage would be substantially broader; persisting the actual port with bind-time fallback is the safer minimal fix.

Files changed (6) +188 / -3

Enhancement (1) +16 / -2
server.goAdd atomic fallback for unavailable preferred ports +16/-2

Add atomic fallback for unavailable preferred ports

• Adds a server option that retries an unsuccessful preferred-port bind with an OS-assigned port. Default behavior remains strict, preserving existing CLI and configured-port semantics.

internal/server/server.go

Bug fix (2) +89 / -1
main.goReuse and record the Desktop server port +13/-1

Reuse and record the Desktop server port

• Desktop startup now prefers the last recorded port when no port is explicitly configured. It enables fallback only for remembered ports and records the server’s actual bound port after startup.

cmd/desktop/main.go

port.goPersist the last successful Desktop port +76/-0

Persist the last successful Desktop port

• Adds validated loading and atomic replacement of '~/.radar/desktop-port'. Invalid values and filesystem failures are handled without preventing Desktop startup.

cmd/desktop/port.go

Tests (2) +81 / -0
port_test.goTest Desktop port persistence and validation +37/-0

Test Desktop port persistence and validation

• Covers initial reads, port replacement, invalid stored values, and cleanup of temporary files after atomic writes.

cmd/desktop/port_test.go

listen_port_test.goTest preferred-port binding and fallback behavior +44/-0

Test preferred-port binding and fallback behavior

• Verifies strict binding fails for an occupied port, fallback selects another port, and available preferred ports remain unchanged.

internal/server/listen_port_test.go

Other (1) +2 / -0
bootstrap.goPropagate port fallback through application configuration +2/-0

Propagate port fallback through application configuration

• Extends 'AppConfig' with 'PortFallback' and forwards it into the shared server configuration.

internal/app/bootstrap.go

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A second launch hides saved preferences ✓ Resolved 🐞 Bug ≡ Correctness
Description
main lets every Desktop process read the same remembered port, fall back when another instance
owns it, and unconditionally replace the shared file with its fallback port. Because there is no
single-instance guard, opening Desktop twice changes the origin used after both processes exit from
the original port to the second process's port, so later launches no longer see preferences stored
under the original origin.
Code

cmd/desktop/main.go[R264-265]

+	if desktopPortRemembered {
+		recordDesktopPort(desktopPortPath(), srv.ActualPort())
Evidence
Desktop reads and later rewrites one per-user port file, while the server deliberately selects a
fallback when the preferred port is already bound. No startup lock or single-instance configuration
prevents a second Desktop process from taking that path, and the new file documents that changing
ports changes the storage origin.

cmd/desktop/main.go[169-175]
cmd/desktop/main.go[252-266]
internal/server/server.go[1151-1157]
cmd/desktop/port.go[11-15]
cmd/desktop/port.go[43-66]
cmd/desktop/wails.json[1-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Overlapping Desktop processes share and overwrite one remembered-port file, allowing a fallback instance to replace the browser origin used by later launches.
## Fix Focus Areas
- cmd/desktop/main.go[169-175]
- cmd/desktop/main.go[264-266]
- cmd/desktop/port.go[43-66]
## Recommended Fix
Acquire a per-user single-instance lock before reading the remembered port or starting the server. If another Desktop instance owns the lock, activate that instance or exit without binding a fallback port or updating `desktop-port`; retain the lock until shutdown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. A helper comment repeats its behavior ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The comment above lastDesktopPort merely restates that the function returns the recorded port or
0, which is already evident from its name and implementation. Because it provides no rationale,
constraint, or non-obvious invariant, later changes must keep redundant prose synchronized with the
function.
Code

cmd/desktop/port.go[27]

+// lastDesktopPort returns the port recorded by the previous launch, or 0.
Evidence
Rule 3036542 disallows newly added comments that only restate obvious code behavior. The cited
comment directly summarizes the helper's name, return statements, and fallback value without
explaining why the implementation is structured this way.

Rule 3036542: Avoid explanatory comments that restate obvious code behavior
cmd/desktop/port.go[27-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The comment above `lastDesktopPort` only paraphrases the helper's directly visible behavior and adds no rationale or hidden constraint.
## Fix Focus Areas
- cmd/desktop/port.go[27-27]
## Recommended Fix
Remove the comment. If documentation is considered necessary, replace it only with information about a non-obvious design constraint rather than a summary of the return value.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Failed launches change the next origin ✓ Resolved 🐞 Bug ☼ Reliability
Description
main calls recordDesktopPort immediately after the HTTP listener becomes ready, before
wails.Run has established the window and while it can still return an error. When the preferred
port was occupied and this pre-UI launch selected a fallback, the dead session's port replaces the
persisted value, so the next successful launch opens on an origin where the user never had a usable
session.
Code

cmd/desktop/main.go[R264-266]

+	if desktopPortRemembered {
+		recordDesktopPort(desktopPortPath(), srv.ActualPort())
+	}
Evidence
The server is started and the selected port is persisted before execution reaches wails.Run. The
same function treats a later Wails error as fatal, but there is no rollback of the already-written
port, so a fallback selected by an unsuccessful UI launch remains the next preferred origin.

cmd/desktop/main.go[252-266]
cmd/desktop/main.go[287-334]
cmd/desktop/main.go[169-175]
internal/server/server.go[1151-1157]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The fallback port is persisted before Wails confirms that the Desktop UI started, allowing a failed launch to redirect future launches to an unused browser-storage origin.
## Fix Focus Areas
- cmd/desktop/main.go[261-266]
- cmd/desktop/main.go[287-332]
## Recommended Fix
Move remembered-port persistence into a Wails lifecycle callback that runs only after successful UI startup, such as a wrapper around `OnDomReady`. Preserve the existing callback behavior and do not update `desktop-port` if Wails fails before that callback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread cmd/desktop/port.go Outdated
Comment thread cmd/desktop/main.go Outdated
Comment thread cmd/desktop/main.go Outdated
A launch that failed before its window appeared, or a second Desktop
window that fell back because the first holds the port, replaced the
remembered port. Record on the first DOM-ready instead, and keep the
remembered port when another Radar is serving on it.
…nswer

The window loaded localhost:<port>, which can resolve to [::1] where
another service may hold the same port; it now loads 127.0.0.1:<port>,
the address Radar bound. The remembered port is replaced only when
something other than Radar answers on it: the probe uses the in-memory
/api/connection, and a refused connection, a timeout or a slow Radar
leave the record alone.
/api/connection lists every kubeconfig context unless asked not to; with
a large kubeconfig the reply passed the probe's read limit and a real
Radar was taken for something else.
A remembered port inside a reserved range (Windows reserves blocks for
Hyper-V/WSL and can move them at boot) fails to bind on every launch with
nothing listening there, so each launch fell back. When nothing answers
on the remembered port and it still can't be bound, adopt the new one;
if it binds again, it was only briefly busy and is kept. Dial errors are
told apart by timeout only, since refusal codes differ across platforms.
@nadaverell
nadaverell merged commit faeba4b into main Sep 26, 2026
8 checks passed
nadaverell added a commit that referenced this pull request Sep 27, 2026
Adds the v1.15.0 entry to the What's New catalog. This replaces #1877,
which was stacked on the closed #1876; the dialog landed on main in
#1902 instead. `scripts/check-whats-new.sh` (#1908) refuses to release
v1.15.0 without this entry, so this must merge before the tag.

## Highlights
A full-width lead card, then a 2x2 grid. Each secondary card has its own
icon color, so they read apart.

1. **AI investigations that show their work** (lead, no CTA; the
investigations workspace redirects home in run modes that can't host
local agents). This recaps the v1.14.1 rework (#1766, #1802) on purpose:
it's the first What's New dialog, and we want to put AI investigations
in front of people. OpenCode (#1265) is the new v1.15 part.
2. **Full manifest diffs for Argo CD resources** (#1622)
3. **Rightsizing and cost data in MCP**: new agent tools, and risks lead
before savings in the UI too (#1820, #1723)
4. **Deeper batch + AI/ML workloads support**: Kueue, JobSet and Ray
pages; Job and JobSet details show why a queued job has no Pods (#1874,
#1885, #1900, #1849, #1893, #1897)
5. **Canary and blue-green in Topology** (#1626)

## Also in this release
Eight one-liners, paired by length because the grid fills row by row:
table sort memory (#1428), OCI Helm installs (#1815), expiring TLS
certificates in Checks (#1829), Radar Desktop keeping preferences across
restarts (#1907), Pod vs template in Issues (#1848), PVC usage
availability (#1828), Strimzi and admission webhook failures in Issues
(#1840, #1830), `--listen-address` (#1845).

Left out: security fixes, dependency bumps, refactors and CI,
accessibility work (#1901, #1909), and narrow items (ALB action
annotations, INSTALL_DIR). No usage-stats line is needed, because
#1879's consent question already sits in this dialog.

## UI changes beyond the catalog
- The heading names the release line (`v1.15`, via `releaseLine`), since
patch releases show the same notes. The "You just updated from → to"
line keeps exact versions.
- An optional `tone` on highlights colors the icon tile (violet, teal,
emerald, indigo). It uses Radar's existing `bg-X-500/10 text-X-600
dark:text-X-400` pattern, and was checked in light and dark themes.

## Verification
- `scripts/check-whats-new.sh v1.15.0` → found notes
- `make tsc` clean; What's New and usage-data tests: 39 passed
- Previewed live with `VERSION=v1.15.0 make build`, opening
`/?whats-new`, with #1879's consent question in place

The matching public changelog is skyhook-dev/marketing#312.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Release-notes catalog and presentational What's New UI only; no auth,
API, or data-handling changes.
> 
> **Overview**
> Adds the **v1.15.0** What's New catalog entry (five highlights with
CTAs where appropriate, plus eight improvement one-liners) so
`check-whats-new` can ship the tag.
> 
> The dialog heading now shows the **minor release line** via new
`releaseLine()` (e.g. `v1.15` instead of `v1.15.0`), while the upgrade
subtitle still uses exact versions. Secondary highlight cards can set an
optional **`tone`** (violet, teal, emerald, indigo) for distinct icon
tiles; the lead card stays on the accent style.
> 
> Tests cover `releaseLine` and the preview dialog title expectation.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f0038c3. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@nadaverell
nadaverell deleted the fix/desktop-stable-port branch September 27, 2026 13:09
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