Repository navigation
feat(gui): unified 52 px title bar on Qt 6.12 window chrome — native captions on macOS/Windows, frameless on Linux (RFC #4764) - #6198
Conversation
…hersdr#4764) Replaces the 32 px in-app strip with a single 52 px bar that is the window's only title bar on all three platforms. The window stays frameless everywhere (Windows via WM_NCCALCSIZE, as before), so exactly one thing draws chrome. What the bar owns, left to right: window controls, brand mark, radio tabs, spacer, audio cluster, applet-dock selectors. New widgets * BrandMark logo + wordmark, "SDR" filled with the brand gradient * RadioTabBar per-radio tabs, status dots, discovered-radios popover * WindowCaptionButtons three control styles: macOS traffic lights (12 px on a 20 px pitch, glyphs on group hover, grey when the window is inactive), Windows 46 px caption buttons, Linux bordered chips * mac/WindowChrome objc++ trim for the frameless NSWindow — puts NSWindowStyleMaskResizable back so AppKit keeps owning edge resize, plus continuous rounded corners and shadow Heartbeat folded into the active tab's dot The standalone lamp is retired; one dot now answers both "which radio is this" and "is its link alive". The state machine is unchanged, including the two rules that are easy to lose in a rewrite: three missed sweeps is the alarm threshold, and the alarm holds SOLID red when the operator has blinking disabled rather than going quiet. Only its output moved. Platform chrome * Windows: WM_NCHITTEST answers HTMAXBUTTON over the maximize control so Snap Layouts opens on hover, and DwmSetWindowAttribute asks for DWMWCP_ROUND (Win10 falls back to square). Hover and click there are driven from the WM_NC* messages, since answering HTMAXBUTTON takes the mouse away from Qt. * Resize band widened to 8 px, still via QWindow::startSystemResize() so the compositor keeps ownership and OS tiling/snapping behave. No more emoji as controls The speaker/headphone controls were literal 🔊 / 🎧 glyphs, which render in the platform's own colour and weight and cannot follow the theme. Replaced with painter-drawn thin-line marks at the house 1.8 px stroke. Theming 15 new tokens under color.titlebar.* and color.brand.* in both bundled themes, seed regenerated, canonical-tokens.md updated. The traffic-light and chip fills are deliberately NOT tokenised — red/amber/green is the affordance being borrowed from the platform, and a themed "red" close control would stop reading as one. Automation New `get titlebar` model and `titlebar` verb (selectRadio / showDiscovery / minimize / maximize / close). Both the bar and each tab report a screenRect, and the model exposes a live linkPulse level so the heartbeat is assertable instead of eyeballed. docs/automation-bridge.md regenerated. Tests tests/unified_title_bar_test.cpp, headless. Pins the bar height, that nothing reserves a strip above it (offsetInWindow == 0), that a status-only change reuses the tab widget rather than rebuilding it, and that state is never encoded by dot colour alone — every tab spells it in words on the rendered status line and in its accessible name. 242/242 pass. Not yet verified on hardware: Windows (Snap Layouts, DWM corners, and the aethersdr#4557 top-edge-drift path this touches) and Linux (chips, fractional-scaling drag jitter). Per RFC review those are PR gates and need someone else's machines. Refs aethersdr#4764, aethersdr#3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things the textual rebase merged cleanly but got semantically wrong, plus one regression of my own that only became visible once the bar landed. setHeadphoneMuted() was setting an emoji again (aethersdr#4722) Upstream added the reconcile setter while this branch was replacing the 🔊/🎧 emoji with painter-drawn marks, so git kept its setText() verbatim and the button would have carried an emoji AND an icon. Routed through applyAudioIcon(). The setter's contract is unchanged and still load-bearing: the QSignalBlocker suppresses `toggled`, so the repaint has to be explicit or the checked state and the mark disagree. titlebar_headphone_mute_test asserted on those emoji bytes It would have failed outright. Both contracts it exists for are still exactly right — the explicit repaint, and that a reconcile must not re-emit and turn the radio's status echo into a fresh command (Principle II) — so the test is retargeted from the label text to the rendered icon rather than weakened. Comparing the drawn mark is also stronger: it survives the next change of mark, where a byte comparison against a glyph does not. Minimal mode had lost the radio-link indicator This branch folded the heartbeat lamp into the active tab's dot, and setMinimalMode() hides the radio strip — so minimal mode would have had no link indicator at all, where the old standalone lamp stayed visible. Added RadioTabBar::setCompactMode(): active tab only, no "+", indicator intact. Minimal mode is precisely when an operator has nothing else on screen that would show them a dropped link. Also: TitleBar.h's class comment still described the macOS approach this branch abandoned (keeping the real NSWindow and floating the native traffic lights over the bar). Rewritten to describe what actually ships. Bridge verb table regenerated — 65 verbs with main's additions alongside `titlebar`. Test suite on the new base: 282 tests, all green. Five hl2_* tests fail under `ctest -j8` and pass serially; they share a single-client hpsdrsim and carry no RESOURCE_LOCK, and this branch touches no HL2 file. Pre-existing, not from here. Refs aethersdr#4764 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 52 px bar's three applet controls each toggled a field of their own —
dock side, visibility, floating — with no single owner, so the fields could
disagree. A dock click while the panel was floating docked it back and then
fell through and hid it: one click doing two things, leaving an invisible
panel that took two more clicks to recover. That is the "takes two or three
pop-outs" report.
Route all three controls, Ctrl+Shift+S and the bridge through one entry
point, applyAppletPanelState(floating, dockedLeft, visible), which takes the
COMPLETE desired state instead of mutating one field. Two combinations are
now unrepresentable by construction: floating-and-hidden (an empty float
window has no affordance to bring the contents back), and side-set-without-
visibility. The dock-click rule is stated once — clicking the wall the panel
already occupies is the hide gesture, and that only reads as "hide" when the
panel is genuinely docked there and on screen.
Two further defects surfaced only under test, both of which would have
survived a reading:
* QSplitter will not re-slot a HIDDEN child. Setting the wall while the
panel was hidden persisted the new side but left the widget in its old
slot, so it reappeared on the wrong wall. Show before reordering.
* setAppletPanelDockedLeft() was not idempotent — pre-existing, and the
one with teeth. insertWidget() detaches the panel first, which shifts
m_panStack down one index, so re-inserting at the now-stale panIdx drops
the panel on the far side and silently flips the dock. Docking left twice
moved it right. The title-bar path never hit it because clicking the same
wall means hide; the new verb hit it on the first call. Re-asserting the
current side is legitimate (it happens on un-float and on restore), so
the guard lives in the setter rather than in its callers.
The dock and pop-out controls carried only tooltips: no objectName, no
accessibleName. They were absent from dumpTree and invisible to screen
readers, and had to be driven by raw pixel coordinates. Named
appletDockLeftBtn / appletDockRightBtn / appletPopOutBtn, with accessible
names and descriptions.
New `applet` verb (dock left|right, float on|off, show, hide, state), 66
verbs total. The snapshot returns geometry and splitterIndex/panIndex beside
the flags, because splitterIndex < panIndex is what actually proves the left
dock — asserting on `side` alone lets a zero-width panel pass as a success.
The mute toggles kept an accent focus ring painted around them after a
click. Fixed with focusPolicy(Qt::TabFocus) rather than by deleting the
:focus rule, so the ring still appears for keyboard navigation, where it is
the only indication of where focus sits.
Verified against the built-in simulator over the bridge: 158 checks, 0
failures, driven through the real widgets via clickAt on the labels rather
than through the verb, so it exercises the operator's own path. Covers the
original repro, eight consecutive pop-out clicks alternating every time, the
full 3-control x 4-state transition matrix with invariants asserted after
every transition, one-click recovery onto the REQUESTED wall from every
state, verb/echo parity, and 30 rapid-fire clicks with no settle. Asserting
the wall rather than just visibility is what caught the hidden-reorder bug.
The state machine is radio-independent and was reproduced with the radio
fully disconnected — it is not specific to the simulator.
Not covered: the applet logic lives inside MainWindow, which has no test
harness, so none of these three bugs is guarded in CI. The driver script is
not checked in.
Developed in parallel with the preceding reconcile commit, which fixed the
same setHeadphoneMuted() emoji leftover and retargeted
titlebar_headphone_mute_test to the rendered icon independently; that work is
kept as it stands there and dropped from here.
Refs aethersdr#4764
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving the applet panel to the LEFT wall left a 260 px strip on the right
that showed the desktop through the window. Measured on real screen
captures: 6/6 left docks on a fresh launch, 0/6 right docks, with the strip
falling at exactly the slot the panel had occupied when docked right.
The panel does not resize when it changes walls, it TRANSLATES, so the
panadapter has to slide the same 260 px the other way. Geometry was already
correct — the widget reported the full 1450 px while its native surface was
still the pre-flip 1190. So the pan repainted happily, every frame, into a
drawable 260 px too narrow, and the strip beyond it was painted by nobody.
MainWindow is WA_TranslucentBackground and its paintEvent is the only source
of pixels for regions the tree leaves bare, and Qt excludes native children
from that backdrop, so "painted by nobody" composites to the desktop rather
than to the app background.
The asymmetry is why the report was always "panel on the left": when the
panel moves RIGHT the strip needing new cover is taken by the applet panel,
an ordinary widget that paints correctly. Only the left flip depends on the
native surface keeping up.
Two changes, because either alone leaves a hole:
* PanadapterStack::refreshAfterLayoutShift() re-realizes each spectrum's
native window after a same-window move. resetGpuResources() + update()
was tried first and did NOT work — re-rendering into the same undersized
surface changes nothing, which is also why the continuously redrawing
waterfall never healed it. Destroying the native window is the part that
re-establishes the geometry, so this reuses the full reparent path at the
cost of a Metal re-bind on an explicit, infrequent operator action.
* AppletPanel paints its own background. It had none — no
autoFillBackground, no WA_StyledBackground, no paintEvent — so its area
depended entirely on a backdrop that does not reach behind native
children. The FLOATING panel has always been opaque; this gives the
docked panel the same guarantee.
Also: the pop-out opened full-screen. restoreGeometry() faithfully restores a
MAXIMIZED state if one was ever captured, and the float window re-saves its
geometry on every Move/Resize — so one accidental maximize (a title-bar
double-click will do it) poisons the setting and every later pop-out opens
across the display. A 260 px applet rail maximized full-screen is never the
intent, so the state is dropped on restore and clean geometry written back,
healing an already-poisoned config on first use.
Verified over the bridge against the built-in simulator: 0/6 left docks
broken (was 6/6), 0/8 right docks, plus 14 rapid clicks with no settle,
float/dock interleaving, and hide/show interleaving — all clean. Every
measurement gates on confirming AetherSDR is frontmost first, after an
earlier run measured an overlapping window and reported false failures.
Refs aethersdr#4764
…rier, Windows minimal mode Five blockers from @ten9876 and @aethersdr-agent, plus the two nits. The radio-tab commit is split out to its own branch as asked; what remains here is the chrome. **A re-clicked tab stayed unchecked forever.** RadioTab is checkable and belongs to no exclusive group, so QAbstractButton toggles it off on press; setActiveRadio() then early-returned on an unchanged id, so applyActiveState() never re-checked it — and nothing else did either, since setRadios() also early-returns on an identical list. Clicking the tab of the radio you are on is the most natural thing on the strip to click, and it stopped the strip showing which radio you were on. The click now re-asserts the checked state. **Activation was optimistic.** The click moved m_activeId even though MainWindow deliberately opens the picker instead of switching, so the strip and the bridge's activeId both claimed a radio the client had never connected to, with the link visuals following the wrong tab. The active tab now follows the session only — setActiveRadio() from refreshRadioTabs(). Same fix on the popover row, which had the same optimistic claim. **The link indicator had no carrier while disconnected.** Only the tab matching m_activeId took the override and alarm, so with nothing connected the state rendered nowhere — and "searching" is reported precisely when nothing is connected, which is the state it exists for. Minimal mode compounded it: the strip showed only the active tab, so with no active radio it rendered empty, the exact loss the strip is kept in minimal mode to prevent. The carrier now falls back to the first tab, and compact mode shows whichever tab is carrying. An active id that matches no tab yet (mid-rebuild) is deliberately left uncarried rather than painted onto an unrelated radio. **Windows: the maximize control stopped exiting Minimal Mode.** WM_NCHITTEST answered HTMAXBUTTON unconditionally, so Windows routed the click non-client, Qt never saw it, and the branch maximized a strip that is not meant to be maximized — a regression against main, where the control was a QLabel through TitleBar::eventFilter. Both the hit-test answer and the click are now gated on minimal mode; declining HTMAXBUTTON there also stops Snap Layouts offering to tile the strip. Also added the missing TrackMouseEvent(TME_LEAVE|TME_NONCLIENT), without which WM_NCMOUSELEAVE never arrives and the maximize button stays lit after the cursor moves into the client area. **refreshAfterLayoutShift() fired on the no-op path.** applyAppletPanelState() routes show, hide, un-float, pop-out, Ctrl+Shift+S and the bridge verb through setAppletPanelDockedLeft(), so a per-panadapter native re-realize ran on transitions where nothing moved — and on the hide path it ran before the visibility change that causes the geometry shift. Now scoped to an actual wall change. The visibility flip does need the same treatment (the panadapter gains or loses the panel's 260 px), so that call moved into setAppletPanelVisible() where the width actually changes, guarded on a real change. Nits: the pulse decay was 1400 ms against a ~1 s beat interval, so the ticker was re-armed mid-decay and ran at 12.5 Hz for the whole session — the opposite of the "an idle bar costs nothing" claim next to it. Now 850 ms, with the comment made honest about why. The macOS CMake comment described keeping the real NSWindow chrome, which is the approach the branch abandoned; reworded per the suggestion. All four behaviours are now guarded in unified_title_bar_test, which is where the review found the gap — blockers 1, 2 and 7 were caught by a reviewer's scratch harness and by running the app, not by CI. Verified as real guards: reverting the fixes fails all four assertions. Verified: 282 tests pass; 158 bridge checks for the dock/float state machine; and the see-through strip stays fixed after the scoping change — 0/10 across six dock flips and four hide/show cycles, the path the refresh moved to. NOT verified, no hardware here: the Windows minimal-mode fix, the TrackMouseEvent change, and the two PR-gate items the RFC review moved from RFC to PR (aethersdr#4557 frameless top-edge drift, Wayland fractional-scaling drag jitter). Refs aethersdr#4764
Use Qt expanded client areas and native captions where supported, retaining a shared Linux fallback. Improve radio-switcher search, action gating, naming and tab visibility. Align the macOS brand with measured native traffic-light bounds using a native unified toolbar. Validated with the macOS Qt 6.12 preview build, four focused offscreen tests, native Cocoa geometry assertions and isolated automation-bridge checks. Windows/Linux CI and native tiling certification remain deferred; this is a testing branch, not a release-toolchain migration.
…rinciple XI.
The bar never drew its own fill or border: `TitleBar {}` cannot select a
namespaced class, so it rendered as the window colour. It now paints a
pre-composited opaque fill and a 1 px hairline (WA_OpaquePaintEvent), so a
tab repaint stops at the bar instead of repainting the window under it.
Painting the fill exposed MainWindow's window-wide `QWidget { background }`
rule slabbing the drag gutter, audio cluster and empty tab viewport in the
window colour; those are now transparent by object name (not by type — the
discovery popover and menus are descendants and keep their panels).
- Tabs 36 px, 8 px clear of the bar edges: outside FramelessResizer's band on
Linux and Qt's top resize border on Windows.
- Line two no longer repeats the name: [model ·] status [· detail]; the model
shows only when a nickname hides it. a11y/tooltip keep the name.
- Heartbeat glow repaints only the dot's footprint, not the whole tab.
- A vertical mouse wheel scrolls an overflowing tab strip.
- A user disconnect stops the miss timer and clears the link alarm; an
unexpected loss still raises it (verified with `sim disconnect`).
- Minimal mode no longer leaves the dock separator dangling (m_dockSep was
never assigned).
- "+" rings for keyboard focus only; popover empty state says nothing has
been found yet instead of "No matching radios".
- refreshAfterReparent back to macOS-only, as on main; the Metal fault was
never reproduced on D3D/GL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ciple VIII. ThemeManager.h's include count moved 162 -> 165 with the rebased title-bar work; tools/gen_touchpoint_manifest.py --check failed Static checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @jensenpat — CI on What failed
That step is WhyI fetched the branch and reproduced it. The regenerated manifest differs from the committed one by exactly one row: -| `core/ThemeManager.h` | 162 | ui-support — Qt token-based theming singleton …
+| `core/ThemeManager.h` | 165 | ui-support — Qt token-based theming singleton …The column is "how many files include this touchpoint header", and this branch adds three new GUI files that include
162 + 3 = 165. Confirmed with The header total stays at 239 — the five new The fixOne line changes. Worth re-running after any rebase too — #6195 and #6140 also regenerate this manifest (different rows, so they should merge clean, but the regen is free insurance). Scope of this report
Thanks for the careful writeup on this one — the honest Test-gates checklist and the explicit RFC deviation table make this much easier to review. 73 🤖 aethersdr-agent · cost: $5.3245 · model: claude-opus-5 |
… Principle VIII. ConnectionPanel::renameRadio() constructed a PersistentDialog, so every target that compiles ConnectionPanel.cpp now needed the frameless-dialog stack. startup_autoconnect_lockout_test does not link it and failed to link on Linux and macOS CI. ConnectionPanel keeps the decision (canRenameRadio) and the write (setRadioNickname), and asks its owner for the dialog through radioRenameRequested. MainWindow builds the same PersistentDialog — same object names (radioNicknameDialog / radioNicknameEditor / saveRadioNickname), geometry key, styling and behaviour — so the bridge targets and the operator's view are unchanged, and ConnectionPanel is back to main's dependency footprint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty_test on Apple. Principle VIII. aethersdr#6044's test compiles TitleBar.cpp, which on this branch calls the macOS native-title helpers (mac::updateNativeTitleVisibility / nativeCaptionBounds). Add it to the existing if(APPLE) foreach that already supplies NativeWindowTitle.mm and AppKit to the other TitleBar-linking tests; without it check-macos fails to link the target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
1. Issue fit
Implements the approved RFC #4764 and delivers most of it: the single 52 px bar, the brand mark, radio tabs with a worded status line, the + popover, the heartbeat folded into the active tab's dot (with the solid-red-when-blink-off rule preserved — I checked RadioTabBar.cpp:616-640), the de-emojified audio cluster, the tokens, and the get titlebar / titlebar bridge surface. The right-click blink toggle the RFC's risk 4 promised did move onto the tabs (TitleBar.cpp:259), and the View-menu checkbox still routes through the same setBlinkEnabled().
Partially, on one named RFC deliverable: feature 7 and the use case "Snap Layouts on Windows 11 — hover the maximize button, get the layout flyout" were part of what @ten9876 approved, and this branch regresses them (W1) rather than shipping them, because it hands caption drawing to Qt. The RFC's own problem statement (§2) is Snap Layouts' absence. The deviation is honestly disclosed and the macOS half of the trade is clearly better than approved (decision 3's lost tiling menu comes back) — but "approved RFC feature not delivered on the platform the RFC named it for" is a maintainer call, not a reviewer one. Same for the ~31 px Qt caption buttons vs the RFC's 46 px (W2).
The PR is self-declared Draft with four Windows gates, one Linux gate and one macOS gate unchecked. Nothing below is a reason to re-open that; it is already open.
2. Scope
RFC implementation scope was "14 files, ~1100 insertions". This is 35 files, 4734 insertions.
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
TitleBar.*, BrandMark.*, RadioTabBar.*, WindowCaptionButtons.*, WindowChrome.h, mac/NativeWindowTitle.* |
The bar itself | Yes (RFC + body) | In scope |
MainWindow.{h,cpp}, MainWindow_Session.cpp, MainWindow_Menus.cpp |
Bar wiring, tab feed, chrome config | Yes | In scope |
resources/themes/*, ThemeSeedGenerated.cpp, canonical-tokens.md, logo-96.png, resources.qrc |
21 tokens + asset (RFC said 15) | Yes | In scope; token growth undisclosed, see nits |
AutomationServer.{h,cpp} — titlebar verb + get titlebar |
RFC feature 9 | Yes | In scope (new public surface — see B1) |
AutomationServer.{h,cpp} — applet verb |
New public bridge verb: dock/float/show/hide/state | No — body never mentions it; only commit ee5ac3b does |
Needs maintainer decision, + body update |
AppletPanel.cpp, PanadapterStack.{h,cpp} |
Docked-panel background + refreshAfterLayoutShift() for the dock flip |
No | Explained by 58b4e30/ee5ac3b, not by the body — name it in the body |
ConnectionPanel.{h,cpp} (+ MainWindow_Session.cpp dialog) |
selectRadio/selectManualConnection/rename plumbing; QInputDialog → PersistentDialog |
Partly (the popover needs the selectors) | In scope-adjacent; the nickname-dialog rewrite is a side effect, disclose it |
docs/qt612-window-chrome.md (new, 186 lines) |
Branch diary | No | Blocker B2 |
docs/automation-bridge.md, docs/architecture/aetherd-touchpoints.md |
Verb/model docs, manifest count 162→165 | Yes | In scope |
tests/tests.cmake, tests/unified_title_bar_test.cpp, tests/titlebar_headphone_mute_test.cpp |
New test + link sets | Yes | In scope |
No CHANGELOG.md entry — correct.
Socket-test record: this PR adds no socket-owning test and removes none. tests/unified_title_bar_test.cpp is socket-free (no QTcpServer/QLocalServer/QUdpSocket/listen/connectToHost); it is registered in tests.cmake:7186 and joins AETHER_SETTINGS_CONSUMERS. It is not in any ctest -R filter in ci.yml, so the body's 18/18 is a local result and CI does not gate it.
3. Blockers
B1 — src/core/AutomationServer.cpp:9244-9263: doTitleBar runs a button click and a window close synchronously inside the socket read callback, against this file's own written rule
Credit: security-audit (CodeGuard), confirmed against the code.
if (!m_titleBarActionHandler(action.trimmed(), target.trimmed(), &why)) {That handler is MainWindow::automationTitleBarAction, and its three sinks are exactly the ones this file defers everywhere else:
selectRadio→tab->click()(MainWindow.cpp:4562) — aQAbstractButtonclick.AutomationServer.cpp:4499-4503on theinvoke clickpath: "a click can open a popup menu or modal dialog with a nested event loop; run synchronously inside the QLocalSocket read callback it corrupts socket/window state (SIGSEGV inQWindow::geometry()). Defer to a clean main-loop turn… (#3646)". Clicking a tab reachesshowConnectionDialog(), which raises a top-level window.showDiscovery→RadioTabBar::showDiscoveryPopover()— constructs a popover with aQMenuper row and shows it.doShowMenudefers for this reason.close→close()(MainWindow.cpp:4588) oremit caption->closeRequested(), direct-connected towindow()->close()(TitleBar.cpp:601).doCloseat9722wraps the identical call inQTimer::singleShot(0, qApp, …)and replies"deferred": true, with the rationale in its header: "a confirm-dialog closeEvent can't re-enter the socket callback".
The aggravating detail: the new function was inserted immediately beneath doWindow's #3918 header block, which is still at 9238-9243 and still says
// State changes don't spin a nested event loop, so they're safe to run
// synchronously here (unlike click/menu, which defer).
so doWindow's documentation is now orphaned above a body that does both the click and the close that sentence excludes, and reads as this function's own justification.
Second half, same hunk at 9264-9266: after the handler has run close(), doTitleBar calls m_titleBarSnapshotHandler() to build the echo — reading a MainWindow that has already run its whole closeEvent teardown (MainWindow.cpp:3921+: settings save, socket disconnects, thread wait()). Not a use-after-free today — MainWindow sets no WA_DeleteOnClose — but it is one the day that changes.
Fix: defer, as the siblings do, and take the snapshot before the action rather than after.
QJsonObject reply{{QStringLiteral("ok"), true},
{QStringLiteral("action"), action.trimmed()}};
if (!target.trimmed().isEmpty()) {
reply.insert(QStringLiteral("target"), target.trimmed());
}
// Snapshot BEFORE the action: `close` tears the window down, and the
// post-action read would inspect a MainWindow that has already run
// closeEvent. Re-read `get titlebar` to confirm the result.
if (m_titleBarSnapshotHandler) {
reply.insert(QStringLiteral("titlebar"), m_titleBarSnapshotHandler());
}
reply.insert(QStringLiteral("deferred"), true);
return reply;…with the handler call itself moved into deferInvokeAction(...) / QTimer::singleShot(0, qApp, …). The validation half has to stay synchronous so an unknown action or a missing tab still errors — splitting "resolve and validate now, activate on the next turn" is the shape invoke click already uses.
doAppletPanel at 9289 has the same structure: applyAppletPanelState() creates and tears down the top-level applet float window (MainWindow.cpp:4501-4520) from the read callback. Lower confidence that it can spin a loop, but it's the same invariant, so I'd apply the same treatment.
The docs change needs a matching correction: docs/automation-bridge.md currently promises "The reply echoes the post-action get titlebar snapshot, so a caller never needs a follow-up read."
B2 — docs/qt612-window-chrome.md: a personal branch diary, and it is false against current main
The first paragraph:
This is a development migration, not a release-toolchain change. CMake requires Qt 6.12; the local macOS SDK is the official 6.12.0 preview package dated 2026-09-01. Windows/Linux CI and release packaging remain on their upstream toolchains and cannot build this branch unchanged. Do not publish it as a release-ready, cross-platform-verified migration.
That was true when it was written and is not true now. main already carries the Qt 6.12 floor — CMakeLists.txt:315, find_package(Qt6 6.12 REQUIRED …), shipped with v26.10.1 (8df6db2f, "Qt 6.12") — and this PR's CMakeLists.txt diff does not touch it. At head 93d39e02, build (Linux), check-windows, check-macos, Static checks and Sanitizer option configures are all green. A merged doc telling the next reader that CI cannot build this tree is worse than no doc.
The rest of the file is branch-local process notes, not architecture: a fork branch (jensenpat/AetherSDR/tree/aether/applet-picker), a local laptop bundle name (AetherSDR qt612-applet-picker-0de5a2b6-dirty 1748.app), SFTP/quarantine/ad-hoc-signing steps, "preserved locally on aether/applet-picker-integration", a worktree-local build/qt-sdk recipe, and two dated "Local evidence" sections including "Computer Use clicked the + button". That belongs in the PR body (where most of it already is), not in docs/.
What's genuinely worth keeping is the ~40 lines that describe the shipped design: the WindowChrome.h policy table, the per-platform notes, and the radio-switcher behaviour table. Those read as architecture and would sit well as docs/architecture/window-chrome.md. My ask is: keep that, drop the diary and the stale toolchain framing.
4. Nits (non-blocking)
src/gui/AppletPanel.cpp:284-296— the new comment's premise is contradicted by this same PR: "MainWindow isWA_TranslucentBackground, so its paintEvent is the only thing filling any region the widget tree leaves bare".WindowChrome::configure()setswindow->setAttribute(Qt::WA_TranslucentBackground, false)and the body says the window is now opaque. The second half of the comment (Qt excludes native QRhi children from the backdrop) is the part that still holds; the first clause will send the next reader looking for an attribute that isn't set.tests/tests.cmake:6444—connection_panel_size_testgainedsrc/gui/PersistentDialog.cpp, but115e822emoved the dialog out ofConnectionPanel.grep PersistentDialog src/gui/ConnectionPanel.cppnow matches only a comment (ConnectionPanel.cpp:1610). Looks like a leftover from the pre-115e822eshape.docs/automation-bridge.mdget titlebar—RadioTabBar::state()also returnslinkPulse,linkAlarm,linkOverrideColoranddiscovered[].canRename, none of which the doc mentions.linkPulse/linkAlarmare the fields that make the body's "the heartbeat is asserted rather than eyeballed" claim true (and the RFC's use case nameslinkPulseexplicitly), so they're the ones most worth documenting.resources/themes/*.json,canonical-tokens.md— the body admitscolor.titlebar.background.macis now unused. Dropping it in this PR is cheaper than a follow-up that has to touch both themes, the seed and the token count again. Also worth a body line that the RFC's "15 new tokens" became 21.src/gui/TitleBar.cppbarState()—WindowChrome::usesNativeCaption(window())andwindow()->windowFlags()are unguarded two lines abovewindow() && window()->windowFlags()….QWidget::window()won't return null here, so this is consistency rather than a crash, but the mixed guarding suggests one of the two readings is wrong.src/gui/MainWindow_Session.cpp(radioTabActivatedlambda) —m_connPanel->selectRadio(radioId)returnsbooland the result is dropped.ConnectionPanel::selectRadioonly searchesm_radios/m_wanRadios, so clicking a tab for a SmartLink radio while logged out, or for the routed/"Manual" synthetic tab, opens the picker on whatever page it was on with no feedback.src/gui/MainWindow_Session.cpp(radioActionRequestedremove/restore) —settings.setValue("RadioSwitcher", …)result is unchecked;docs/agents/settings.mdasks writers to check. The JSON document also carries no schema version, where Principle V asks for "one versioned JSON document" (the "one object per feature, not another loose flat key" half is satisfied, and app-global is the right scope for a hidden-tab list).src/gui/MainWindow_Session.cppstatusForfor SmartLink —wan.status.compare("Available", Qt::CaseInsensitive) != 0means an empty or unrecognised status renders as "in use", i.e. "another station has it". Defaulting the unknown case toAvailablewould be the safer read.src/gui/RadioTabBar.cppshowDiscoveryPopover()—"Discovered radios"and"Connect manually…"are bareQStringLiteralwhile everything around them istr().src/gui/PanadapterStack.cpp—refreshAfterLayoutShift()'s native re-realize is macOS-only by design, so if the dock-flip see-through strip reproduces on Windows/Linux the repaint sweep alone won't fix it there. Reasonable given no repro, but it's worth saying out loud that58b4e30is a macOS fix.
5. What I tried to break (and didn't)
- Bridge auth and authorization ordering on the new verbs. The constant-time token gate (
AutomationServer.cpp:3935) and the observe-only gate (3963) both run inhandleLinebefore verb dispatch, andtitlebar/appletare absent fromisReadOnlyRequest's safe set (2723-2786), so they fail closed in observe-only mode.get titlebarrides thegetentry and is a pure read. No bypass. - TX-guard bypass. Neither verb reaches a keying path, and
titlebar closeis not a new app-terminating capability — the pre-existingclose <target>verb already closes top-levels. No Principle VI issue introduced. - Injection / unknown-token fall-through. Both handlers are exact-match allow-lists with
return fail(...)tails (MainWindow.cpp:4501-4522,4552-4598);targetis used only as an equality key againsttab->entry().idand echoed JSON-encoded.doTitleBarnot lower-casingactionwhiledoAppletPaneldoes is an inconsistency that fails closed. - Handler lifetime. All four are null-checked (
9250,9278,9285) and their[this]captures areMainWindow, which ownsm_automationas aunique_ptrmember — so it outlives the server. Only the post-close()snapshot (B1) is exposed. - Deleted guards. Every
-line citing an issue (#4827,#4886,#4328,#2584) is code motion, not removal: those comments and their guards still exist in head atFramelessResizer.h,WindowGeometryRestore.h,MainWindow.cpp:2593andMainWindow_Workspace.cpp:412. The other-lines with hex in them are hardcoded colours going away, which matches the body's −50 colour-reference claim. - The light-theme caption-glyph contrast I suspected.
color.titlebar.caption.glyphresolves to{color.gray.100}=#506070on a#f5f5f8bar — that's the same ink astext.secondary, so it passes. Not a finding. - Applet state machine. The two "deliberately not representable" combinations are actually enforced in code, not just in the doc:
applyAppletPanelStateforcesvisible = truewhen floating (MainWindow.cpp:9719-9721), andhidepassesfloating=false(4520). TheQSplitter-won't-re-slot-a-hidden-child comment at9742-9750describes a real Qt behaviour and the show-then-move order handles it. - Rename-path equivalence. The removed
QInputDialogbranch and the newPersistentDialogroute end at the samehl2::Hl2Discovery::setNickname+ relabel + eager commit;showRadioContextMenu's earlyreturnafterrenameRadio()is correct becausesetRadioNickname()does the relabel and emitsradioNicknameChanged(). The one behaviour change is the match key (serialonly, vsfamily && serialin the surviving clear-nickname path) — harmless unless two families share a serial. addDemoRadio—demo.serialis assigned beforeradioDisplayName(demo, …), so the simulator's nickname override resolves rather than silently falling back.- Test-link completeness. Only four
tests.cmaketargets compilesrc/gui/TitleBar.cppand all four gained the three new sources; theif(APPLE)foreach covers the same four. No target is left short ofNativeWindowTitle.mm.
Everything above is reasoned from reading the code at /tmp/aetherclaude/pr-6198 — I ran no build, no test and no GUI. The whole class of findings the open gates cover is outside what I can reach: macOS native hit-testing under the empty NSToolbar (M1), Snap Layouts hover (W1), Qt's caption-strip synthesized press (W3), Windows top-edge drift (#4557), and Wayland fractional-scale drag/resize. I also cannot judge the screenshots' "before" against anything I can render.
6. Recommendation
Needs maintainer decision, with B1 and B2 to fix first.
B1 is a concrete correctness fix inside the PR's own new code and should land before the Windows/Linux gates get spent on a build that can crash the bridge on titlebar selectRadio. B2 is a one-file decision. Beyond those, the three things only @ten9876 can rule on are: the undelivered Snap Layouts feature from the approved RFC, the opaque-window default that retires main's backdrop-alpha experiment, and the applet verb as new permanent public bridge surface arriving unannounced.
The bar itself is good work, and the test is the honest kind — it mirrors MainWindow's real stylesheet cascade so the bug it was written for (TitleBar {} never matching a namespaced class) actually reproduces in it, and it checks a rendered pixel rather than asserting the implementation back at itself. Thank you for the RFC correction note and for listing the gates as unchecked rather than claiming them.
🤖 aethersdr-agent · cost: $18.7288 · model: claude-opus-5
…k; docs to architecture. Principle VIII. Review of aethersdr#6198 (B1): `titlebar` clicked a tab (which can raise the Connect window), built the discovery popup, or ran MainWindow::close() synchronously inside the QLocalSocket read callback — the aethersdr#3646 hazard `invoke click` and `close` already defer around — then read a post-close snapshot. `applet` created and tore down the float window the same way. Both verbs are now two-phase: the MainWindow handler validates synchronously (unknown action / missing tab still answer ok:false) and returns the activation, which AutomationServer runs via deferInvokeAction on the next main-loop turn. The reply carries "deferred":true and the snapshot taken BEFORE the action. Verified live: showDiscovery/dock land a turn later, bad input errors immediately, `titlebar close` exits cleanly in 0.5 s. The aethersdr#3918 doWindow header that had been orphaned above doTitleBar is back on doWindow. B2: docs/qt612-window-chrome.md was a branch diary with a stale "CI cannot build this" preamble. Its design content moves to docs/architecture/window-chrome.md, updated to this head (painted opaque bar, stylesheet-cascade trap, 8 px tab insets, Snap Layouts status); the local build/evidence notes are dropped. Nits: drop the unused color.titlebar.background.mac token (themes, seed, canonical-tokens 72 -> 71); document linkPulse / linkAlarm / linkOverrideColor / discovered[].canRename; correct AppletPanel's "MainWindow is translucent" premise; tr() the popover strings; an unrecognised SmartLink status reads as available, not in use; a tab whose radio is not in the picker says so; version the RadioSwitcher document; consistent window() reads in barState(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
The implementation substantially covers approved RFC #4764 (brand, two-line radio tabs, discovery panel, audio controls and bridge inspection), but partially satisfies it: the approved Windows Snap Layouts feature is explicitly absent, and the RFC's required Windows/Linux platform checks are still unchecked. Reviewed head cbc4872897efab44c7748214c65864991c5cc85f against merge base 12df694349c0f57081c24563783e267447fc3b20.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
TitleBar.*, BrandMark.*, RadioTabBar.*, WindowCaptionButtons.*, WindowChrome.h, mac/NativeWindowTitle.* |
Unified bar, tabs, controls, platform chrome | Yes | RFC implementation; platform acceptance remains outstanding |
MainWindow.*, MainWindow_Session.cpp, ConnectionPanel.* |
Discovery/session integration, hide/restore, rename dialog, opaque main window | Yes | Disclosed; opaque-window default needs Jeremy's ruling |
MainWindow.cpp status-bar styling hunks |
Change station border, separators, inactive indicators, automation chip, FFT icon and CPU/TX colours below the main content | No specific claim or RFC requirement | Unbundle unrelated visual changes (B1) |
AppletPanel.cpp, PanadapterStack.*, MainWindow_Menus.cpp, applet-state portions of MainWindow.* |
Dock/float state correction and Metal refresh | Yes | Disclosed adjacent bug fixes; no new Windows/Linux native re-realization |
AutomationServer.*, docs/automation-bridge.md |
titlebar and applet public verbs, deferred actions |
Yes | titlebar approved in RFC; applet needs Jeremy's decision |
resources/themes/*, ThemeSeedGenerated.cpp, canonical-token docs |
20 tokens and generated seed | Yes | In scope |
Logo, resource registration, CMake, tests/tests.cmake, titlebar tests |
Assets, build integration and coverage | Yes | In scope |
| Architecture index/window-chrome docs and touchpoint manifest | Documentation and generated include count | Yes | In scope; ThemeManager count rises 162→165 through the three declared new widgets |
No commit predates the branch's initial August 6 feature commit. The body usefully discloses dock/float work and three maintainer decisions, but its list does not explain the separate status-bar visual changes.
Blockers
-
Unbundle the status-bar restyle.
src/gui/MainWindow.cpp:5593changes the bottom status bar's border and separator/inactive colours;:5628changes the automation chip (including adding a border);:5656changes the status FFT glyph;:5929changes CPU warning colours, and:6133changes the inactive TX label. The station border also changes at:364. None implements the 52 px title bar, and none is identified as a separate defect or accepted requirement in #4764. Using tokens is a valid cleanup, but it also changes existing rendered values outside the approved surface.CONTRIBUTING.mdsays “One issue per PR. Keep changes focused and reviewable.”GOVERNANCE.mdlists “Any change to visual design — colors, fonts, spacing, theme, icons” under RFC-required changes. Keep the title-bar token work here; move these unrelated status-bar hunks into their own proposal/PR. This is a scope finding from the diff, not a reproduced rendering defect. -
Complete the expressly required platform acceptance, or obtain an explicit revised maintainer ruling.
src/gui/WindowChrome.h:25replaces the prior Windows custom-frame path with expanded-client-area hints, whilesrc/gui/TitleBar.cpp:684hides the shared caption controls when native captions are used. The PR body acknowledges that this implementation does not deliver the RFC's Windows 11 maximize-hover Snap Layouts and leaves/winbuild, #4557 top-edge drift, Windows pointer delivery and fractional-scaling Wayland checks unchecked. The maintainer's RFC #4764 decision 2 expressly says “it becomes a PR gate rather than an RFC gate”, with/winbuild, deliberate #4557 regression coverage and fractional-scaling Wayland checks named. Headless QWidget tests cannot settle those native interaction questions. Supply that evidence and deliver the accepted Snap Layouts behaviour, or have Jeremy explicitly revise the acceptance criteria; do not present the RFC as fully implemented before then. This is missing required acceptance evidence and an acknowledged feature gap, not a locally reproduced Windows defect.
Nits and maintainer decisions (non-blocking on their own)
- Rename dialog misses runtime chrome propagation, reasoned from code:
src/gui/MainWindow_Session.cpp:537constructs aPersistentDialogbut does not register it inm_persistentDialogs.ConnectionPanel::setFramelessMode()updates only itself; MainWindow's propagation loop updates tracked dialogs. AddtrackPersistentDialog(dialog);before showing it, so an already-open rename dialog follows View → Frameless Window. I could not drive this path because the full app build hit environment quota. src/gui/TitleBar.cpp:110still claims the design system forbids emoji outright, although the RFC explicitly withdrew that unsupported appeal. State only the rendering/theme reason, and shorten the long historical comments inRadioTabBar.cpp:750andPanadapterStack.cpp:355per AGENTS.md's comment guidance.- Jeremy / @ten9876: decide the disclosed opaque-window default at
src/gui/WindowChrome.h:37, the new permanentappletverb atsrc/core/AutomationServer.cpp:3697, and the changed native-caption approach. The RFC approved a design; these disclosed deviations do not receive approval merely because the code compiles.
Verification and attacks
- Test-boundary preflight passed: added/modified test sources introduce no socket-owning peer. Built only
unified_title_bar_testwith pinned Qt 6.12, Release, in/tmp/pr6198-review; it passes offscreen. - Mutation-checked active-tab truth: removing
applyActiveState()beforeradioActivatedproducesFAIL: re-clicking the active tab leaves it checkedandFAIL: clicking an inactive tab does not check it. Restored the exact source and rebuilt/re-ran the passing head target. This validates genuine behavioural coverage of both active and inactive re-clicks. - Read the hidden/compact tab carrier, overflow wheel mapping, blink-disabled solid alarm, deferred
QPointertab activation, malformed bridge actions and applet side/idempotence paths. The focused test also exercises theme fill/cascade, tab insets, search/actions, overflow and alarm clear. No additional defect was established by these attacks. - Ran the four seam gates on exact merge base and head: 102 engine findings unchanged in signature, zero blocking growth; capability booleans 70→70; raw above-seam commands 369→369; manifests current. Head theme seed and bridge-doc generation checks pass. No checker/baseline weakening in the diff. Automated
code-reviewskill unavailable. - No app/bridge session: the full build stopped before compilation with
AutoGen ... Writing depfile ... failed/ninja: ... Disk quota exceeded. A focused test build succeeded after freeing only this review's artifacts. No existing instance was driven, no hardware was connected, and no runtime persistence/native-pointer/dock-rendering claim is independently verified here. macOS claims remain author evidence; Windows/Linux native gates remain open. - CI is stale relative to current main: earliest run creation on this head was
2026-10-05T06:43:19Z, current main tip334276112is dated2026-10-05T07:51:14Z. Withstrict: false, green checks do not prove the current merge result.
Recommendation: Request changes. Unbundle the unrelated status-bar styling and complete/revise the explicit native-platform gates. The focused titlebar test and mutation check are useful positive evidence, but they cannot replace native interaction checks or Jeremy's outstanding design/public-surface decisions.
ten9876
left a comment
There was a problem hiding this comment.
Reviewed at head cbc48728. I built it on Linux against Qt 6.12.0 and drove it visibly on a Hyprland Wayland desktop at 1.667× fractional scale, connected only to the built-in demo radio (DEMO-0001, isolated settings, TX disabled). That is the Linux gate the body leaves open. The prior agent review was against 93d39e02; I re-checked its two blockers on this head (both fixed, see below) rather than relying on it.
1. Issue fit
This implements approved RFC #4764, and the Qt 6.12 deviation is disclosed honestly. The bar, the tabs with a worded status line, the popover, the get titlebar / titlebar surface and the heartbeat folded into the tab dot are all present and working on Linux. The remaining RFC-level calls are the ones the body already names as maintainer decisions: W1 Snap Layouts, the opaque window, and the applet verb. One correction to the body: it says "Draft.", but isDraft is false, so the PR is open for merge as it stands.
2. Scope
| File / group | Claimed? | Verdict |
|---|---|---|
TitleBar.*, RadioTabBar.*, BrandMark.*, WindowCaptionButtons.*, WindowChrome.h, mac/NativeWindowTitle.* |
Yes | In scope |
MainWindow*.cpp/.h: chrome wiring, tab feed, rename dialog, Windows nativeEvent/#4328 re-anchor removal |
Yes ("our nativeEvent/DWM code (removed)") | In scope |
MainWindow.cpp:1144: FramelessResizer::install(this, 8), which drops topMoveReserve and widens 6 → 8 px on every edge |
Only the top band is mentioned | Undisclosed for the bottom and side edges. See nit 1 |
TitleBar.cpp: removal of the xcb manual-move fallback (#4827) |
No | Blocker 2 |
AutomationServer.*: titlebar verb, get titlebar |
Yes (RFC feature 9) | In scope |
AutomationServer.*: applet verb |
Yes ("please rule") | Needs maintainer decision. New public bridge surface |
AppletPanel.cpp, PanadapterStack.* (dock-flip strip) |
Yes | In scope. The re-realize is macOS-only, as the body says |
ConnectionPanel.*: rename moved to PersistentDialog, selectors |
Yes | In scope. Side effect noted in nit 6 |
Themes, seed, canonical-tokens.md (20 tokens vs the RFC's 15), logo, qrc |
Yes | In scope |
docs/architecture/window-chrome.md, automation-bridge.md, README index, touchpoint manifest (ThemeManager includers 162 → 165) |
Yes | In scope |
tests/tests.cmake, unified_title_bar_test.cpp, titlebar_headphone_mute_test.cpp |
Yes | In scope. Socket-free (step-0 preflight: no socket or peer in any added test source) |
There is no CHANGELOG.md entry, which is correct. Engine boundary: check_engine_boundary.py reports 102 findings on both the merge base and the head, and the per-file diff shows only line shifts. gen_touchpoint_manifest.py --check is clean on both.
The prior review's blockers, re-verified on this head:
- B1 (fixed):
doTitleBar/doAppletPanelnow validate synchronously and run the action throughdeferInvokeAction(AutomationServer.cpp:9245-9314), taking the snapshot first. Driven live: the replies carry"deferred":truewith the pre-action state;applet stateread 1 s later shows the flip (side:right→left,splitterIndex 3→2); unknown actions and missing tab ids fail synchronously ("no radio tab with id 'nope'"). - B2 (fixed):
docs/qt612-window-chrome.mdis gone and its design content is indocs/architecture/window-chrome.md.
3. Blockers
1. src/gui/RadioTabBar.cpp:664-678: after an unexpected link loss, the red "link lost" alarm is painted on whichever radio's tab happens to come first, not on the radio that dropped
if (m_activeId.isEmpty()) {
for (RadioTab* tab : m_tabs) {
if (tab->entry().visibleInTabs) {
return tab;
}
}
}When the session drops, refreshRadioTabs() clears the active id (MainWindow_Session.cpp:210, activeSerial is empty when disconnected). The carrier then falls back to the first visible tab, and onHeartbeatLost() raises the alarm on that tab 4.5 s later. LAN radios are listed first, so on any network with a second radio, the radio you were never connected to is the one that goes red.
Reproduced on the demo, with a FLEX-8600 also on the LAN. Bridge, sim disconnect, then get titlebar once per second:
0.2s alarm False active '' [('2125-1213', 'available', 'Radio FLEX-8600, available')]
3.4s alarm False active '' [('2125-1213', 'available', 'Radio FLEX-8600, available')]
4.4s alarm True active '' [('2125-1213', 'available', 'Radio FLEX-8600, available')]
5.5s alarm True active 'DEMO-0001' [('DEMO-0001', 'connected', ...)]
A grab RadioTabBar at +4.7 s shows the red alarm ring on "FLEX-8600 · available · KK7GWY", while the Simulator tab, which actually dropped, shows a plain grey dot. The body's own pitch for this design is "One dot answers both 'which radio' and 'is its link alive'", and here it answers "which radio" wrongly. An operator with two radios is told that the idle one lost its link.
Fix: keep the carrier on the radio whose link this is until a different session starts. For example:
void RadioTabBar::setActiveRadio(const QString& id)
{
if (m_activeId == id) {
return;
}
if (!id.isEmpty()) {
m_carrierId = id; // the link the heartbeat describes, kept through a drop
}
m_activeId = id;
applyActiveState();
}
RadioTab* RadioTabBar::linkCarrierTab() const
{
const QString& id = m_activeId.isEmpty() ? m_carrierId : m_activeId;
for (RadioTab* tab : m_tabs) {
if (tab->entry().id == id) {
return tab;
}
}
return nullptr;
}This needs a QString m_carrierId; next to m_activeId in RadioTabBar.h:199. Whether a user disconnect should also clear it is your call; clearLinkAlarm() already handles that path. Please also add a unified_title_bar_test case with two tabs, the second one active, then setActiveRadio({}) plus the alarm, asserting that the second tab is the carrier. The current suite passes with this bug present.
2. src/gui/TitleBar.cpp:810-824: the #4827 xcb manual-move fallback was removed, so dragging the title bar is dead again under QT_QPA_PLATFORM=xcb on Mutter/XWayland
startWindowMove() is now only windowHandle()->startSystemMove(). On main (12df69434:src/gui/TitleBar.cpp:580-640), xcb skipped the system move and used a grabbed manual w->move(), because, per FramelessMoveHelper.h:11-14, which is still in the tree:
On xcb, startSystemMove()/startSystemResize() report success but the WM grab is silently dropped under Mutter/XWayland (QTBUG-69716, #4827)
On xcb startSystemMove() returns true, so the new code accepts the press and nothing moves the window. That is the exact symptom #4827 closed ("Click-and-hold on the menu bar to move the window: the cursor changes to the drag hand, but the window never follows the pointer"). git log -S systemMoveResizeUnreliable traces the removal to 0e0cf124, and the body doesn't mention it. FramelessResizer kept its own xcb fallback, so resize still works; only moving the window regressed. xcb is still reachable as the wayland;xcb fallback (main.cpp:299), on plain X11 sessions, and for anyone who sets QT_QPA_PLATFORM=xcb, which #4725 calls the common setup.
Fix: restore main's branch, so that if (FramelessMoveHelper::systemMoveResizeUnreliable(w)) takes the manual move with grabMouse(), continueWindowMove, and finish on release. It only applies on the Linux frameless path now, because macOS and Windows use native captions.
Reasoned from code, not reproduced. I can't generate a real compositor pointer drag here, and Hyprland is not Mutter. The bridge's synthetic pointer verbs don't go through a WM grab either, so they can't prove or disprove this. A one-minute check on GNOME with QT_QPA_PLATFORM=xcb settles it.
4. Nits (non-blocking)
MainWindow.cpp:1144: the 6 → 8 px band widens the existing #4886 exposure. Measured live in a 1400×800 window: the status-bar controls (automationChip,addPanadapterButton, "Cancel transmit", …) span y = 757–798, so 6 px of each now sits inside the bottom resize band, up from about 4 px. Clicks there resize the window instead of activating the control. The pan-menu column at x = 2 loses the same 2 px on the left edge. #6018, which insets the status bar for the 6 px band, is still open and will need to account for 8. Please state the margin change in the body.- Stale comments left by the
topMoveReserve/ #4328 removal:TitleBar.h:34-38(still "MainWindow can pass it as FramelessResizer's topMoveReserve … must stay in lockstep"),FramelessResizer.h:20-21and:58-61("MainWindow reserves TitleBar::kHeight"),MainWindow.h:877-883(a #4328 comment orphaned abovetoggleMinimalModeFromAction()after its declaration was deleted),MainWindow.cpp:9648("under WA_TranslucentBackground, shows the desktop"; the window is opaque now), andPanadapterStack.h:79-80("elsewhere this is just the repaint sweep", althoughrefreshAfterReparent()'s non-mac branch callsresetGpuResources()on every pan per flip). src/gui/WindowGeometryRestore.{h,cpp}is now dead. Nothing insrc/callsclampFrameToWorkAreaorparseSavedWindowGeometry, yet the file is still inCMakeLists.txt:1173, still included byMainWindow.cpp:15, and still pinned bywindow_geometry_restore_test. Delete it with its test, or say it's being kept for the #4557 gate.MainWindow_Session.cpp:502-505: SmartLink tabs outlive a logout.m_smartLinkRadiosis a copy that nothing clears.ConnectionPanelclears its own list (ConnectionPanel.cpp:1280,:2210), but the strip keeps showing those radios as "available", which contradicts the comment at:225("the strip and the picker can never disagree"). The tab-click fallback text handles the click, but not the false status.- The link alarm is colour-only to a screen reader. While
linkAlarmwastrue(the 5.5–6.6 s rows above), the carrier tab's accessible name read"Radio Simulator (not on the air), connected". The PR advertises the status line as WCAG 1.4.1 words, but the one state the dot exists to show has no words. Given #4896, I'd add "link lost" to the status text and accessible name whilem_alarmis set. - The demo's "not on the air" label is now user-overridable.
addDemoRadio()now runs the label throughradioDisplayName(), andget titlebarreports"canRename": trueforDEMO-0001.mainhardcoded the nickname because "the nickname carries the unmistakable 'not on the air' label". The model ("AetherSDR Demo") still appears on line two once renamed, so this is low-risk, but it is a deliberate safety label becoming editable. Either exclude the sim family incanRenameRadio()or say it's intended. applet stateis refused in observe-only mode.isReadOnlyRequest()(AutomationServer.cpp:2720) has no entry for it, although the doc and the handler both call it read-only.- Nothing pins the deferral contract. No test covers
titlebar/appletreturningdeferredplus the pre-action snapshot. A socket-freeAutomationServerhandler test would keep B1 from silently regressing.
5. CI and evidence
- All five checks are green, but the runs were created 06:43Z and
mainmoved at 07:51Z (#6173, #6176: sanitizer lane and WDSP; no file overlap with this PR). The green describes a merge base that is no longermain's tip. - Built and ran locally (Linux, Qt 6.12.0, offscreen):
unified_title_bar_test,titlebar_headphone_mute_test,hl2_pc_audio_lock_test,mixer_control_availability_test,frameless_resizer_testandwindow_geometry_restore_testall pass.connection_panel_size_testfails one check ("horizontal overflow is reachable"), and it fails identically on the merge base, so that is my environment and not this PR. - Mutation: removing
m_dockSep->setVisible(!on)(TitleBar.cpp:1545) makesunified_title_bar_testfail withminimal mode leaves no dangling separator (got 1, want 0). Restoring the line (sha256 matchesgit show HEAD:) makes it pass again.
6. What I tried to break that held
- No false alarm after a user disconnect (body claim):
disconnect, then 8 s ofget titlebarshowedlinkAlarm falsethroughout. It holds. - Unexpected loss raises and clears the alarm: it raised at 4.4 s and cleared about 2 s after the auto-reconnect. The timing matches the body; only the tab is wrong (blocker 1).
- Tabs clear the 8 px top band: live
screenRecty = 8, h = 36 in a 52 px bar. No interactive bar child starts in the top 8 px except drag-handle labels and the caption container, whose 36 px chips sit at 8–44. - Dock flip on Linux:
applet dock left/righton the demo. Agrab MainWindowafter each flip shows no unpainted strip. refreshAfterReparent"back to macOS-only, as on main": the function body is byte-identical to the merge base.- The
showDiscoverypopover opens on the deferred turn (popoverVisible trueone second later). - Opaque window: no orphaned backdrop-alpha setting or menu remains. Only the stale comment in nit 2.
Linux gate status (RFC decision 2): the bar, caption chips, tab insets, dock flip, popover and link indicator all render and work on Wayland at 1.667×. Not proven: drag jitter and real-pointer resize feel, because I have no way to inject a real pointer here, so that part of the gate stays with a human. One environment note so nobody chases it: on Hyprland the floating window's Qt state reported maximized while the compositor kept it at 1400×800, which suppresses FramelessResizer edges. That is a compositor interaction I did not compare against main, and I'm not counting it against this PR.
7. Recommendation
Request changes for blockers 1 and 2. Both have small, local fixes. The maintainer decisions the body lists (the applet verb, the opaque window, W1 Snap Layouts, 20 tokens vs 15) are still open and independent of these. The Windows and macOS M1 gates are untouched by this review.
…cb move, scope. Principle XI. Link carrier (blocker): after an unexpected drop the active id goes empty and the link carrier fell back to the first visible tab, so on a two-radio LAN the idle radio went red. RadioTabBar remembers the last session's radio (m_carrierId) and keeps the link there; a fallback carrier (no session yet, or the dropped radio's tab hidden) never shows the alarm. While alarmed the tab says "link lost" on line two and in its accessible name (WCAG 1.4.1). New unified_title_bar_test case fails on the old carrier logic. xcb window move (blocker, aethersdr#4827): restore main's manual-move fallback — startSystemMove() reports success on xcb but Mutter/XWayland drops the WM grab. Only the Linux frameless path reaches it now. Scope: revert the status-bar restyle hunks (station border, separators, indicators, automation chip, FFT glyph, CPU/TX colours) to main; they are not part of the title-bar RFC. Restore main's FramelessResizer install (6 px, topMoveReserve = TitleBar::kHeight, aethersdr#4886) instead of a silent 8 px band with top-edge resize. PanadapterStack::refreshAfterLayoutShift() re-realizes only on macOS — elsewhere it no longer resets GPU resources on every dock flip. Nits: rename dialog registered with trackPersistentDialog so it follows View → Frameless Window; the demo's "not on the air" name is not renamable again (main's behaviour); SmartLink tabs drop on sign-out; `applet state` allowed in observe-only mode; deferral + observe-only contract pinned in automation_boundary_core_test; emoji comment no longer cites a rule; long comments shortened; stale aethersdr#4328 / translucency comments removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`/pr-review` and `/pr-land` both tell the agent to build the PR head. The obvious way to do that builds the default `all` target, which is the whole tree. This PR changes both skills to configure a lean build and build only the targets the review actually runs. ## Why: what a full build costs Measured while reviewing #6195 and #6198. Each head was built on Linux (GCC, Qt 6.12.0, `RelWithDebInfo`, static core: the default configuration): | | Full tree (default `all`) | |---|---| | Build directory | **84 GB per PR** (two PRs plus one merge base: ~172 GB) | | Of which, top-level executables | 83.7 GB | | Test executables | ~628; 232 of them over 100 MB each | | `AetherSDR` | 832 MB, of which ~680 MB is `.debug_*` sections and ~137 MB code and data | | Largest tests | `radio_setup_*_test` ~397 MB each, `weather_radar_loading_test` 408 MB | The review itself ran about twenty of those tests. Two causes: 1. **The static core is copied into every test.** Each executable that links `aethercore` absorbs a full static copy. The `AETHER_SHARED_CORE` comment in `CMakeLists.txt` already records this ("~200 MB in a Debug ASan build … shared, they average ~13 MB"); the sanitizer lane switched to a shared core for exactly this reason. 2. **Full debug info per binary.** `-g` under RelWithDebInfo makes debug info about 5× the size of the code. That ratio repeats across every binary. ## What changes **`/pr-review` step 7**, new "Build only what you run" paragraph: ```sh cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DAETHER_SHARED_CORE=ON \ "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG" \ "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O2 -g1 -DNDEBUG" cmake --build build --parallel --target AetherSDR <test_a> <test_b> ctest --test-dir build -R '^(test_a|test_b)$' --output-on-failure ``` - **Choosing tests:** the ones the PR adds or modifies, the ones its body names, and the ones whose `tests/tests.cmake` block compiles a file the PR touches. - **Full-tree builds** are kept for claims about the tree itself (CMake or link-contract changes), and the report must say when one was done. - **The merge-base comparison** ("before blaming the PR") builds the same targets instead of a clean full tree. - **Caveats, stated in the skill:** - Linux and macOS only. `AETHER_SHARED_CORE` is a configure error on Windows, and `-g1` is a GCC/Clang flag. - The app loads the core as a shared library, which isn't the shipped shape. Fine for driving behaviour; a finding that could depend on link mode should say so. - A test that calls WDSP directly can't rely on the shared core exporting it (WDSP is a private, hidden-visibility dependency). It links `aether_wdsp` explicitly or goes through `WdspChannel.h`; no static reconfigure is needed. - `-R` matches the `add_test(NAME …)` name, which can differ from the target name, so both recipes pass `--no-tests=error`. A filter that matches nothing fails instead of passing silently. - One binary can also be registered under several names (`deepfist_cw_model_test` five times; #6208 adds `rtl_capture_worker_slow_device_test` on `rtl_capture_worker_test`). A filter built from the target name runs only one, and `--no-tests=error` can't notice, so the names come from `grep -n 'COMMAND <target>' tests/tests.cmake`. - Test selection follows an `OBJECT` library or `…_SOURCES` list to its consumers. After #6217 the logging sources appear once in `tests.cmake`, and files compiled into `aethercore` reach every test that links it. - An unfiltered `ctest` reports unbuilt tests as "Not Run". That means the `-R` filter is missing, not that a test failed. **`/pr-land` step 5:** the old `cmake --build build --parallel` (full tree) becomes the same lean configuration and named targets. The selection also includes the tests covering the findings being fixed. ## Evidence for the lean recipe Same machine and toolchain, on `main` at this branch's base. Built `AetherSDR` plus `radio_setup_label_theme_token_test`, `sim_backend_test` and `frameless_resizer_test`: | | Full tree (before) | Lean build (this PR) | |---|---|---| | Build directory | 84 GB | **1.3 GB** | | `AetherSDR` | 832 MB | 144 MB (+ 134 MB `libaethercore.so`, shared by all targets) | | `radio_setup_label_theme_token_test` | 397 MB | **18 MB** | | Cold build, all ccache misses | — | 219 s | - `ldd build/AetherSDR` resolves `libaethercore.so` from the build tree, so the shared core really is the one in use. - `ctest -R '^(radio_setup_label_theme_token_test|sim_backend_test|frameless_resizer_test)$'`: 100% of 3 passed. For reference, a second worktree at the same commit, building the same targets, took **41 s with a 99.4% ccache hit rate** (1094/1101). That needs a local ccache setting (`base_dir` set and `hash_dir=false`, so absolute worktree paths don't split the cache key). It is a per-machine choice, so it is not in the skill text. ## Also: a test that tested nothing under `-DNDEBUG` `tests/digital_voice_mode_registry_test.c` does all of its checking inside plain `assert()`, with the calls under test inside them. RelWithDebInfo and Release define `NDEBUG`, so there it compiled to an empty `main` and passed whatever the registry did. That covers CMake's default RelWithDebInfo flags, CI's build and TSan lanes, and this PR's recipe; only the Debug (ASan) lane ever ran it. `#undef NDEBUG` before `<assert.h>` fixes it, since `<assert.h>` re-reads `NDEBUG` on each inclusion. Built at `-O2 -g1 -DNDEBUG`, with the first assertion inverted on purpose: | | Unmodified test | First assertion inverted | |---|---|---| | Before | 100% passed | **100% passed** (the check never ran) | | After | 100% passed | **0% passed, 1 failed** | It is the only test that includes `<assert.h>` without handling `NDEBUG`. The automated code-review pass found it while checking this PR's recipe, and it is folded in here by maintainer decision. ## Not in this PR - **Per-test recompiles:** #6217 compiles the settings store and logging sources once; the widget sources (`DragValuePopup.cpp` ×40 and others) remain open in #6214. - **No build-logic change.** `CMakeLists.txt` and `BUILD-OPTIONS.md` change only `AETHER_SHARED_CORE`'s help string, comment and table row: it now names local review builds as well as the sanitizer lane. `tests/tests.cmake`, CI and release builds are untouched, and shipping builds keep the static core. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r. Principle XI. An inline QMenuBar does not sit on the 52 px bar's centre line, so on platforms without a native menu bar its menus now live behind a hamburger button ahead of the brand mark (Firefox-style, but on the left). The QMenuBar stays as a hidden child so MainWindow keeps building into it and the automation bridge still resolves actions through it; menus added after the hand-off are mirrored. macOS's native menu bar is untouched. The button opens the menu with popup(), not a button-owned menu's exec(), so no nested event loop re-enters discovery, socket or timer callbacks while a menu is open. Menu shortcuts keep firing because the visible button carries the menu's action, and each menu's Alt+<letter> mnemonic is re-registered to pop that menu under the button, since the hidden bar's own mnemonics never match. The glyph carries 1x/2x/3x pixmaps so it stays crisp across screens. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ar's vocabulary. Principle VIII. Closes #6249. RFC #6226 (style guide), #6198 (title bar). Structure - Header modelled on a title-bar radio tab: the title in ink over a status line led by a status dot, then a hairline. The status line moves up from the bottom of the panel, reserves two lines so a wrapped refusal never shifts the slots, and reads like the title bar's subline ("Previewing · slot 3 · 0.4s of 4.2s", "Play failed (slot 3): …"). - Slot rows are 8 px canon.nested cards with a canon.line hairline; hover brightens the edge, and the selected row takes the title bar's active-tab pair (titlebar.tab.active.*). The row rule is scoped by property, so it no longer leaks into its QLabels (QLabel is a QFrame), which drew every name and length as a box inside the box. - F-keys are 22 px key caps and the transport buttons are 24 px, both on the canon secondary-control recipe (control fill, lineHi edge, cyan text, aqua hover and focus, 4 px corners). - Controls that cannot act are disabled: STOP while idle, PLAY and PREV on an empty slot, and the other start controls while one runs. - The slot menu is the title bar's rounded menu; the title bar's helper and rules move to RoundedMenu.{h,cpp} so both stay identical. Disabled items get canon.muted in the DVK menu, which routinely disables Clear/Export. - The rename field gets a control fill and a cyan edge. States (provisional, to be adjusted after review) - On air (PLAY, the playing slot's F-key, its progress bar and the status dot) uses the MOX family: background.tx, tx.mox.border, tx.mox.text. - Recording and preview use the canon cyan active treatment. - Refusals use accent.danger and always say "failed". - No red elsewhere: the canon keeps red for transmit indicators. One tracked stylesheet on the panel resolves every colour from color.canon.*, color.titlebar.*, the MOX tokens and accent.danger; widgets carry state as properties. All 11 colour literals are gone (audit ratchet: -5 unique colours, -35 references, -12 setStyleSheet calls). Tests: dvk_panel_theme_test (33 cases) drives a real DvkPanel from fw 4.2.20 status lines. It checks that every token resolves in Default Dark and Default Light, that there are no colour literals, and that each state (selected, empty, preview, on air, recording, refusal) lands on the right widget without moving the slots. Set DVK_PANEL_GRAB_DIR to save a PNG of each state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ar's vocabulary. Principle VIII. Closes #6249. RFC #6226 (style guide), #6198 (title bar). Structure - Header modelled on a title-bar radio tab: the title in ink over a status line led by a status dot, then a hairline. The status line moves up from the bottom of the panel, reserves two lines so a wrapped refusal never shifts the slots, and reads like the title bar's subline ("Previewing · slot 3 · 0.4s of 4.2s", "Play failed (slot 3): …"). - Slot rows are 8 px canon.nested cards with a canon.line hairline; hover brightens the edge, and the selected row takes the title bar's active-tab pair (titlebar.tab.active.*). The row rule is scoped by property, so it no longer leaks into its QLabels (QLabel is a QFrame), which drew every name and length as a box inside the box. - F-keys are 22 px key caps and the transport buttons are 24 px, both on the canon secondary-control recipe (control fill, lineHi edge, cyan text, aqua hover and focus, 4 px corners). - Controls that cannot act are disabled: STOP while idle, PLAY and PREV on an empty slot, and the other start controls while one runs. - The slot menu is the title bar's rounded menu; the title bar's helper and rules move to RoundedMenu.{h,cpp} so both stay identical. Disabled items get canon.muted in the DVK menu, which routinely disables Clear/Export. - The rename field gets a control fill and a cyan edge. States (provisional, to be adjusted after review) - On air (PLAY, the playing slot's F-key, its progress bar and the status dot) uses the MOX family: background.tx, tx.mox.border, tx.mox.text. - Recording and preview use the canon cyan active treatment. - Refusals use accent.danger and always say "failed". - No red elsewhere: the canon keeps red for transmit indicators. One tracked stylesheet on the panel resolves every colour from color.canon.*, color.titlebar.*, the MOX tokens and accent.danger; widgets carry state as properties. All 11 colour literals are gone (audit ratchet: -5 unique colours, -35 references, -12 setStyleSheet calls). Tests: dvk_panel_theme_test (33 cases) drives a real DvkPanel from fw 4.2.20 status lines. It checks that every token resolves in Default Dark and Default Light, that there are no colour literals, and that each state (selected, empty, preview, on air, recording, refusal) lands on the right widget without moving the slots. Set DVK_PANEL_GRAB_DIR to save a PNG of each state. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ar's vocabulary. Principle VIII. Closes #6249. RFC #6226 (style guide), #6198 (title bar). Structure - Header modelled on a title-bar radio tab: the title in ink over a status line led by a status dot, then a hairline. The status line moves up from the bottom of the panel, reserves two lines so a wrapped refusal never shifts the slots, and reads like the title bar's subline ("Previewing · slot 3 · 0.4s of 4.2s", "Play failed (slot 3): …"). - Slot rows are 8 px canon.nested cards with a canon.line hairline; hover brightens the edge, and the selected row takes the title bar's active-tab pair (titlebar.tab.active.*). The row rule is scoped by property, so it no longer leaks into its QLabels (QLabel is a QFrame), which drew every name and length as a box inside the box. - F-keys are 22 px key caps and the transport buttons are 24 px, both on the canon secondary-control recipe (control fill, lineHi edge, cyan text, aqua hover and focus, 4 px corners). - Controls that cannot act are disabled: STOP while idle, PLAY and PREV on an empty slot, and the other start controls while one runs. - The slot menu is the title bar's rounded menu; the title bar's helper and rules move to RoundedMenu.{h,cpp} so both stay identical. Disabled items get canon.muted in the DVK menu, which routinely disables Clear/Export. - The rename field gets a control fill and a cyan edge. States (provisional, to be adjusted after review) - On air (PLAY, the playing slot's F-key, its progress bar and the status dot) uses the MOX family: background.tx, tx.mox.border, tx.mox.text. - Recording and preview use the canon cyan active treatment. - Refusals use accent.danger and always say "failed". - No red elsewhere: the canon keeps red for transmit indicators. One tracked stylesheet on the panel resolves every colour from color.canon.*, color.titlebar.*, the MOX tokens and accent.danger; widgets carry state as properties. All 11 colour literals are gone (audit ratchet: -5 unique colours, -35 references, -12 setStyleSheet calls). Tests: dvk_panel_theme_test (33 cases) drives a real DvkPanel from fw 4.2.20 status lines. It checks that every token resolves in Default Dark and Default Light, that there are no colour literals, and that each state (selected, empty, preview, on air, recording, refusal) lands on the right widget without moving the slots. Set DVK_PANEL_GRAB_DIR to save a PNG of each state. Squashed-from: #6251 Co-authored-by: Jeremy [KK7GWY] <105957118+ten9876@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…behaviour. Principle VIII. (#6250) ## Summary Found while refreshing the GitHub wiki for v26.5.1 → v26.10.1: in-repo text that no longer matches the app. Each item was checked against current source before changing it. **In-app help (`resources/help/`)** - `aethersdr-help.md`: Title Bar section rewritten for the 52 px bar (#6198) — radio tabs with the heartbeat on the status dot, ☰ menu, `+` popover; no heartbeat lamp, lightbulb or Minimal Mode button. Tools → AetherTX… replaces Aetherial Audio; DIGI applet, overlay DSP button and status-bar applet ☰ removed. Default applet order now matches `AppletPanel::kDefaultOrder` (23 IDs, `PHNE`). - `understanding-noise-cancellation.md`: BNR runs in-process on a local NVIDIA GPU (no Docker/container, not cloud), with a ~1 GB one-time download matching the Download button (`docs/nvidia-bnr.md` aligned too); NNR added as the seventh client method; RN2 Noise Floor slider; controls live in Settings → AetherRX… → AetherNR; unavailable methods are dimmed with a reason. - `configuring-aethersdr-controls.md`: a single click on an off-screen indicator activates and recentres (#4795). **Docs** - README: APRS has a station *table*, not a map; the lightbulb line points to Help → Submit your Idea… 💡; TMate 2, Ulanzi Dial and AetherPad added to Tested Controller Devices. - CONTRIBUTING: Help-menu idea route, per-launch logs under `<config>/AetherSDR/logs/` (attach the newest; `aethersdr.log` links to it on Linux/macOS), Help → Support & Diagnostics… / File an Issue…. - BUILDING: QRhi failure shows the "Spectrum renderer unavailable" card (#4787). - `psk-reporter-city-lights.md`: Warmth default is 0 throughout (`kDefaultWarmth`). - `rtty-decoder-implementation.md`: the pane opens for RTTY only. - `DEVELOPER-GUIDE.md`: theme tokens instead of hard-coded hex, per AGENTS.md. - `docs/agents/gui.md`: naming examples use the CAT applet (`CatControlApplet`) and the current Support & Diagnostics log category labels. **UI strings** - Automation-bridge Access Token tooltip now agrees with `docs/automation-bridge.md` (export `AETHER_MCP_TOKEN` only in the launching shell, never in an MCP config). All four bridge tooltips on Radio Setup → Network link the hosted doc rather than a source-tree path. - Ulanzi mapper fallback no longer says "Linux build only" (backends exist on all three platforms). - File an Issue and Submit your Idea AI prompts fetch `AGENTS.md` instead of `CLAUDE.md`. **Packaging** - `60-hid-encoders.rules`: TMate 2 (`1721:0614`, from `kSupportedDevices` in `HidDeviceParser.cpp`), and a note that AetherPad (`2341:0266`) is deliberately absent because it is the Arduino Giga R1's stock identity. ## Testing - AetherSDR + 7 related test targets built (lean build, Qt 6.12); `radio_setup_label_theme_token_test`, `radio_setup_recording_mode_dim_test`, `radio_setup_max_power_field_test`, `radio_setup_region_field_test`, `hl2_pc_audio_lock_test`, `titlebar_headphone_mute_test`, `automation_menu_lookup_test` pass. - All Static-checks scripts pass, incl. `gen_bridge_docs.py --check`; colour ratchet +0; a11y warnings unchanged from base. - No test or translation pins any changed string. - Not run: the full suite or a live app launch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ple VIII. The unified title bar (#6198) rendered each radio tab's state in the same all-lower-case spelling the automation bridge publishes: `connected`, `available`, `in use`, and `link lost` under the link alarm. On the tab, in its tooltip and in its accessible name that text is prose an operator reads, and prose starts with a capital. Capitalizing `radioTabStatusText()` itself was not an option: docs/automation-bridge.md pins `radios.tabs[].status` to the lower-case token, so every driver matching on it would have broken. The two spellings are now separate functions — `radioTabStatusText()` stays the untranslated machine token, and a new `radioTabStatusLabel()` returns the translated sentence-case label. Only the first letter is capitalized, so the two-word state is `In use`, not `In Use`. `RadioTab::statusWord()` and the discovery popover's rows (text, tooltip, accessible name) take the label; `state()`'s `status` fields keep the token. The switcher's search text carries both, so filtering still matches a typed query against a translated label. Evidence (Principle VIII), with no cmake on this machine: - tests/unified_title_bar_test.cpp asserts the new display casing on the status line, the accessible name and description, and on both ends of the link alarm, plus — the point of the split — that `state()` still reports `status:"connected"` / `"in use"` while `statusLine` reads `Connected`. A negative assertion pins `In use` against `In Use`. Six assertions in that file were on the old lower-case spelling and are updated; a repo-wide sweep for the prose forms ("link lost", "· connected", `statusLine`, `statusWord`) found no other consumer in tests, tools, resources or the in-app help — only two prose mentions in docs/architecture/window-chrome.md and one comment in src/gui/MainWindow.h, both corrected here. - Both touched translation units pass `clang++ -fsyntax-only -std=c++20` against Qt 6.11.2 headers. The test binary itself was NOT built or run locally; CI is the verification of record. - tools/gen_bridge_docs.py --check, tools/gen_touchpoint_manifest.py --check and tools/check_test_registration.py all pass; tools/check_a11y.py reports no finding in RadioTabBar.cpp. Blast radius: not run — `radioTabStatusLabel()` is a new free function in the RadioTabBar.h/.cpp pair with three in-file call sites, and no symbol moved between translation units, so no test target gains a link dependency. CodeGuard: no credential, crypto, certificate, parsing or storage surface is touched; the C/C++ memory-safety rule finds nothing to apply — the change is QString-only with no raw buffers, and no banned str*/mem* function is introduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t/dark setting (aethersdr#6266). Principle VIII. Since aethersdr#6198 handed the Windows frame to Qt's expanded client area, the left, right and bottom edges are drawn by DWM, coloured from the OS light/dark preference. A dark AetherSDR theme on a light-mode Windows 10 install showed a light strip on those three edges. - ThemeManager sets Qt's colour scheme from color.background.app on every theme switch and on live edits of that token. Qt's Windows plugin turns that into DWMWA_USE_IMMERSIVE_DARK_MODE (Windows 10 1809+). - On Windows 11, MainWindow also sets DWMWA_BORDER_COLOR to color.background.app on show and on every theme change, so the border matches the app exactly. dwmapi is linked on Windows only. No nativeEvent or hit-test changes; aethersdr#6224 decides that seam. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nded chrome (#6266). Principle VIII. (#6269) ## Summary Fixes #6266. On Windows 10 with Frameless Window on, a light 1–2 px strip ran along the left, right and bottom edges of the main window. It started with #6198. **Cause:** under `Qt::ExpandedClientAreaHint`, Qt 6.12's `QWindowsGeometryHint::handleCalculateSize` takes over the top caption, but keeps a resize-border inset on the left, right and bottom as non-client. Windows 11 draws that area invisibly. Windows 10 paints it as a flat light strip, whatever the OS colour scheme or accent settings are. **Changes:** - **`MainWindow::nativeEvent` (Windows 10 only):** answers `WM_NCCALCSIZE` with the whole window as client area when `WindowChrome::claimsWholeWindowAsClient()` holds — `ExpandedClientAreaHint` set and the OS older than Windows 11 (build 22000). While maximized (not full screen), it insets the sides by `SM_CXSIZEFRAME + SM_CXPADDEDBORDER` and the top and bottom by `SM_CYSIZEFRAME + SM_CXPADDEDBORDER`, at the window's DPI, so nothing sits past the screen edge. No `WM_NCHITTEST` code is added: Qt's `handleNonClientHitTest` already returns `HTLEFT`/`HTRIGHT`/`HTBOTTOM`/corners from the client edges, which now are the window edges. - **`MainWindow::applyWindowsFrameColor` (Windows 11 only):** sets `DWMWA_BORDER_COLOR` to `color.background.app` on show and on every `themeChanged`, with Frameless Window on **or off**, so the DWM border follows the theme. `dwmapi` is linked on Windows only. - **Window geometry:** the native client-rect save/restore from #6303 (#6310) runs after this handler has taken effect. `applyWindowsCaptionStyles()`'s `SWP_FRAMECHANGED` in `showEvent` routes `WM_NCCALCSIZE` through it before `setFramelessWindow()` and the startup / minimal-mode re-apply measure the frame, so on Windows 10 the measured frame is zero and a saved client rect is restored as the window rect exactly. A comment at that call names the dependency. - `docs/architecture/window-chrome.md` describes both, and that edge resize on this path rests on Qt 6.12's hit-test alone. macOS and Linux are untouched. On Windows 10, Frameless Window off is untouched. Snap Layouts (#6224) is not addressed. ## Constitution principle honored Principle VIII: the window edge is continuous with the app's chrome on all four sides. ## Test plan - [x] Windows 10 22H2 (19045), FLEX-6600, dark theme, Frameless Window on: the strip is gone on the left, right and bottom (bridge luminance readback in the PR thread). - [x] Windows 10: resize from all four edges and all four corners; maximize and restore; Win+Up and Aero Snap (checked by hand). - [x] Windows 10: Frameless Window off is unchanged (checked by hand). - [x] `unified_title_bar_test` (any host): the engage predicate is on for Windows 10 under the expanded chrome, off for Windows 11 build 22000 and off for Frameless off; a zero frame restores a client rect as the window rect. Both predicate checks fail when the predicate is mutated. - [ ] Windows 10, after the rebase onto #6310: window position and size across Frameless off → on → off, three relaunch cycles, a maximized relaunch then un-maximize, and View → Minimal Mode twice. - [ ] Windows 11: border follows `color.background.app` and updates when the theme changes. - [ ] CI (`check-windows` is the compile proof for the Windows-only lines). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: CJ Johnson <chibondking@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
…ow on (#6272). Principle VIII. (#6328) Fixes #6272. ## What On Windows with **Frameless Window on**, the main window's ☰ menu and the panadapter's right-click menu flashed open and closed immediately, and double-click to tune did nothing. All three work with Frameless off. This regressed with the unified title bar (#6198) and is still on `main` after #6269. ## Root cause With Frameless on, the main window uses Qt's expanded client area with `CustomizeWindowHint` and **no** title or caption-button hints (`WindowChrome::chromeFlags`). For exactly those flags, Qt 6.12 answers `WM_NCHITTEST` in the `else if (CustomizeWindowHint)` branch of `QWindowsWindow::handleNonClientHitTest` (`qwindowswindow.cpp:3415-3427`): - It reads the live mouse buttons with `GetAsyncKeyState`. - Whenever they differ from the last hit test (`oldMouseButtonState`, a **function-static shared by every window**), it **synthesizes a mouse press or release** with `QWindowSystemInterface::handleMouseEvent<SynchronousDelivery>` at the cursor, anywhere in the window. Windows sends `WM_NCHITTEST` around a real click, so the click arrives twice: - The ☰ menu opens on the first press (`popup()` on `pressed`), the second press lands outside it and dismisses it, and the replay guard stops it reopening. - The panadapter's `exec()` context menu is closed the same way. - The extra press/release pairs break double-click detection, so double-click to tune never fires. This also explains two puzzles from the reproduction on the issue: - **The agent automation bridge's own click verbs never reproduced it:** they deliver Qt events directly and never cause a `WM_NCHITTEST`. - **One earlier real-click run passed:** the shared static makes the outcome depend on hit-test timing. ## How `QWindowsContext` runs the window's own `nativeEvent()` before its hit-test switch (`qwindowscontext.cpp:858` vs `:1019`). Under the expanded client area, `MainWindow::nativeEvent()` now answers `WM_NCHITTEST` itself, with no synthetic events: - the resize codes (`HTLEFT` … `HTBOTTOMRIGHT`) on a band of `SM_CXSIZEFRAME + SM_CXPADDEDBORDER` at the window edges, the same band Qt used; - no band while maximized or fullscreen; - `HTCLIENT` everywhere else. `WindowChrome::expandedFrameHit()` is the pure part. `window-chrome.md` now says edge resize on this path rests on this handler. It applies on Windows 10 and 11 alike, since the Qt branch doesn't depend on the OS version. **Popped-out panadapters** use plain `Qt::FramelessWindowHint`, which Qt hit-tests on its ordinary path. They aren't affected and aren't changed (proven below). +114 / −6 in 4 files; every behaviour change is inside `#ifdef Q_OS_WIN`. ## Testing ### Proof with the agent automation bridge, using real OS input Run on Windows 10 22H2 (Intel HD 520), FLEX-6600 connected, AetherSDR UI scale 85%, your real settings with Frameless Window on. Builds came from the fork's Windows Installer workflow (Qt 6.12): - **Before:** `main` `e1785e78`, run 37902283037. - **After:** this branch `1c51a580`, run 37902771818. Method: - Clicks are **`SendInput`** at positions from `dump_tree`, with physical = Qt logical × 0.85. - Popup detection: the process's visible `Qt…PopupSaveBits` windows, 300 ms after each click. - Tune detection: the slice frequency read back through the bridge after each double-click. - The popped-out pan was made with `pan float` and targeted by its window title. Its click points sit below the slice flag; my first popped-out run was invalid because the click landed on the flag. | Target | Action | Before (main) | After (this PR) | |---|---|---|---| | Main window | ☰ menu stays open | **0 / 5** | **5 / 5** | | Main window | Panadapter right-click menu stays open | **0 / 5** | **5 / 5** | | Main window | Double-click tunes the slice | **0 / 5** | **5 / 5** | | Popped-out pan | Right-click menu stays open | 5 / 5 | 5 / 5 | | Popped-out pan | Double-click tunes the slice | 5 / 5 | 5 / 5 | **Edge resize still works on the fix build.** Real `SendInput` drags, with the window rect from `GetWindowRect`: | Edge | `WM_NCHITTEST` there | Drag | Window before → after | |---|---|---|---| | Bottom | 15 (`HTBOTTOM`) | down 30 | 1240×451 → **1240×481** | | Bottom | 15 | up 30 | 1240×481 → **1240×451** | | Left | 10 (`HTLEFT`) | left 30 | 327 x, 1210 w → **297 x, 1240 w** | | Left | 10 | right 30 | 297 x, 1240 w → **327 x, 1210 w** | Three earlier drags didn't resize because the press landed on another window (the floating pan, then the floating TX Controls). `WindowFromPoint` confirmed that, and those drags are excluded. ### Unit test (local, Qt 6.12.0 pinned, MSVC 2022, offscreen) The new `expandedFrameHit` checks in `unified_title_bar_test` all pass: - client area at the ☰ corner, mid-window, and one pixel past the band; - every edge and corner; - no band while maximized; - a point outside the window. **Mutation-checked:** - An off-by-one on the band fails "the first pixel past the band is client area". - An always-client answer fails every edge and corner check. One existing check in the same binary, *"four radios fit whole in an 1800 px bar"*, fails in my local Windows offscreen run **with and without this change** (also on plain `main`). CI on `main` is green. ### Not verified - **Windows 11.** The Qt branch is OS-independent, but only Windows 10 was available. - **Snap Layouts** (#6224). Unchanged: nothing returned `HTMAXBUTTON` before either. - **Top-edge resize** under the title bar. The band covers the top too, as Qt's did; only the bottom and left edges were dragged. ### Agent automation bridge: gaps found - **Click verbs that go through Windows.** The bridge's `clickAt`, `rightClick` and `doubleClick` deliver Qt events and skip `WM_NCHITTEST`, so they can't reproduce input bugs below Qt. A real-input mode (`SendInput` at a widget's centre) would let the bridge prove this class of bug on its own. - **Non-ASCII targets.** The MCP wrapper garbles non-ASCII target names (`Theme Editor…`); plain tooltips are a workaround. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: CJ Johnson <chibondking@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#6230) Refs #6229, Refs #5198. ## Limits first - **Nothing was keyed for this PR and nothing was observed on a radio.** The evidence is 30 existing captures of one radio and socket-free tests that replay three of them. - **The guard rests on one event.** One forward-power word in 293 211 is the defect. The marker catches it: 1 of 1, one unit. - **The marker is incomplete.** 61 responses at key-on are not measurements; 54 carry a temperature word of 0 and are skipped. The other 7 carry a wrong non-zero temperature (512 to 1007) and still pass. Their forward words are 0 to 4, so nothing shows here. A high forward word in such a response would still be displayed. - **It does not explain the gauge pinned at 6 W on #5198.** No word in the captures reaches the gauge top. - **This corrects my own #6019.** The window maximum keeps every sample, so it also keeps one that is not a measurement. - The cause in the radio is not established. Other units, other gateware versions, Linux and Windows hosts are not measured. - Only the forward-power window skips a marked response. It still reaches the last-value fields. If the 10 Hz publish lands while it is the last value (1.3 to 2.0 ms per key-on in the three replayed captures), the raw temperature row shows 0 for one publish and the smoothed PA temperature steps toward −50 °C through its α = 0.2 pole: 18 °C down from 40 °C, 5.9 °C still missing five publishes later, under 1 °C after 13. The forward word 2944 can likewise be the last value for 661 µs and reach the instantaneous forward row and the SWR pair. Whether to gate these at the receive loop instead is open for the maintainer (#6229 triage). - The new fixture header is 64 KB of recorded words. ## What happens For about 2 ms after the T/R switch-over, the radio's slow-ADC words in RADDR 1 and RADDR 2 are not measurements (#6229 has the counts). In one of 34 recorded key-ons the forward word was 2944 counts on a carrier of 1649. `ForwardPowerWindow` took it as the window maximum and the peak hold showed 3.82 W on a 1.23 W carrier, falling over 3 s. ## The change `ForwardPowerWindow::observe` skips a non-ACK RADDR-1 response whose temperature word, DATA[31:16], is 0. The rule is the free predicate `isNonMeasurementRaddr1()` in `MetisProtocol.h`. The sensor formula puts 0 °C at 0.5 V, so 0 counts is 0 V, which no working sensor reads. The window counts what it skips; the count rides the telemetry emit as `forwardPowerSkipped` and the backend publishes its session total as one Radio Health row, `forwardPowerSkippedTotal`. Four source files: `MetisProtocol.h`, `MetisClient.cpp`, `Hl2Backend.h`, `Hl2Backend.cpp`. - No timer and no threshold: the marker is in the same response as the forward word. - A temperature word of 0 occurs in 54 responses at 27 of 34 key-ons, 1.74 to 4.05 ms after the first EP2 frame with MOX, and in none of 292 842 responses elsewhere, nor in 516 820 receive-only ones. - The skipped responses come 16 ms or more before the first RF sample (20.9 ms on TUNE), so no real sample is lost. - If every response of a window is skipped (a unit whose temperature channel reads 0), the peak is `nullopt` and `publishTelemetry` falls back to the last value, as before #6019: low on speech, never high. `forwardPowerSkippedTotal` then rises all the time, which is how that unit shows. - `forwardPowerRaw`, SWR, temperature, reverse power, bias and the existing Radio Health rows are unchanged. One row is new: `forwardPowerSkippedTotal`, "Forward responses skipped, not measurements (since connect)", absent until a RADDR-1 response arrived. The `HL2 directional` debug line prints the window's skipped count. Nothing else above the backend seam, no capability, no setting. Options not built: a wider marker (temperature word jumped by more than a threshold); a fixed blanking time after key-on (needs a time per unit); dropping the marked response from the last-value fields too, which the #6229 triage asks for at the receive loop. It protects the last-value forward word, the SWR pair and the temperature pole, and a unit that always reads 0 would then lose forward power, SWR and temperature outright. Left for the maintainer; with the predicate it is one call. Rejecting a lone sample above both neighbours, or a median at the head of the over, would discard real one-sample speech peaks, which #6019 exists to catch. ## Tests Socket-free. `tests/Hl2KeyOnRaddr1Fixture.h` holds the recorded RADDR-1 words of three key-ons (2789 responses; captures named with their sha256 in the header). The replay feeds them through `MetisClient::handleDatagram` and `Hl2Backend::publishTelemetry` at ten phases of the 100 ms publish window. | replay | a plain window maximum shows | this PR shows | |---|---|---| | 1 kHz tone at 40 %, the over with the lone word, first second | 3.822 W | 1.211 W, the carrier's own maximum in that second | | TUNE 100 %, first 0.4 s | 5.715 W | 5.715 W, every window equal | | TUNE 10 %, first 0.4 s | 0.879 W | 0.879 W, every window equal | - `hl2_backend_seam_test`: 86 checks, 16 new. The tone over never displays above its carrier; both TUNE key-ons publish the plain maximum in every window, so the rise is not delayed by any window; a **synthetic** speech-like envelope with one-sample peaks at 190 responses a second still wins every window. The health row is absent, then 0, then the session total; the tone replay shows exactly the recording's 2 marked responses and the synthetic speech replay shows 0. - `hl2_metis_protocol_test`: 15 new checks and 2 `static_assert`s. A marked response is neither peak nor counted; the response after it is kept; a lone sample far above both neighbours is kept; a temperature word of 1 is not a marker; a window of marked responses only has no peak. The predicate alone: an ACK, RADDR 0, 2 and 3, and any non-zero temperature word are not marked. The skipped count rises on a marked response and on nothing else; a window of marked responses only shows a count and no peak. - `hl2_fwd_window_wiring_test`: 5 new checks: the emit carries the skipped count and the next window starts at zero. Its existing checks, `hl2_tx_level_policy_test`, and the existing checks in the two files above are unchanged and pass. Full suite on macOS (Qt 6.12.0) at `ba223304`, on `main` `5f09371e`: 646 tests, 643 pass. Three fail at `-j8`, and clean `main` `5f09371e` fails the same three on this Mac: - `vkamp_connection_test`: #5254; passes serially here. - `peripheral_auth_dialog_test`: #6008; this diff touches no dialog file. - `unified_title_bar_test`: two checks ("Alt+F still opens the File menu with the menu bar hidden", "opening a menu rounds the submenus added to it since"), the same two on clean `main`. New with #6198; this diff touches no GUI file. ## Mutations Each a scratch commit on `ba223304`, built and run through the full suite, never pushed. Red checks in the three HL2 tests: - Guard removed: the tone replay check is red (3.822 W shown) and the replay's health count, plus 8 leaf and wiring checks. - Guard widened to normal temperature words (`< 2048`): both TUNE replays, the synthetic speech check and both replay health counts are red, plus 16 leaf and wiring checks. - Guard also drops the response after a marked one: 3 leaf and wiring checks are red. **The replays stay green**, because in the recordings that response carries forward 0 to 4. - Predicate flipped: 6 seam checks and 20 leaf and wiring checks. - Skipped count not incremented: the tone replay's health count and 4 leaf and wiring checks. - Skipped count also incremented on a kept response: both replay health counts and 4 leaf and wiring checks. - Health row reading the response total instead: 6 seam checks. The two predicate mutations also delete the two `static_assert`s, which otherwise stop the build. ## Not verified - Anything on a radio with this build. Nothing was keyed. - Any second unit. A capture of 20 or more key-ons at 384 kHz from another station would show whether its marked responses look the same. - Windows and Linux builds (CI). - The Radio Health dialog and the bridge `health` reply with the new row: the app was not launched; the row is tested through `healthSnapshot()`. - `docs/HERMES.md` is not updated. `docs/automation-bridge.md` has one paragraph on the new row. Static checks: every step of the "Static checks" job in `static-checks.yml` was run locally and is green at `ba223304`. The sanitizer configure job needs the CI image and was not run. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…remote operation over CGNAT (#6389) ## Summary Release prep for **v26.10.2**: the six release files plus the Stable docs snapshot. Metadata and docs only; no source, test or CI changes. Cutoff: `origin/main` at `46943eb3c` (#6083, merged 2026-10-11). Anything that merges after it is folded in before the tag. **106 merged changes** from **15 human contributors**, AetherClaude (2) and Dependabot (0). First-time contributor: **@dacacioa**. This was confirmed by checking each login for a merged PR before the previous tag, not from `author_association`. ## Changelog The range `v26.10.1..46943eb` was mapped through the commit-to-PR API (`repos/…/commits/<sha>/pulls`), not from commit subjects. It holds 106 PRs and no direct commits, and every one is cited in the section. `v26.10.1` is an ancestor of `main`, so nothing is excluded. #6083 merged before the branch was cut, so it is inside the range and needs no unmerged-PR banner. The notes lead with: - the unified 52 px title bar (#6198); - docs.aethersdr.com (#6286, #6298, #6294); - remote operation over CGNAT through the in-radio tailnet shim (#6273, #6318, #6325, #6319); - the canon windows (#6227, #6334, #6339, #6340, #6341, #6364, #6372, #6386). Then come DVK import, FFT Smooth/De-comb, TCI tune and receiver events, RTL-SDR multi-receiver delivery, HL2 and ANAN-G2 state memory, the IC-7300MK2 fixes, the first clean sanitizer runs (stated beside #6350's TSan red) and Authenticode signing. Numbers come from the PR bodies. "On upgrading" callouts cover the behavior changes the bodies flagged: - HL2 receive audio is 12.04 dB quieter (#6254); - HL2 host CW is +6.02 dB at a fixed drive (#5975); - ANAN AGC-T is remapped (#6282); - sleep prevention is on by default (#6195); - the Tailscale MTU cap has no opt-out (#6319); - IC-7300MK2 Auto SQL is withdrawn (#6184). DeepFist (#5953, #6103) is described as applying only to `ENABLE_DEEPFIST_EXPERIMENT` builds. ## Docs refresh, not just version strings - **README**: - Highlights: FFT Smooth/De-comb on the spectrum line; AetherMap naming; remote over CGNAT and TCI tune/receiver events on the SmartLink/TCI line; pre-NR Copy Assist; a new docs.aethersdr.com line. - Supported Hardware: the KPA1500, marked as not yet validated on hardware. - Other radio families: HL2 state restore extended; ANAN-G2 gains AGC-T, NB2, supply voltage/link and per-radio memory; RTL-SDR now "bundled in every official package", with WFM stereo, PPM, per-receiver SQL/meters and DAX/TCI feeds, and multi-receiver explicitly an evaluation. - Roadmap section: the CTR2 line advanced and a visual-canon line added. - Maturity labels are unchanged. - **ROADMAP**: - In flight: a v26.10.2 sentence each for aetherd, Icom, ANAN-G2, RTL-SDR (remaining is now production multi-receiver qualification and digital FM, RFC #6079), Hermes-Lite 2 and the CTR2 relay (RFC #6332 extensions). A new entry for the visual canon (RFC #6226 / #6239). - Queued: RFC #4214 (approved 2026-10-09) added. - Open RFCs, checked against `label:rfc`: #4214 leaves (approved); #4764 and #4861 leave (shipped by #6198 and #6216); #6346, #6232, #6212 and #6209 join. #6332 is approved and folds into the CTR2 entry. - Recently shipped: nine new `(v26.10.2)` entries, still within the v26.10.x cycle, so no month-rollover trim. - Backends (six: anan, flex, hl2, icom, rtl, sim) and NR engines (seven) were checked against the tree; both are unchanged. ## Judgment calls flagged for the maintainer All of these were decided through AskUserQuestion: - Version **26.10.2**, date **2026-10-11**, opened ready for review. - **#6350**, the TSan red in the weekly sanitizer lane: proceed, and name it below. - RFCs **#4764** and **#4861** come off Open RFCs as shipped. The issues themselves are left open for you to close. - RFC **#4214** goes under Queued (next cycle). - A **visual canon** In-flight entry, plus a README Roadmap line. - The **KPA1500** is listed in README Supported Hardware with "not yet validated on hardware". - Including **#6083**: you asked for it, and it merged before the cutoff, so it ships as an ordinary part of the range. ## Known failing on `main` - **`ctr2_hid_thread_port_test`** fails in the weekly TSan lane (runs of 2026-10-08 and 2026-10-10; #6350). `main`'s CI and Full Suite were green at `414082197`. The CHANGELOG headline and intro say so: the lanes achieved their first clean runs this cycle, and the test remains failing under TSan. This prep does not fix the test, and the release ships with it red in that lane. ## Scope | file | change | |---|---| | `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.2)` | | `README.md` | Current version line, plus the prose refresh above | | `AGENTS.md` | Current version line | | `CHANGELOG.md` | new `[v26.10.2] — 2026-10-11` section; `[Unreleased]` stays and stays empty; history below byte-identical | | `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` | `<release version="26.10.2" date="2026-10-11"/>` first in `<releases>` | | `ROADMAP.md` | cycle heading → post-v26.10.2, plus the prose refresh above | | `docs/user/versioned_docs/version-stable/**`, `docs/user/versioned_sidebars/version-stable-sidebars.json`, `docs/user/stable-version.json`, `docs/user/src/components/LogAnalyzer/rules.json` | docs snapshot (`tools/docs/snapshot_stable.py 26.10.2`), in its own commit. This is the first snapshot since #6286. | ## Validation Output of `check_release_prep.py --mapping mapping.json` at the branch head: 30 checks, 1 FAIL (explained below), 1 WARN (the snapshot files, listed in the scope table). ``` PASS v26.10.1 is an ancestor of origin/main PASS all six version spots say 26.10.2 PASS CHANGELOG heading date equals metainfo release date PASS appstreamcli validate PASS metainfo <release> list is date-descending with unique versions PASS `## [Unreleased]` stays and stays empty PASS `## [v26.10.2]` sits directly under `[Unreleased]` PASS the previous section is `[v26.10.1]` PASS CHANGELOG diff is exactly the inserted section PASS new section opens with a `### <headline>` PASS intro sentence has the house shape PASS new section has a `### Contributors` block PASS release_contents.py --check (citations, intro and Contributors counts) PRs in range: 106 cited: 106; intro 106/15 matches; Contributors (16 entries) matches; first-time ['dacacioa'] citations outside the range: #4764, #4861, #5468, #6192, #6226, #6271, #6332 (RFCs), #6238, #6350 (issues) PASS code fences balance / no duplicated table headers or doubled `---` (README, ROADMAP, new section) PASS every local link in README.md and ROADMAP.md resolves PASS no heading removed from README/ROADMAP is still linked elsewhere PASS Recently shipped `(vX)` markers run newest-first and lead with the new version PASS Recently shipped intro names the current cycle (v26.10.x) FAIL only the four current-version lines lose the `26.10.1` string outside CHANGELOG historical mentions must stay: "label": "26.10.1" PASS diff touches no src/, tests/, third_party/ or .github/workflows/ path PASS all six release files are in the diff WARN files beyond the six (the docs snapshot) PASS git diff --check ``` - **The one FAIL is the snapshot's version label, not a historical mention.** `docs/user/stable-version.json`'s `"label"` is the docs site's current Stable version, and `snapshot_stable.py` rewrites it by design. The validator predates #6286 and knows only the four current-version lines. Run on the six-file commit alone, the same check passes (29 checks, 0 FAIL, 0 WARN). - The docs build passes: `npm ci && npm run build` in `docs/user`. `node tools/docs/test_log_rules.mjs` reports 707 checks passed for 45 rules. The line moves it reports were already on `main`. - Four merged PRs (#6340, #6341, #6364, #6372) named screenshots to recapture after merge (`psk-reporter-sidebar.png`, `menu-tools.png`, `connect-dialog.png`, `rx-agc-calibration.png`), and #6386 named the panadapter shots (`panadapter.png` and the main-window captures, which still show the old title strip). They were not recaptured, so the Stable snapshot carries the older images. - No configure, build or test was run for this prep; it is metadata only, and CI builds it. - `main` CI at the cutoff `46943eb3c`: Docs and CodeQL - Code Quality are green; CI, Full Suite, CodeQL and the System Libraries Canary were still running when this was opened. At `414082197`, every one of them was green. After merge: tag the resulting **main** commit, not this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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:  After — the same saved-left startup on the fixed build:  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.
Implements the approved RFC #4764, Frameless window retrofit: one 52 px unified title bar with radio tabs, rebuilt on Qt 6.12's window-hint APIs. Supersedes #4906 and #5489.
What the bar is
One 52 px bar owns the application menu, the brand mark, the radio tabs, the audio cluster, the applet dock/float controls and the window controls.
QMenuBarstays as a hidden child. MainWindow still builds into it, and the automation bridge still resolves actions through it.popup()(no nested event loop).color.titlebar.menu.background, 0.92 alpha).PersistentDialog(themed, remembered geometry, follows frameless mode).Window chrome per platform (
src/gui/WindowChrome.h)ExpandedClientAreaHint+NoTitleBarBackgroundHint, empty unifiedNSToolbarfor 52 px light placementExpandedClientAreaHint+NoTitleBarBackgroundHint(Qt owns the frame, shadow, rounded corners and resize borders; replaces ournativeEvent/DWM frame)WS_MINIMIZEBOX/WS_MAXIMIZEBOXare restored on the HWNDFramelessWindowHint+FramelessResizer(6 px band, no top-edge resize under the bar, #4886)startSystemMove(), except on xcb, where the #4827 manual move is keptView → Frameless Windowremains the escape hatch everywhere.docs/architecture/window-chrome.mddocuments the design.The main window is opaque (
WA_TranslucentBackgroundoff). The bar paints a pre-composited opaque fill and a 1 px hairline (WA_OpaquePaintEvent). Its containers are made transparent by object name, so MainWindow's window-wideQWidgetrule cannot slab them.Also in this change
get titlebarand thetitlebarverb (selectRadio / showDiscovery / minimize / maximize / close) expose geometry, tabs, link state, chrome mode and the ☰ menu.appletverb (dock left|right, float on|off, show, hide, state) covers the dock/float controls."deferred":true, with the pre-action snapshot).selectRadiorefuses a hidden tab.applet stateis allowed in observe-only mode.PanadapterStack::refreshAfterLayoutShift()re-realizes native pan windows after a dock flip on macOS (Metal) only. Elsewhere it is a repaint sweep.color.titlebar.*and 2color.brand.*, documented indocs/theming/canonical-tokens.md. The theme seed is regenerated.unified_title_bar_test(new, socket-free) covers:minimal_mode_exit_order_testnow anchors on the exit branch'srestoreGeometry().automation_boundary_core_testpins the deferral and observe-only contract.Maintainer rulings (@ten9876)
appletverb/winbuildScreenshots (macOS 27.0.1, Qt 6.12.0, isolated profile, built-in demo radio, nothing on the air)
🤖 Generated with Claude Code