Skip to content

fix(macOS): restore B1 on saved-left panel startup - #6453

Merged
jensenpat merged 2 commits into
aethersdr:mainfrom
rfoust:codex/fix-6443-pan-startup
Oct 14, 2026
Merged

jensenpat merged 2 commits into
aethersdr:mainfrom
rfoust:codex/fix-6443-pan-startup

Conversation

@rfoust

@rfoust rfoust commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #6443.

On macOS with GPU spectrum enabled, restarting with the applet panel saved on the left explicitly hid the startup B1 spectrum widget. B1 stayed blank after connecting, while a later-created B2 worked. The same build started normally with the panel saved on the right.

buildUI() creates the placeholder B1 before restoring the saved dock side. That restore calls refreshAfterLayoutShift() before the main window is shown. refreshAfterReparent() saved isVisible(), temporarily hid the spectrum, then skipped show() because the ancestor window was still hidden. Qt retained the spectrum's explicit hide flag after the main window appeared.

This change preserves isHidden() across the temporary hide/show cycle, and skips the macOS native surface refresh while the stack's top-level window is hidden. Live dock changes retain their existing Metal refresh. The user guide documents dock-side persistence. Windows/Linux rendering paths are unchanged.

Proof

The agent automation bridge exercised the actual app startup with fresh settings profiles and the built-in demo source. Each profile seeded AppletPanelDockedLeft before launch; this was not an in-session dock change standing in for startup. TX automation was pinned off, and no live radio was connected.

Build and action B1 get rhi reading
Unchanged upstream/main, saved LEFT panIndex: 0, visible: false, gpu: true, renderer: "QRhi initializing"
Unchanged upstream/main, saved RIGHT visible: true, renderer: "GPU QRhi (Metal; Apple M3 Max)"
Fixed, saved LEFT visible: true, rendererFailed: false, renderer: "GPU QRhi (Metal; Apple M3 Max)", 1260 × 732
Fixed, saved RIGHT visible: true, rendererFailed: false
Fixed, live RIGHT/LEFT dock moves and panel hide/show B1 remained visible, rendererFailed: false

applet state verified the actual dock side and splitter ordering. dumpTree and grab pan-visible 0 captured the blank baseline and rendered fixed spectrum, waterfall scale, VFO flag and overlay controls. get transmit confirmed transmitting: false. Each owned app was stopped and the shared radio lock released after the run. The agent automation bridge was sufficient; no gaps found.

Before — saved-left startup on unchanged upstream/main:

Blank B1 before the fix

After — the same saved-left startup on the fixed build:

Rendered B1 after the fix

Proof images are GitHub attachments, independent of the fork's branches, and are absent from this PR's diff.

Validation

  • Complete macOS ARM64 AetherSDR and aetherd products built with the local ARM64 toolchain, RADE and GPU spectrum enabled. Host/system processors and final executable verified ARM64; no RNNoise x86 sources in the Ninja graph.
  • native_widget_topology_test, floating_restore_policy_test and window_show_state_test: 3/3 passed. These are neighboring coverage; the before/after startup regression proof is the real macOS GPU bridge run above.
  • User guide: npm ci && npm run build passed. Generated reference check, log-rule checks (707 checks), strict test registration and accessibility checks passed. Existing log-rule line-number drift was reported without rule failures.
  • Independent read-only review found no actionable defects in the refresh helper's other float/dock/canvas call sites.
  • Local baseline and fixed launches emitted existing duplicate QtCore class warnings from the Homebrew/pinned Qt dependency mix; the checks above and Metal rendering completed. Linux/Windows builds, the full test suite and reporter FLEX-6400 hardware confirmation were not run for this macOS-only repair.

Regression introduced by #6198; its broader platform follow-up is #6224. No visual design, UX defaults, protocol or transmit behavior changes.

Generated with OpenAI Codex.

@rfoust
rfoust marked this pull request as ready for review October 14, 2026 01:42
@rfoust
rfoust requested a review from a team as a code owner October 14, 2026 01:42
Copilot AI balanced review requested due to automatic review settings October 14, 2026 01:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T01:48:05.784989Z 88a1ad9 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Yes. #6443 is a deterministic macOS regression from #6198: buildUI() creates the B1 placeholder, then restores the saved dock side at MainWindow.cpp:5895, which fires refreshAfterLayoutShift() before MainWindow::show(). refreshAfterReparent() read isVisible() (false for a child of an unshown top level), called hide(), and never paired the show() — leaving Qt's explicit hide flag set, which showChildren() honours. Both halves of the triage ruling on #6443 are implemented, in the form it proposed: isHidden() in the helper (PanadapterStack.cpp:37) and a pre-show early return in refreshAfterLayoutShift() (:360-362). The reporter's own A/B (close with panel LEFT → blank B1, RIGHT → fine) matches the fixed path. Nothing in the issue is left untouched; the only requirement the thread named and the diff does not carry is the regression test (nit 1).

Scope

File / hunk What it changes Claimed by title/body Verdict
src/gui/PanadapterStack.cpp:33-49 (refreshAfterReparent) isVisible() → isHidden() for the hide/show round trip Yes — the primary fix In scope
src/gui/PanadapterStack.cpp:356-362 (refreshAfterLayoutShift) early return when window() is not visible Yes — "skips the macOS native surface refresh while the stack's top-level window is hidden" In scope
docs/user/docs/panadapter-controls.md:164 two sentences on dock-side persistence and macOS startup Yes — "The user guide documents startup with either saved dock side" In scope (see nit 2)

Everything in the diff is explained by #6443. No sibling copy of the visibility bookkeeping exists — refreshAfterReparent() is the single helper and all six call sites (:376, :667, :1021, :1093, :1164) share it. The body's "Windows/Linux rendering paths are unchanged" holds literally: both hunks sit inside #if defined(Q_OS_MACOS) && defined(AETHER_GPU_SPECTRUM), and the #else arm is still a bare resetGpuResources(). versioned_docs/ and CHANGELOG.md are correctly untouched. No new public surface, no settings key, no removed guard — AppletPanelDockedLeft persistence at MainWindow.cpp:10035 predates this PR, so the doc's first sentence documents existing behaviour rather than new behaviour.

Blockers

None.

Nits

  • No regression test (non-blocking). The #6443 triage asked for one: build the stack, call refreshAfterLayoutShift() before show(), then assert !sw->isHidden() after the window is shown. The honest obstacle is that the whole seam is compiled out except on macOS with AETHER_GPU_SPECTRUM, so such a case added to native_widget_topology_test would pass vacuously on the Linux full suite — which is presumably why the triage said the bookkeeping would need hoisting out of the #if, i.e. a restructure this fix does not otherwise need. check-macos does run ctest, but only -R '^mac_nr_filter_test$' (ci.yml:1174) and asr_gpu_probe_test (:1189), so a new macOS-only case would not gate PR CI without touching the frozen test gate. The three tests the body cites (native_widget_topology_test, floating_restore_policy_test, window_show_state_test) are neighbouring coverage and none of them would fail against the unfixed code — the body says as much, which is the right call. Reporting as a nit rather than a blocker because there is no deterministic policy-compliant seam here without that restructure.
  • docs/user/docs/panadapter-controls.md:164 — the second sentence documents the absence of a bug:

    On macOS, the primary panadapter's spectrum, waterfall and overlay controls remain visible with the panel saved on either side.
    A reader with no memory of #6443 has no reason to be told this, and it will age badly in a cross-platform guide. Suggest keeping the first sentence and dropping the second. (I checked — there is no #6443 known-issue line anywhere under docs/user/docs that needs removing in exchange.)

  • src/gui/PanadapterStack.cpp:360 — window()->isVisible() is safe (QWidget::window() returns the widget itself when unparented and never null), but :1156 in the same file writes if (auto* w = window()). Purely a local-consistency note; no fix needed.
  • The early return also skips the trailing update() sweep at :381, which was previously the one unconditional side effect of this function. On a non-visible window update() is a no-op, so this is harmless — flagging only so it is a deliberate choice rather than an accident.
  • PR body, not code: the before/after screenshots are hosted on the fork's fix-6443-assets branch, which the body asks reviewers to keep alive. Once it is deleted the merged history has dead image links. Re-uploading them as GitHub drag-and-drop attachments would make them permanent.

What I tried to break

  • Every sibling call site of refreshAfterReparent(), for a behaviour change outside the targeted path. None. At :667, :1021 and :1093 the caller follows the helper with an explicit sw->show(), so the inner decision is moot. At :1164 (dockPanadapter) sw->show() precedes the deferred call, so isHidden() is false and it shows — identical to the old isVisible()==true branch. On the float/dock paths that hide() the widget first (:1000-1009, :1066, :1116-1119), old isVisible()==false and new isHidden()==true both skip the inner show(), and the caller's own show() still runs. The only divergence between old and new is "not explicitly hidden but inside a hidden ancestor" — exactly the pre-show case #6443 reports. I specifically probed the comment at :1089 ("Defer sw->show() until after refreshAfterReparent()") for an intent the inner show() would now violate: it does not, because that path hid the widget explicitly at :1066.
  • Whether the two halves are redundant — that is, whether the early return makes the isHidden() change dead code. They are not. MainWindow.cpp:3997 notes toggleMinimalMode(true) can emit a showEvent mid-construction; Qt sets the visible state before dispatching showEvent, so a dock change inside that window would not take the early return, and the isHidden() half is what carries it. In the other direction, isHidden() alone would still destroy and re-create a native Metal view on an unshown top level — cost with nothing to fix. Both are load-bearing, and either alone would have produced the body's "Fixed, saved LEFT → visible: true" reading, so that table cannot discriminate between them.
  • A dock toggle while the top level is hidden, post-fix. If the main window is hide()-ed (not merely minimised — Qt keeps isVisible() true for a minimised window) and the dock side then changes, the native re-realize is now skipped and nothing re-runs it on the next show: grep showEvent finds no refresh hook in either MainWindow or PanadapterStack. So in principle the #6198 stale-strip symptom could surface on re-show. I could not find a reachable path other than startup (the bridge handlers at MainWindow.cpp:4779-4804 and the menu path at MainWindow_Menus.cpp:1125 all run with the window up), and pre-PR that same path blanked B1 permanently, so this is strictly an improvement. Naming it, not asking for a change.
  • The CodeGuard findings — all four refuted (security-audit (CodeGuard)). PanadapterStack.cpp:685, :1278, :1283 and :1308 are each for (const QString& id : …); the & id byte sequence is read as a shell background operator followed by the Unix id command. The file contains no QProcess, system(, popen, exec* or startDetached sink at all, and :1263's structurally identical const QString& panId is not flagged, which confirms the lexical trigger. None of the four intersect either hunk — the nearest is 300+ lines away, and the scan clearly ran a whole-file sweep on the PR head. (The ${PR_DIFF} tokens in the diff I was handed are prompt-side mangling of &&; the checkout has &&.)
  • Claims in the body I could check. "Windows/Linux rendering paths are unchanged" — confirmed by the #if placement. CI is green on all seven checks at 88a1ad9, including check-macos, so the changed macOS branch does compile. I could not check: the macOS/Metal runtime behaviour, or the bridge get rhi readings in the proof table. Every statement above is reasoned from the head checkout at /tmp/aetherclaude/pr-6453, not reproduced at runtime — the bridge run in the PR body is the only runtime evidence this fix has, and its before/after discrimination (visible: false → visible: true on the same saved-LEFT profile) is the right shape for the claim.

Recommendation

Approve with nits. This is a four-line fix that matches the diagnosed root cause exactly, is confined to the macOS GPU branch, changes nothing at any live-window call site, and is backed by a real before/after on the reported configuration. The nits are cosmetic or coverage-boundary notes; none of them should hold the merge, and given #6443 blanks the primary panadapter in the default single-pan layout on shipped v26.10.2, with downgrading as the only workaround, the case for landing promptly is strong. Concrete next step for the author: trim the second sentence in panadapter-controls.md, and re-host the two screenshots as attachments so the merged body does not depend on a fork branch staying alive. Thanks for the careful write-up — stating plainly which builds and suites were not run made this much faster to review.


🤖 aethersdr-agent · cost: $8.0920 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (4 finding(s))

  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/PanadapterStack.cpp /tmp/aetherclaude/pr-6453/src/gui/PanadapterStack.cpp:685
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/PanadapterStack.cpp /tmp/aetherclaude/pr-6453/src/gui/PanadapterStack.cpp:1278
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/PanadapterStack.cpp /tmp/aetherclaude/pr-6453/src/gui/PanadapterStack.cpp:1283
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/PanadapterStack.cpp /tmp/aetherclaude/pr-6453/src/gui/PanadapterStack.cpp:1308

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $8.2264 · model: claude-opus-5

@jensenpat jensenpat self-assigned this Oct 14, 2026
@jensenpat

Copy link
Copy Markdown
Collaborator

Landing pass at e3e6a92: the documentation and attachment nits are already addressed. All four CodeGuard alerts are false positives on const QString& id range-for loops (PanadapterStack.cpp:685, :1278, :1283, :1308); no command-execution sink exists in the file and none intersects a changed hunk. The regression-test coverage concern was verified directly on macOS/Metal: pre-fix saved-LEFT startup fails with visible:false, renderer:"QRhi initializing"; exact-head saved LEFT and RIGHT pass with visible:true, rendererFailed:false, GPU QRhi (Metal; Apple M2 Ultra). Final build and three neighboring headless tests pass. Only isolated DEMO-0001 clients were driven, with TX disabled, then disconnected and stopped. No review threads or blocking reviews require resolution/dismissal.

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified exact head e3e6a92 against main 709093e.

The macOS/GPU-only repair preserves explicit widget hide state at src/gui/PanadapterStack.cpp:37 and skips native-surface refresh before the top level is shown at :360. All helper callers were inspected, including float, dock and canvas moves; existing explicit hides and caller show ordering remain intact. No new engine coupling, settings keys, protocol or TX behavior enters the diff.

Earlier review dispositions:

  • Documentation nit fixed by e3e6a92: panadapter-controls.md documents only dock-side persistence. The PR body now uses permanent GitHub attachments for both proof images.
  • The four CodeGuard command-injection alerts at PanadapterStack.cpp:685, :1278, :1283 and :1308 are false positives: each points to a C++ range-for reference (const QString& id). There is no QProcess, system, popen, exec or startDetached command sink in the file. None is in a changed hunk.
  • The three existing tests are neighboring coverage, not a regression test for this path. The direct macOS/Metal bridge assertion was independently run against both implementations instead; no workflow or architectural restructure was needed.

Local evidence on this head: app build passed with pinned Qt 6.12.0 and GPU spectrum enabled; native_widget_topology_test, floating_restore_policy_test and window_show_state_test passed 3/3 with QT_QPA_PLATFORM=offscreen. The four aetherd static gates passed on merge base and head with byte-identical findings.

Runtime proof on Apple M2 Ultra using fresh settings profiles, explicit owned sockets and DEMO-0001 only, with TX disabled: both saved LEFT and RIGHT startups returned panIndex 0, visible=true, rendererFailed=false, renderer="GPU QRhi (Metal; Apple M2 Ultra)". Dock LEFT/RIGHT and panel hide/show preserved that result. Captures showed the spectrum, waterfall, VFO and overlay controls. get transmit returned transmitting=false. Both owned clients were disconnected and stopped.

Negative check: temporarily restoring pre-fix PanadapterStack.cpp produced visible=false, renderer="QRhi initializing", 100x100 B1 and failed the same startup assertion. Exact PR source was restored and rebuilt before this review.

Freshness: exact-head pull_request CI was created 2026-10-14T02:29:44Z, after main's tip timestamp 2026-10-13T23:37:44Z. All four required contexts (build, check-windows, check-macos, Static checks) are successful; signatures are verified; merge-tree is clean. No unresolved review threads or CHANGES_REQUESTED reviews exist.

The model-invocable automated code-review pass was unavailable; this was a manual adversarial pass. No live-radio or RF validation occurred. Post-merge main CI remains the independent verification and will be checked after landing.

@jensenpat
jensenpat merged commit 9fdb616 into aethersdr:main Oct 14, 2026
7 checks passed
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.

[Bug] Primary panadapter B1 blank in v26.10.2 — works in v26.10.1 on macOS

3 participants