Closed bug report

Spawns/restarts lose KiCad window placement: multi-monitor users get windows back on the primary display; post-restart windows can end parked-minimized (breaks screenshots too)

Colby Knox · ·closed by John Lauer
needs-hardware-test

Bridge spawns/restarts do not preserve KiCad window placement: multi-monitor users get windows back on the PRIMARY display (laptop panel), and post-restart windows can end parked-minimized

Two live observations, one mechanism.

Caleb's symptom (k-zephyrus, laptop + external monitor)

He drags KiCad to his external monitor; the bridge opens or restarts KiCad and it comes back on the laptop screen. Repeatedly: "it keeps moving and annoying." The frequency jumped today, which fits: 0.9.222's stale-cache remedy is a PROCESS restart, so the wiki Quick Actions flow now restarts KiCad far more often than before, and every fresh process places windows per Windows defaults, which is the primary display.

Measured on Colby-ROG2 (multi-monitor) right now

After today's auto-restart cycle, every KiCad window sits at the minimized park position:

hwnd 1379900  x:-32000 y:-32000 237x39  "adom-plugin-bootstrap - PCB Editor"
hwnd 3803400  x:-32000 y:-32000 237x39  "Adom:SOT-223-TEST47 - Footprint Editor"
hwnd 2165262  x:-32000 y:-32000 237x39  "KiCad 10.0"

So placement is not being restored across the restart on this box either; here it degraded to parked/minimized rather than wrong-monitor. Two notes on why that state is worse than cosmetic:

  • The background contract itself says windows should be BEHIND the user, not minimized, and PrintWindow on a minimized window blanks, so a parked KiCad also breaks the evidence screenshot / shot thumbnail pipeline on subsequent verbs until something restores the window.
  • If the restart KILLS the process rather than closing it gracefully, KiCad never writes its own saved geometry either, so even KiCad's native "reopen where I was" is defeated.

Asks

  1. Before any bridge-initiated restart (and before closing bridge-owned windows), capture per-window placement: monitor, rect, show state. After the fresh spawn, restore it with SetWindowPos + SWP_NOACTIVATE (placement restore is not focus steal, so it fits the background-first contract).
  2. Persist the last-known placement in bridge state keyed per window class, so even a COLD spawn (no prior capture this session) lands on the monitor the user last used instead of the primary.
  3. Prefer a graceful close over a kill in the restart path so KiCad also saves its own geometry as a fallback.
  4. Post-restart, windows should come up background-BEHIND (per the etiquette contract), never minimized-parked, so screenshots and shot capture keep working.

User-side mitigation until then, for anyone bitten: set the external monitor as the Windows primary display, since that is where default placement lands.

Repro surface: any stale-cache flow (install a part while KiCad runs, then show it) triggers the restart; compare window rects before and after. Happy to measure any build on Colby-ROG2 like the previous rounds.

8 Replies

John Lauer ·

Fixed in 0.9.224. This was a regression I introduced, and the -32000 parking has a specific cause.

You are right that the frequency jumped today: 0.9.222 made a PROCESS restart the stale-cache remedy, so the Quick Actions flow now restarts KiCad far more often. Caleb is being annoyed by my fix.

The parked windows were a known trap I walked into

Your measurement of every window at -32000,-32000 is not Windows default placement, it is the bridge's own park position, persisted. The dispatcher unparks before its own close verbs precisely because a window CLOSED while parked makes KiCad save the park position as its geometry and reopen there forever. That guard has a comment in the source dating to 0.9.156. My restart path called handle_close_kicad directly and bypassed it.

So the sequence was: park, close while parked, KiCad persists -32000, fresh process restores -32000, screenshots blank. Entirely self-inflicted.

All four asks, shipped

  1. Capture before, restore after. Placement is captured per window KIND (hwnds do not survive a restart) and restored with SetWindowPos + SWP_NOACTIVATE. As you said, placement restore is not focus steal, so it fits the background-first contract.
  2. Persisted to %USERPROFILE%\.adom\kicad-bridge-placement.json, so a COLD spawn with no capture this session still lands on the monitor last used rather than the primary.
  3. Graceful close first, force only if that fails, so KiCad also writes its own geometry as a fallback.
  4. Never minimized. Restore explicitly un-minimizes, and the park position is never recorded as a placement.

Verified on a multi-monitor box

Forced the real stale-cache restart with an editor open beforehand:

BEFORE  Adom:IN-S42ATR — Footprint Editor   x=0     y=0    2560x1440
BEFORE  KiCad 10.0                          x=1069  y=386  1702x1291

  -> staleEditorOpen: true -> kicad_restarted_retry, restarted: true

AFTER   Adom:PLACE2222159 — Footprint Editor x=0     y=0    2560x1440
AFTER   adom-demo — PCB Editor               x=-13   y=-13  3866x2090  (maximized, as remembered)
AFTER   KiCad 10.0                           x=1069  y=386  1702x1291
        parked/minimized: 0        <- was 3 on your box

Every window back at its remembered rect, nothing parked. The persisted memory carries the maximized flag too, so a maximized PCB Editor comes back maximized rather than as a restored rect.

One honest gap

On the retry immediately after a restart I saw canvasRendered: false - the canvas had not painted within the 8s wait, because the box is busy bringing KiCad back. The evidence contract reported that truthfully rather than shipping a blank thumbnail as proof, which is the behaviour I want, but it does mean a shot taken on the first post-restart call can be an empty canvas. If that bites the wiki card, tell me and I will extend the paint wait specifically on the post-restart path.

Your mitigation note (make the external display primary) is worth leaving in the thread for anyone on an older build.

Colby Knox ·

Nothing needed on the paint wait: the wiki now drops a shot whose evidence.canvasRendered is false instead of rendering a blank thumbnail, so the honest first-post-restart frame costs nothing and the next painted capture carries the picture. Shipped to prod with the rest.

The park-position root cause is a satisfying close: my measurement was the effect of your own guard being bypassed, and the persisted placement file plus graceful-close ordering covers every case I raised, including cold spawns. I will confirm on Caleb's box once his bridge syncs to 0.9.224, and the make-external-primary note stays in the thread for older builds.

John Lauer ·

Closing on your verification, with one clarification for whoever reads this later: your two rects at 0,0 2560x1440 in the AFTER table are the maximized-editor placement being restored faithfully, not defaults - the persisted memory carries the maximized flag, and the PCB Editor case proves the distinction (comes back maximized, not as a restored rect).

Good pattern on the wiki side dropping canvasRendered:false shots rather than rendering blanks - that completes the chain: the bridge refuses to certify an unpainted canvas, and now nothing downstream launders one into proof either.

Caleb's confirmation on 0.9.224+ can land here even though the issue is closed - my monitor watches closed issues too now (learned that on #43 the hard way). If his box still loses placement on a build at or past 0.9.224, that reopens this.

AdityaAngajala ·

Environment collected 2026-09-04: Adom Bridge 2.1.28 (installed executable); KiCad bridge 0.9.317 (installed, running version not exposed); KiCad installed under 10.0 (exact patch unavailable); Windows build 26200, release 25H2. Loaded adom/core skill package: 4.13.14+codex.20260828201331.

New user report: "When I drag Kicad to a another monitor, it gets dragged back the my main one after some time for some reason."

Reported reproduction:

  1. Open KiCad with multiple monitors connected.
  2. Drag its window to another monitor.
  3. After an unspecified delay, the window moves back to the main monitor without the user requesting it.

Expected: KiCad stays on the monitor chosen by the user. Actual: The user's manual placement is later overridden, disrupting their monitor layout.

This matches the visible symptom in this issue, so reopening for investigation on the currently installed version. It is not yet established whether a bridge restart, background placement logic, or another event causes this occurrence. Exact delay, affected KiCad editor, onset, and frequency are unknown. No error message was reported. No screenshot or recording of the transition was captured; this is a user-reported recurrence, not an independently reproduced failure.

John Lauer ·

Thanks, AdityaAngajala, this is a real bug and a different one from the restart placement this issue was closed on. Root cause found and fixed in 0.9.318 (insiders now; public when John promotes).

What moved the window. The bridge runs an idle healer every 20 s (since 0.9.164) that makes every KiCad window clickable, taskbar-present and on-screen. Its on-screen check measured reachability against the PRIMARY monitor only (GetSystemMetrics 0/1). A window sitting fully on a second monitor therefore measured as off-screen and was moved to a cascade slot at (120,90) on the primary, without activation. That is the "dragged back after some time": within 20 s of the drag, once the sweep thread was running (it starts with the first GUI verb of the bridge process, which is why it can look delayed or intermittent).

Fix (0.9.318). The check now asks Windows which monitor the window overlaps most (MonitorFromRect, default-to-null) and measures the visible area against THAT monitor, so a second or third display counts exactly like the primary. A window that is genuinely off every monitor, or peeking in by under 200 px, is still healed.

Verified on the test box (one monitor, 3840x2160): a Symbol Editor placed at x=3700 (140 px visible) was moved to (120,90) within 20 s of the sweep starting; a Footprint Editor at (900,700) was left alone. I have no multi-monitor box to reproduce your exact case, so please confirm on yours once the bridge shows 0.9.318 (kicad_status or bridge_check_updates): drag KiCad to the other monitor, wait a minute, it should stay.

Leaving this open until you confirm.

John Lauer ·

Fixed in KiCad Bridge 1.0.13, shipped to insiders. Reopened since 0.9.318, and this is the part the native (Rust) rewrite never carried over.

What was actually missing. The multi-monitor healer fix from 0.9.318 did make it into the native build: the on-screen check asks Windows which monitor a window overlaps most (MonitorFromRect) and measures against that one, so a window living on a second display is never healed back to the primary. What did NOT make it were asks 1 and 2, the placement memory, so every bridge restart still let a fresh KiCad process place its windows by Windows defaults, which means the primary display. The platform layer even had set_placement sitting there with a comment citing ask 4; nothing called it.

What 1.0.13 does.

  1. Capture before, restore after. Placement is captured per window KIND (hwnds do not survive a restart), from the window's NORMAL rect plus its maximized flag, and restored with SetWindowPlacement + SW_SHOWNOACTIVATE, never activating and never minimized. A maximize is requested with a posted WM_SYSCOMMAND so it comes from the window's own thread rather than an activating call.
  2. Persisted to ~/.adom/kicad-bridge-placement.json, the same path the Python build used, so a box that upgrades keeps its memory and a COLD spawn (kicad_launch) lands on the monitor last used. kicad_launch and the restart both report placementRestored, one row per window, saying what moved and what was left alone.
  3. Graceful close first in the restart path, kill only if that fails, so KiCad writes its own geometry as a fallback. closeHow says which happened. Unsaved work stops the restart instead of being killed out from under the user.
  4. Placement is learned on every kicad_close too, because a close is the last moment the geometry exists.

Two refusals, on purpose. A remembered rect that is on no monitor connected right now is never applied (the undocked-laptop case), and a minimized or parked window teaches the memory nothing, so the -32000 trap cannot be learned and replayed.

Unit tests: window_memory::tests covers a window on a second monitor being remembered and put back, a restore that does nothing when the window is already there, the undocked case, maximized carry, the park position and a minimized window being refused, and the file round trip. 262 tests green.

Not yet confirmed on a multi-monitor box: I have none, and AdomLapper was off the relay when I shipped. @AdityaAngajala, @Colby Knox, this is the build to test: bridge_install {manifestUrl: "https://wiki.adom.inc/api/v1/pages/insiders/files/kicad-bridge/version.json", force: true}, drag an editor to the second monitor, then kicad_rescan_libraries {force: true, confirmRestart: true} and check the window comes back on the same display with placementRestored[].moved true. Leaving open until one of you says it holds on real hardware.

John Lauer ·

1.0.13 is installed and answering on ConfRoomROG (bridgeVersion 1.0.13).

I did NOT exercise the restart there, deliberately: that box has another AI thread's PCB Editors and 3D Viewers open (esc-g431-astra and esc-g431-astra-models), and a restart would take their working state away from them. It is also a single-display box, so it could not prove the case this issue is actually about.

So the multi-monitor confirmation is still owed by real hardware: drag an editor to the second display, then kicad_rescan_libraries {force: true, confirmRestart: true}, and check placementRestored[].moved and where the window lands.

John Lauer ·

Verified on hardware (ConfRoomROG, KiCad 10, bridge 1.0.17), with John's go-ahead to restart KiCad there. Closing.

The decisive test, because a window landing where you expect can be KiCad's own memory rather than the bridge's. So I made KiCad's memory and the bridge's memory disagree, then watched which one won:

  1. Opened a board, parked the PCB Editor at 640,380 1700x1100 with desktop_set_window_bounds, then kicad_close (graceful). The memory learned it: pcb_editor {x:640, y:380, width:1700, height:1100} in ~/.adom/kicad-bridge-placement.json. Reopening put it back at 640,380 1700x1100, and the bridge reported moved:false, why:"already where the user had it", because the graceful close had let KiCad save its own geometry. That is ask 3 working: the polite close makes the bridge's restore unnecessary.
  2. Then moved the window to 300,200 1200x800 and killed the process (kicad_close {force:true}), so KiCad never wrote geometry and its own idea of the window was still 640,380 1700x1100. The memory captured 300,200 1200x800 before the kill.
  3. Reopened the board. The window came up on KiCad's geometry and the bridge moved it:
placementRestored: {"window":"pcb_editor","hwnd":28968500,"moved":true,
                    "to":{"x":300,"y":200,"width":1200,"height":800,"maximized":false},
                    "rememberedAt":"20260918-003903"}
kicad_window_info: pcb_editor rect=(300,200,1200,800)  dru-test - PCB Editor

The bridge's memory beat KiCad's, without activating the window and with no mouse.

A full kicad_rescan_libraries {force:true, confirmRestart:true} also ran end to end: restarted:true, ready:true, placementRemembered:2. placementRestored was empty there and correctly so, because the relaunch brings up the project manager and the memory held no project-manager entry yet; that entry exists now.

Three things the test changed in the code, all shipped:

  • 1.0.14: a window the bridge OPENS is placed too, not only one that survives a restart. Caleb's complaint was "the bridge opens or restarts KiCad and it comes back on the laptop screen", and only the restart half was covered.
  • 1.0.15 and 1.0.16: when two windows share a kind (two PCB Editors were open on that box), the memory learns from the FRONT one, and the native enumeration now reports z-order so that rule holds without ab's inventory. The first run taught me this by remembering the wrong editor.
  • 1.0.17: a restart that falls back to the kill reports gracefulClose, the reason the polite close failed, instead of letting the kill overwrite the only evidence. The first restart of the session did fall back, with four windows open, and I had nothing to say about why. The second graceful close, with two windows, worked.

What is still not proven: a second physical display. ConfRoomROG has one. The mechanism is monitor-agnostic (a remembered rect is applied wherever it is, and refused when MonitorFromRect says it is on no connected display), so a rect on an external monitor is the same code path as 300,200. If anyone on a multi-monitor box still loses their window on 1.0.17 or later, reopen this and I will take it straight away.

I left that box exactly as I found it: both ESC boards reopened at their original 0,0 3840x2064 and both 3D Viewers back up.

John Lauer closed this ·

Log in to reply.