Repository navigation
feat(gui): AetherRX and AetherTX are CanonWindows, re-skinned to the canon palette through a canon theme scope. Principle VIII. - #6334
Conversation
…canon palette through a canon theme scope. Principle VIII. Both windows now use the About window's recipe for tool windows, as Network Diagnostics and Waveforms do: CanonWindow chrome (no title bar, 16 px rounded corners, ambient ground, corner close button, drag any empty area to move, Escape / Ctrl+W to close), a canon header, FramelessResizer on every edge, and the existing geometry keys via setGeometryKey(), so saved positions carry over. Both still pin their size at 720x480 on open. AetherTX loses its inline 18 px title bar, its min/max buttons and its own geometry and frameless-toggle code. It no longer follows View -> Frameless Window. Close still hides it rather than deleting it, because MainWindow keeps feeding it state. AetherTX's startup restore now waits for MainWindow's first show. Shown earlier, a tiling compositor (Hyprland) took the parentless dialog for a top-level window, tiled it, and CanonWindow saved the tiled size. Re-skin: a new `canon` scope in both bundled themes remaps the base background, text, border, accent, knob and slider tokens to the color.canon.* palette. The windows sit under canon/aetherRx and canon/aetherTx. Shared content (AetherNR's widget, the curve, meter and knob canvases) therefore turns canon inside these windows only, and the docked applets and floating editors keep their look. The painted widgets they use resolve colours through the widget's scope (color(this, ...), plus a widget-aware theme::withAlpha overload). The Strip* panels, which are only used here, trade their hard-coded colours for tokens. theme_manager_test pins every scope mapping to its color.canon.* token in both themes. Dark repeats the canon hex values in the scope, since scope values can only alias primitives. A mutation (one dark value off by one) fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
The visual migration largely delivers its stated intent: both windows adopt CanonWindow, scoped content uses the canon palette, existing geometry keys are reused, and startup restoration is deferred until MainWindow shows. There is no closing issue; I reviewed against the PR body and the approved direction in RFC #6226 / tracker #6239. Request changes: the new QDialog host introduces an Enter-key regression, and the user-facing changes need their matching user-guide update.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
AetherRxDialog.{cpp,h}, AetherialAudioStrip.{cpp,h} |
CanonWindow hosts, headers, geometry and close lifecycle | Yes | In scope; keyboard regression below |
MainWindow.{cpp,h} |
Create/raise RX directly, defer TX startup show, remove frameless propagation | Yes | In scope |
StagePage.{cpp,h} |
Shared canon header and transparent-body stylesheet | Yes | In scope |
Nine ClientComp* / ClientGate* / ClientDeEssCurveWidget / ClientTubeCurveWidget canvases and WaveformWidget.cpp |
Widget-aware paint-token lookup | Yes | In scope |
Ten Strip*Panel.cpp files |
Replace fixed colors with tokens; keep waveform title with plot | Yes | In scope |
Bundled theme JSONs, ThemeManager.h, ThemeSeedGenerated.cpp, theme_manager_test.cpp |
Canon scope, alpha helper, generated seed and mapping assertions | Yes | In scope |
Three docs/style guides and docs/architecture/aetherd-touchpoints.md |
Migration documentation and regenerated theme include count | Yes | In scope; user guide still missing |
Everything in the diff is explained by the body. The single commit's author date is 2026-10-09; there is no unrelated historical commit. No base theme token values, protocol verbs, new dependencies, or enforcement baselines change. The documentation checklist accurately names contributor/style documentation, but it does not satisfy the separate user-documentation requirement.
Blockers
-
Enter in a compressor value editor also activates the Gate tab.
src/gui/AetherialAudioStrip.cpp:95, with the button construction atsrc/gui/StageTabBar.cpp:139.Switching from QWidget to CanonWindow makes the processing controls descendants of a QDialog. Their QPushButtons retain
autoDefault=true. In a QtTest harness linked against the PR's actual desktop objects, I opened AetherTX, clicked Compressor, focused a visible knob's QLineEdit, typed4, and pressed Return. The edit committed, but QDialog also clicked the default Gate button and switched away from Compressor:Before Enter: Compressor_selected=1 Before: RN2=0 autoDefault=1 default=0 focus=QLineEdit DEFAULT: Gate visible=1 enabled=1 CLICKED: Gate aetherTxTabGate After Enter: RN2=0 RN2_clicked=0 knob=4.001 After Enter: Compressor_selected=0With automatic/default buttons disabled, the identical edit stays on Compressor:
Before Enter: Compressor_selected=1 Before: RN2=0 autoDefault=0 default=0 focus=QLineEdit After Enter: RN2=0 RN2_clicked=0 knob=4 After Enter: Compressor_selected=1A separate fixture using the former QWidget/Qt::Window host with the same gate panel produces no button click on Return. This comparator isolates the host-type change; it is not a full merge-base app build. The harness is socket-free and has no radio connection. The demo bridge independently verified the real window's tab selection and close/reopen paths; its invoke API does not send physical Return keys, so the keyboard reproduction uses QtTest on the real widgets.
Disable automatic defaults on the processing-window buttons, preserving deliberate defaults in separate confirmation dialogs. A drop-in mitigation at the end of AetherialAudioStrip's constructor, after the panels have been embedded, is:
for (QPushButton* button : findChildren<QPushButton*>()) { if (button->window() == this) { button->setAutoDefault(false); button->setDefault(false); } }
Add a socket-free regression that selects a different stage, commits a knob edit with Return, and checks the selected stage remains unchanged. This also implements the existing docs/DEVELOPER-GUIDE.md instruction: “Disable
autoDefaulton QPushButtons inside QDialogs.” -
Update the matching user guide in this PR.
docs/user/docs/aetherial-audio.md:59.This section explains using the two windows, but the PR changes their close shortcuts, drag/resize chrome, relationship to View → Frameless Window, and AetherTX's independent-window/taskbar behavior without editing any
docs/usercontent. AGENTS.md:162–163 explicitly requires: “a PR that changes what a user sees or does updates the matching page in the same PR, never in a follow-up.” Add the new window behavior here and validate the docs build. The stale screenshot disclosure in the body meets the separate screenshot exception; I am not requiring screenshot recapture to block this review.
Nits — non-blocking
- Correct the principle's name in the PR body.
CONSTITUTION.md:226: Principle VIII is Evidence Over Assertion, not “one visual language.” Cite VIII for the verification evidence and RFC #6226 for the visual direction.
Verification and attacks that held up
- Built the exact head
be53b1c2757547c8234f4180aee45025adfe8fe8in a scratch worktree with Qt 6.12 on Linux, shared core and-O2 -g1. All ten tests named in the body passed: theme manager/seed, CanonWindow, stage drag, modem chrome, compact metrics, comp makeup fader, RX chain runner/reorder, and window show state. RX chain runner needed explicitQT_QPA_PLATFORM=offscreenin this headless environment. No new/modified socket-owning test was found. No independent automated code-review skill was available. - Mutation-checked the added mapping assertion using a separate resource object: changed dark
canon.color.background.0from#0a121eto#0a121f;theme_manager_test.cpp:1512failed with the exact mismatched RGBA values. The original head test passed. Original sources and resource objects were untouched. - Ran engine boundary, capability-record, command-plane and touchpoint checks on merge base
7a649a9e6and head; their findings are identical. The two added ThemeManager includes are UI theming and disclosed; the generated manifest and theme seed check cleanly. No seam, TX-gate, or settings-key growth was found. - Drove the owned
aethersdr-pr6334-reviewbridge against DEMO-0001 only, with fresh settings, offscreen rendering and TX disabled.get radioconfirmedmodel: "AetherSDR Demo",connected: true,transmitting: false. Opened both menus/windows, switched TX stages, captured Dark and Light windows, closed/reopened both, and resized TX to 900×600 then confirmed reopening pins it back to 720×480. A separate restart with the saved visibility flag explicitly set True restored TX at{ "h": 480, "w": 720, "x": 340, "y": 160 }. All owned app instances were closed. - Verification limits: no physical compositor edge drag, native Windows/macOS window-manager behavior, or live-radio behavior was tested. Offscreen Qt reports QRhi unsupported, so the black waveform capture is not evidence for the PR's Wayland/GPU claim. A normal demo-session quit also cleared TX's visibility flag; I have not attributed that to this PR without a merge-base comparison, so startup verification is specifically the seeded-flag restore path rather than a claim that quit persistence was proven.
All required CI checks are successful on the reviewed head. Their pull-request runs were created at 2026-10-09 21:07:16 UTC, after current main's 20:30:34 UTC tip (d6f6a4f00), so there is no observed stale-main timing gap.
Recommendation: Request changes. Fix Return/default-button dispatch and add the user-guide update, then rerun the focused keyboard regression. The rest of the scoped migration survived the checks above.
There was a problem hiding this comment.
Issue fit
No fixes/closes line in the body, so I reviewed against the PR's own stated intent and against the canon-migration tracking issue #6239. The stated work is all present and coherent: both windows derive CanonWindow, both carry theme::setContainer(this, "canon/aether{Rx,Tx}"), setGeometryKey() reuses the old keys (and CanonWindow::restoreGeometryFromSettings() has a base64 fallback, so the pre-existing saveGeometry() blobs under AetherRxDialogGeometry2 / AetherialStripGeometry2 do carry over — I checked that path specifically), the canon scope exists in both bundled themes plus the generated seed, and theme_manager_test pins it.
The governance question is whether these two windows were in scope yet — see Blockers 1.
Scope
The task handed me a 30-file list; the commit is 37 files (src/gui/StripPuduPanel.cpp, StripReverbPanel.cpp, StripRxOutputPanel.cpp, StripTubePanel.cpp, StripWaveformPanel.cpp, WaveformWidget.cpp, tests/theme_manager_test.cpp were missing). This review covers all 37.
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
AetherRxDialog.{h,cpp}, AetherialAudioStrip.{h,cpp} |
PersistentDialog/QWidget(Qt::Window) → CanonWindow; inline title bar, frameless toggle and own geometry code deleted |
yes | In scope |
MainWindow.{h,cpp} |
ensureAetherRxDialog() creates directly instead of showOrRaisePersistent(); strip restore deferred to showEvent; setFramelessMode/setWindowFlag(Qt::Window) calls dropped |
yes | In scope (see Blockers 2) |
StagePage.{h,cpp} |
new makeCanonHeader() / canonBodyStyleSheet() |
yes | In scope |
resources/themes/default-{dark,light}.json, ThemeSeedGenerated.cpp |
new canon scope, 15 token remaps each |
yes | In scope. No base token value changed — I diffed the root scopes; this respects #6239's "a migration PR never changes a base token's value" |
ThemeManager.h |
widget-aware theme::withAlpha overload |
yes | In scope, used in 4 files |
ClientComp*, ClientGate*, ClientDeEss*, ClientTube*, WaveformWidget |
root color(...) → widget-aware color(this, ...) |
yes | In scope |
Strip*Panel.cpp (10 files) |
hard-coded hex QSS → {{token}} via applyStyleSheet |
yes | In scope. "only used here" verified: every Strip*Panel is instantiated only from AetherRxDialog.cpp / AetherialAudioStrip.cpp |
docs/style/*.md, docs/architecture/aetherd-touchpoints.md |
doc updates + touchpoint count 169→171 | yes | In scope |
tests/theme_manager_test.cpp |
+37 lines pinning the scope | yes | In scope |
No unrelated files, no new public/protocol surface, no new settings key (both geometry keys are reused), no CHANGELOG.md entry. No socket tests added or modified — I read the added test source, not the working tree.
Blockers
1. #6239 does not list AetherRX or AetherTX, and this is two windows in one PR. #6239 (the tracking issue this work sits under) says:
Each item below is its own issue and its own PR. Screens move to the canon only when an issue asks them to; nothing here authorises restyling a screen as a side effect of other work. Items under "Candidates" are not approved scope until the maintainer picks them.
Neither window appears on it. The nearest line is "Persistent tool dialogs (one PR per dialog)" under Candidates: surfaces whose widgets move from color.canon.* — explicitly unapproved, and explicitly one dialog per PR. docs/style/aethersdr-style-guide.md:112 likewise reads "the tool windows the maintainer has moved to it", and this PR edits that sentence to add the two windows.
I checked the authorship before raising this: git log shows the Waveforms and Network Diagnostics canon migrations, and #6239 itself, are all by the same identity as this commit (Jeremy [KK7GWY] <105957118+ten9876@…>). So this is most likely you ticking your own list, not a scope objection — in which case the fix is procedural: add the two items to #6239 (or open the per-window issues) and link them from the body, so the trail matches the rule the tracking issue sets. I'm flagging rather than waving it through because the rule is unusually explicit about authorisation, and the next contributor will read this PR as precedent for moving a screen with no issue.
2. MainWindow.cpp:10189 — AetherTX stops being an independent top-level window; the deleted comment names what is lost. The diff removes:
// Override the parent-window relationship so the strip behaves as
// an independent window (own taskbar entry, raisable separately).
m_aetherialStrip->setWindowFlag(Qt::Window, true);That call was itself a no-op (the old ctor already passed QWidget(parent, Qt::Window)), but the behaviour came from that ctor flag, and CanonWindow's ctor replaces it with setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint) (src/gui/CanonWindow.cpp:44). A parented Qt::Dialog gets no taskbar button on Windows, stays above MainWindow, and is hidden when MainWindow minimises — and with the min/max trio deleted (AetherialAudioStrip.cpp, old lines ~155-185) there is now no way to minimise AetherTX at all. The body discloses losing min/max and the Frameless toggle; it does not mention the taskbar entry or the stays-above/hides-with-parent consequences.
AetherRX is unaffected — PersistentDialog already derives QDialog (src/gui/PersistentDialog.h:21). And Network Diagnostics and Waveforms already took this trade, so this isn't novel. But AetherTX is the one window that was deliberately built as an independent Qt::Window (#2301), and operators park it on a second monitor. Needs a maintainer decision: accept the trade and say so in the body, or construct these two with Qt::Window | Qt::FramelessWindowHint instead of Qt::Dialog.
Nits (non-blocking)
src/gui/StagePage.cpp:139-146— the descendant-wide transparent rule may reach child dialogs' inputs."QWidget#canonBody QWidget { background: transparent; }"is more specific (id + type) than a plain type selector, so it can beat aQSpinBox { … }rule set further down the tree. The exception list coversQMenuand combo popups, but not the childQDialogs built from these pages — e.g. the Quindar editor atsrc/gui/StripFinalOutputPanel.cpp:858-883, whoseQSpinBoxes at:1002-1019have no self-set background. Combos are safe (ComboStyle.h:61setsQComboBox { background: … }on the combo itself, and it resolves scope-aware). This is reasoned from the cascade, not observed — worth one look at that dialog in both themes. Neither Network Diagnostics nor Waveforms uses a descendant-wide rule; they style by object name, which sidesteps this.src/gui/AetherRxDialog.cpp:419-421andAetherialAudioStrip.cppshowEvent— the size pin silently discards every operator resize.CanonWindow::showEventsetsm_placed = truebefore returning, so the subclass'sresize(kLaunchSize)firesresizeEvent→saveGeometryToSettings()→ 720×480 is written back over the stored rectangle. Position survives; size never does, which makes the newly installedFramelessResizera no-op across sessions and doesn't meetdialog-patterns.md's "a workspace tool keeps the size its operator gave it". The doc hunk does say "for now", so this is disclosed — just noting that the mechanism is destructive, not merely ignored.src/gui/AetherDspWidget.cpp:601-613, 750-755— the page the new style-guide line names as the motivating case wasn't converted. The added guidance says painted code "must use the widget-aware lookups … to see it", but AetherNR's labels are rawsetStyleSheetwith hard-coded hexes (#8090a0,#c8d8e8,#48515a) and the status dot resolves root-scopeThemeManager::instance().color("color.text.label"), so they stay base-coloured inside the canon window. Same class atsrc/gui/ClientCompKnob.cpp:61-68(the knob canvas became scope-aware; its value editor's QSS did not). Cosmetic only — on light,#8090a0was already ~2.9:1 against root's#f5f5f8, so canon's#ffffffis not an a11y regression.- The
canonscope covers 2 of the 5 canon windows. About, Waveforms (dialog/waveforms) and Network Diagnostics (dialog/networkDiag) keepdialog/*containers, so a theme edit tocanonwon't reach them.docs/style/theme-style-guide.md's new "Thecanonscope does the same for whole windows" reads broader than what ships. - Pre-existing user themes get an empty
canonscope.loadThemeFile()wipes scope children and then only replays declared paths (ThemeManager.cpp:519) with no tokens, so a theme saved by an earlier version resolves these 15 tokens from root — base-dark content inside a canon ground until it's re-saved. Same exposure asapplet/*, so architectural rather than introduced here; worth a line in the body. Related: any operator override previously written underdialog/aetherRxis now orphaned. - The new test discriminates only in dark.
tests/theme_manager_test.cpp:1484+is a real test (delete the scope and it fails), but in lightcolor.background.1/color.text.primaryalias the same primitives ascanon.nested/canon.ink(#dde5ed,#1a2a3a), so several light rows pass vacuously — which is exactly the aliasing hazard #6239's open question flags. ThecolorAt(QString(), …) == color(…)assertion is identically true for any input.
What I tried to break
- Dead scope path.
lookupRaw()falls back to root, not to the nearest ancestor scope, whenscopeForPath()misses — and the themes declarecanonwith no children, socanon/aetherRxonly resolves becausetheme::setContainer→registerDeclaredContainer→scopeOrCreatelinks the child undercanon(ThemeManager.cpp:252-279). It does, and:519replays it across theme loads. Holds. - Dead token mappings. The four knob/slider remaps are live, not decoration:
ClientCompKnob.cpp:40-41already usescolor(w, …)and sliders go throughapplyStyleSheet→resolveFor(widget). - Collateral re-skin of the docked applets. This was my main suspicion for the widget-aware switch.
applet/tx|rx|compoverride onlycolor.slider.foreground,color.knob.foregroundandcolor.toggle.accent.background.checked; no changed file reads any of those three (verified by grepping all 37 changed files).WaveformWidgetis shared withWaveAppletbut resolves root outside the scope. No change outside the two windows. - "Only used here." Confirmed per-class: every
Strip*Panelhas exactly two (or one) instantiation sites, both in AetherRX/AetherTX. So the hard-coded-hex → token swaps can't shift a docked surface. - Lost guard on the embedded panels. The deleted block in
AetherialAudioStrip.cpphid each panel's min/max/close and recoloured#08121d. That is not a regression —StagePage::tidyEmbeddedPanel()(pre-existing, called atAetherialAudioStrip.cpp:340-344) already does both; the deleted copy was redundant. - Dangling
m_rxDialog.ensureAetherRxDialog()now news the dialog itself withWA_DeleteOnClose;m_rxDialogis aQPointer(MainWindow.h:1692), so thewasFreshtest and the ~20 other call sites are safe. Escape routes throughQDialog::reject()rather thanclose(), and either way the next toggle press re-shows or re-creates correctly. - Stale
setFramelessModecallers. Grepped the tree: nothing calls it on the strip any more, and the declaration is gone from the header. - Theme value drift. Resolved every
canonscope value against thecolor.canon.*palette in both themes by hand (script over the JSON): all 15 match in both, so the new test should pass — consistent with the greenStatic checks/build/check-macos/check-windowsruns onbe53b1c. - Could not check: anything visual. No build, no GUI, no Wayland/X11/Windows session here — the chrome, the transparent-descendant cascade, the drag/resize interaction between
FramelessResizerandCanonWindow's drag-anywheremousePressEvent, and the light-theme canon rendering are all unverified. Every finding above is reasoned from the code as it will exist after merge, not reproduced at runtime. - Security audit (CodeGuard): ran, no confirmed findings. Worth knowing: the harness's file list and its 423 findings were computed against a stale merge base — none of the files this PR actually re-skins were in the list, while most listed files (
AutomationServer.cpp,AudioEngine.cpp,CopyAssistController.cpp,codeql.yml, …) are unchangedmaincontent from #6216/#6205/#6182. TheCMDI-*hits are QSS{{token}}braces and Qt&&label escaping;SSRF-LOCALHOSTis the TCI simulator dialing our own loopback;PII-BANKis a SHA-256 download pin. All refuted at source.
Recommendation
Needs maintainer decision — on the #6239 authorisation trail (Blockers 1) and on AetherTX giving up its independent-top-level-window behaviour (Blockers 2). The engineering is clean: the scope mechanism is sound, the test genuinely discriminates, the "only used here" and "no collateral applet change" claims both hold under checking, and nothing in the diff is unexplained by the stated work. If you're the maintainer ticking your own list, Blocker 1 collapses to adding the items/issue links to #6239 and a line in the body about the taskbar/minimise trade — at which point this is approve-with-nits. Concrete next step: rule on the Qt::Dialog vs Qt::Window question, and take one look at the Quindar sub-dialog's spin boxes in the light theme for the cascade nit.
Nice work on the geometry-format fallback and on keeping the scope purely additive — no base token moved, which is the part of #6239 that would have been easiest to get wrong.
🤖 aethersdr-agent · cost: $16.3123 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (431 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $16.4831 · model: claude-opus-5 |
|
Antares Detector — candidate vulnerable file(s)
The theme loader reads user-supplied theme JSON and user-supplied theme paths from disk. ThemeManager.cpp implements loadThemeFromPath and scope/creation logic that resolves paths without enforcing a safe root-directory restriction. ThemeSeedGenerated.cpp generates the authoritative theme JSON from resources/themes/*, so untrusted themes can be loaded. This creates a path traversal/injection risk if an attacker can supply a theme file path (e.g. via a network path or crafted filename) that is not validated before being used to open/read files. Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting. 🤖 aethersdr-agent · cost: $16.4831 · model: claude-opus-5 |
…d Return in a field no longer clicks a stage tab. Principle VIII. Review of #6334. CanonWindow gains Kind::Workspace. A workspace is an independent Qt::Window, which gives back AetherTX's own taskbar entry (#2301), lets it minimise on its own and lets it sit on another monitor. It has WA_QuitOnClose off. In a workspace QDialog's default-button dispatch is skipped: QDialog had made every QPushButton an auto-default, so Return in a compressor knob's value field also clicked the Gate tab. A focused button still takes Return itself. Kind::Dialog keeps the old behaviour, which About's OK relies on. canon_window_test pins both kinds: the window type, WA_QuitOnClose, and Return and keypad Enter in a field (no click in a workspace, a click in a dialog). Disabling the guard fails it. A throwaway harness linked against the app's objects reproduces the review's scenario on the real AetherTX window: the stage changes with the guard off and stays put with it on. The user guide's AetherTX/AetherRX section now describes the window: moving it, resizing it, closing it, its taskbar entry, and that View -> Frameless Window does not apply. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Addressed in a37c18a. @ten9876's review
@aethersdr-agent's review
The nits are noted and left for follow-up. One finding from the verification limits: AetherTX left open at a normal quit doesn't reopen on the next launch on |
There was a problem hiding this comment.
Issue fit
Yes, with one authorisation question. #6239's "Picked: tool windows on CanonWindow" section names both windows, authorises the two-in-one-PR exception on exactly the reason this PR gives (shared stage tab column, shared Strip* panels, shared canon scope), and specifies Kind::Workspace. Every claim in the body maps to a hunk: chrome (CanonWindow.cpp:42-52, AetherRxDialog.cpp:92-111, AetherialAudioStrip.cpp:95-110), the scope (both theme JSONs + ThemeSeedGenerated.cpp:379-393), Kind::Workspace (CanonWindow.h:32-41, CanonWindow.cpp:117-131), the geometry carry-over (setGeometryKey() on the existing keys), and the startup fix (MainWindow.cpp:3956-3975).
The authorisation caveat: that section lives in an issue body you authored, and #6239's updated_at (2026-10-10 01:22) postdates this PR's creation (2026-10-09 21:07). The precedent in that same thread — your own #6273 status comment — cites the maintainer ruling as a linked #issuecomment URL. Please link the ruling that picked these two, the way #6273 does. The test plan's "Run by the maintainer against the FLEX-8600" suggests it exists; it just isn't citable from the issue as it stands.
Scope
41 files (the harness's file list was truncated at 29; I reviewed the full 7a649a9e6..a37c18a2b range).
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
CanonWindow.{h,cpp} |
Kind enum, Qt::Window + WA_QuitOnClose off, Return/Enter guard |
Yes | In scope |
AetherRxDialog.{h,cpp}, AetherialAudioStrip.{h,cpp} |
PersistentDialog/QWidget → CanonWindow; deletes the inline 18 px title bar, the trio buttons, setFramelessMode(), and the private geometry save/restore |
Yes | In scope |
MainWindow.{h,cpp} |
ensureAetherRxDialog() create-or-raise; drops the strip from setFramelessWindow(); defers the AetherTX startup restore to showEvent |
Yes (incl. the Hyprland tiling rationale) | In scope |
StagePage.{h,cpp} |
makeCanonHeader(), canonBodyStyleSheet() |
Yes | In scope |
ThemeManager.h, ThemeSeedGenerated.cpp, both theme JSONs |
widget-aware withAlpha, canon scope, regenerated seed |
Yes | In scope |
ClientComp*, ClientGate*, ClientDeEss*, ClientTube*, WaveformWidget (9 files) |
color(token) → color(this, token), mechanical |
Yes | In scope — required for the scope to be visible |
Strip*Panel (10 files) |
hard-coded hex → tokens | Yes | In scope |
StripFinalOutputPanel.cpp |
also moves the waveform title row onto the plot | Body calls it out, no requirement needs it | Scope note, not a blocker — it's a layout change riding a re-skin PR; fine to keep since you disclosed it, but say so in the commit message too |
docs/ (5 files), tests/ (2) |
doc + test updates | Yes | In scope |
docs/architecture/aetherd-touchpoints.md is the regenerated manifest (169→171). No CHANGELOG.md entry — correct.
Blockers
1. src/gui/StripFinalOutputPanel.cpp:208 and src/gui/StripRxOutputPanel.cpp:84 — the output level meter's track goes to a mid-grey in Default Light, and the bar on it is effectively invisible.
p.setBrush(ThemeManager::instance().color(this, "color.canvas.background"));color.canvas.background is WorkspaceCanvas's backdrop token (src/gui/workspace/WorkspaceCanvas.cpp:862 is its only other consumer), and in resources/themes/default-light.json:43 it is #7b7b7c — a mid grey. The canon scope does not remap it, so inside these windows the meter track stays #7b7b7c while the ground is #f5f5f8. Resolving the gradient (:227-230) against it:
| Bar stop | On old literal #0a0e16 |
On color.canvas.background light #7b7b7c |
|---|---|---|
accent.success #1a8040 |
3.87:1 | 1.18:1 |
accent.warning #d08010 |
— | 1.36:1 |
accent.danger #c02020 |
— | 1.43:1 |
At 1.18:1 the green portion of the bar is indistinguishable from its own track. This is the TX output level meter — the thing an operator watches to avoid overdriving the transmitter — and the RX meter has the identical change. It also falsifies the body's "Default Light compared against main: the new readability problems are none"; the visual comparison missed it. tools/check_a11y.py has no contrast rule, so nothing gates it.
Not covered by #6239's "light-theme canon values are AI-generated, dark is the canon priority" note: color.canvas.background is a base token, not a color.canon.* one.
The one-line swap, which the canon scope does remap (→ canon.raised: #ffffff light, #0a121e dark):
p.setBrush(ThemeManager::instance().color(this, "color.background.0"));Light becomes green 4.99 / amber 3.10 / red 6.05; dark becomes 3.76 / 6.06 / 3.10. Both clear the 3:1 graphical-object floor in every stop, where today's light does not. Dark loses a little (4.0→3.76 green, 3.3→3.1 red) — if you'd rather not move dark at all, a dedicated color.meter.bar.background token is the cleaner answer, but color.background.0 is correct today and needs no theme change.
Nits
src/gui/StripFinalOutputPanel.cpp:227-230— the body says the −12 dB stop was dropped "because no token maps it", but one does:color.meter.bar.fillGradient(resources/themes/default-light.json:157-167) is a 5-stop theme-aware gradient with exactly this green → yellow-green → amber → red ramp, it has a widget-aware accessor (ThemeManager.h:230), and the sibling meters in these same two windows already consume it —ClientCompMeter.cpp:238andClientLevelMeter.cpp:111. Using it keeps the calibration stop and makes the three meters agree. As written,0.00anddbToRatio(-12.0f)are bothcolor.accent.success, so the duplicate stop is also dead weight.src/gui/StripFinalOutputPanel.cpp:1260-1263— the flashing LIMIT indicator now hasbackground: {{color.accent.danger}}andborder: 1px solid {{color.accent.danger}}, where it was#ff3030on a lighter#ff8080border. Same token on both means the outline disappears on the flash frame.color.accent.brightor a lightened variant restores the two-tone read. Same shape insetQuindarActive()at:1282-1284.docs/style/aethersdr-style-guide.md:97— "remaps the basecolor.background.*,color.text.*,color.border.*,color.accent*,color.knob.*andcolor.slider.*tokens" overstatescolor.accent*: onlycolor.accentandcolor.accent.brightare in the scope.color.accent.dim,.warning,.dangerand.successare untouched, and the convertedClientComp*/ClientGate*widgets read them heavily — so those stay base-palette inside the canon windows. Worth being precise, since the next surface migration will read this table.src/gui/StagePage.cpp:138— "they get their panel back after the rule above" points at a rule that is the first line of the same returned string, not something above it.src/gui/MainWindow.cpp:10192—m_aetherialStrip->setAttribute(Qt::WA_QuitOnClose, false)is now redundant;CanonWindow'sKind::Workspacector (CanonWindow.cpp:49-51) already does it. Harmless, but the comment there now describes someone else's job.src/gui/MainWindow.cpp:3957— the AetherTX restore is gated onMainWindow::showEvent, so a start where the main window is never shown (tray/minimised launch) silently skips it and leavesAetherialStripVisible=True. Low-likelihood, and the Hyprland bug is the worse failure, so this is the right trade — just noting it's now a possible path.
Security audit
Required by the harness (CodeGuard: 423 findings / 14 rule IDs). Ran, no findings attributable to this PR. Only three of the 33 flagged files are in this diff (src/core/ThemeManager.h, src/gui/MainWindow.{h,cpp}), and their changed lines are Qt widget lifecycle and theme-token plumbing — no I/O, parsing, credentials or process execution. All 14 flagged rule IDs are false positives on this codebase and I verified each rather than repeating it: CS-VLN-CMDI-AMP/PIPE/SEMI/BACKTICK fire on C++ &/&& (Qt connect(…, &Class::signal, …)), bitwise-or flag combinations, the statement terminator, and markdown backticks in doc comments; CS-VLN-SSRF-* on a fixed-loopback in-process simulator, on 192.168.x in comments next to code that denies non-discovered targets (AutomationServer.cpp:6960-6999), and on hex colour constants and VITA-49 OUIs; CS-SEC-PASSWORD on setEchoMode(QLineEdit::Password) and on the code that refuses to serialise masked fields. A substantive sweep found no QProcess/system()/popen sink in the audited non-GUI files and no unsafe C string functions anywhere in them.
Two pre-existing items outside this diff, credited to security-audit (CodeGuard), for separate routing — not this PR's to fix:
src/gui/CopyAssistController.cpp:1343— the remote-ASR API key is persisted into plaintextAppSettings(setValue("AsrRemoteApiKey", …)) while the automation-bridge token already goes through QtKeychain (MainWindow_Session.cpp:3085).AGENTS.md's credential rule applies. The endpoint placeholder is alsohttp://with no scheme check, so theBearerkey can go in cleartext.src/gui/SpectrumWidget.cpp:10140—openUrl(QUrl("https://www.qrz.com/db/" + call))concatenates an unencoded callsign thatDxSpotLineParser.cpp:113captures as bare(\S+)from a DX cluster. Bounded (authority already terminated; needs an operator right-click), butQUrl::toPercentEncoding()is the fix.
Socket tests: none added or modified. The two changed test files (tests/canon_window_test.cpp, tests/theme_manager_test.cpp) contain no QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost, and no fake peer.
What I tried to break
- The new
theme_manager_testblock, by hand. Flattened both bundled theme JSONs and checked all 15canon-scope mappings againstcolor.canon.*. Every one matches — dark by repeated literal, light by{alias}— so the block passes, and drifting one dark hex by a digit fails it, as the body claims. Thecanon_window_testblock likewise fails with theKind::Workspaceguard removed: the dialog arm assertsclicks == 2on the same keystrokes, so it isn't asserting the implementation back to itself. - Every token the PR newly references.
color.canvas.background,color.toggle.footer.{danger,success}.*,color.background.tx,color.background.warning,color.meter.gainReductionall resolve in both themes — no unresolved token (walked the nested JSON rather than grepping dotted paths, which returns nothing even when defined). That walk is also what surfaced the blocker. - "None of the converted files reads the three tokens that the
applet/tx|rx|compscopes override." Extracted all nineseedScopedToken("applet/…")entries and grepped every changedsrc/gui/*.cppforcolor.knob.foreground,color.slider.foreground,color.toggle.accent.background.checked. Zero hits. The claim holds: making these widgets scope-aware cannot change a docked applet. - "The
Strip*Panels trade their hard-coded hex colours for tokens."git diff | grep '^+' | grep -oE '#[0-9a-f]{6}'over the whole range yields 13 distinct literals: twelve are thecanon-scope values in the two theme JSONs, and exactly one is in code — the#ffffffon the flashing LIMIT label. The sweep is complete. - "Strip panels are constructed only in these two windows." All 16
new Strip*Panelsites are inAetherRxDialog.cpporAetherialAudioStrip.cpp. The re-skin cannot leak to a docked applet or floating editor. - Geometry carry-over. Base64
saveGeometry()blobs contain no comma, sosplit(',').size() != 4andCanonWindow::restoreGeometryFromSettings()falls through torestoreGeometry()— oldAetherRxDialogGeometry2/AetherialStripGeometry2rectangles do carry over, then get replaced in the new form on the next save. - Dangling pointer on
WA_DeleteOnClose.ensureAetherRxDialog()now hand-rolls create-or-raise, so I checked the member:MainWindow.h:1692isQPointer<AetherRxDialog>, which nulls on delete, sowasFreshis correct and the second open re-creates rather than dereferencing a freed dialog. - The deleted #4266 guard.
AetherialAudioStriplostFramelessResizer::install(this, 6, kTitleBarHeight)and the comment "Reserved from the frameless resize edge so a title-bar grab starts a move, not a top-edge resize (#4266)". The symptom can't recur: there is no title bar left to grab, and plaininstall(this)is whatNetworkDiagnosticsDialog.cpp:350andWaveformsDialog.cpp:939already use. - The
keyPressEventguard's logic.&binds tighter than&&, soplainEnterparses as(mods & Keypad) && (key == Enter)— identical to QDialog's own test.event->ignore()can't leak Return to MainWindow either: Qt stops key propagation at a widget that is a window, andKind::WorkspacesetsQt::Window. - The deleted
recolour/#08121dhack.tidyEmbeddedPanel()(StagePage.cpp:112-114) still performs that replacement, and noStrip*Panelstill hard-codes#08121d— so removing the per-panel loop didn't orphan anything. - Dangling references to everything deleted.
setFramelessMode,m_titleBar,m_titleLbl,m_bodyLayout,m_restoring,EditorFramelessTitleBarin the strip: no remaining reference insrc/ortests/. - Contrast of every text pair the canon scope introduces, both themes. All clear
docs/a11y.md's 4.5:1; the lowest are darkmutedon ground at 6.39:1 and lightmutedon ground at 5.94:1. The blocker above is a graphical-object pair, not text.
Couldn't check, and not counted as clean:
canonBodyStyleSheet()'sQWidget#canonBody QWidget { background: transparent; }is an ancestor sheet, which in Qt's cascade beats the application sheet. AnyQLineEdit/QSpinBox/QComboBoxin these windows that carries no sheet of its own loses the app-wide input background and sits directly on the bloom ground. You ran the real GUI and I can't — which input widgets were in the Default Light comparison?- Runtime behaviour generally: the compositor claims (Wayland/Hyprland tiling, edge resize, ground drag), the taskbar entry, and the "Return in a Compressor knob field switched to Gate" reproduction. All of my findings are reasoned from the source and from arithmetic on the theme JSONs, not reproduced at runtime.
- CI at post time:
check-macos, Static checks, Build docs site, Sanitizer configure and all three CodeQL analyses green;buildandcheck-windowsstill in progress. Worth a glance before merge —check-windowsis the only-DENABLE_ASR=OFFlane, though nothing here is ASR-gated.
Recommendation
Request changes — for the one token swap at StripFinalOutputPanel.cpp:208 and StripRxOutputPanel.cpp:84. Everything else is either a nit or a question. This is a careful, well-evidenced migration: the scope mechanism is the right seam, the two new test blocks genuinely fail when the behaviour they pin is broken, the "nothing changes outside a scope" claim survived the greps I aimed at it, and the hex-to-token sweep is complete to one literal. Next step: swap the meter-track token (and ideally move the gradient to color.meter.bar.fillGradient while you're in there), and link the maintainer ruling that picked these two windows the way #6273's status comment does. Thanks for the thorough test plan — listing the mutation that breaks each new assertion is what made this reviewable without a build.
🤖 aethersdr-agent · cost: $16.7141 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (431 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $16.8913 · model: claude-opus-5 |
…ir pinned size, and keep colour lookups out of the paint loops. Principle VIII. Fixes from the automated review of #6334: - CanonWindow flushes saved geometry when it hides, not on close. Escape and Ctrl+W go through QDialog::reject(), which hides the window without a close event, so a position set before them was never written to disk. - CanonWindow::setLaunchSize() pins the size on every open and applies it before centring. AetherRX and AetherTX used to resize after the base had centred the restored size, so a window with a size but no usable position (an unplugged monitor) landed off-centre. - WaveformWidget resolves its bar colours once per paint rather than per column. Each scoped lookup walks the parent chain. - The Final Output stage re-applies its OVR, LIMIT and activity styles only when they change, not on every meter tick. - Removes tidyEmbeddedPanel()'s #08121d recolour, which no panel matches any more, the stale comments that referred to it, and MainWindow's WA_QuitOnClose call, which CanonWindow's workspace kind now sets. canon_window_test covers both CanonWindow changes, and each test fails when its fix is removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
On the two security scans above.
Automated code review (
Both CanonWindow fixes have tests that fail when the fix is removed. Closed without change:
|
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
…e to CanonWindow, with canon check boxes and radio buttons in every canon window. Principle VIII. (#6339) ## Summary Part of #6239 (style guide follow-ups to RFC #6226). Three canon migrations, one commit each. **1. AetherModem moves to `CanonWindow`,** following the About window's recipe for tool windows, as AetherRX and AetherTX did in #6334: - No title bar, the ambient ground, a canon header and the corner close button. Drag any empty area to move it, resize it from every edge, and close it with Esc or Ctrl+W. - It's a `CanonWindow::Kind::Workspace`: an independent top-level window with its own taskbar entry, where Return in a field never clicks a default button. - It stays a service host. MainWindow keeps one instance for the session because it owns the KISS TCP server and the APRS modem, so closing it only hides it. - Geometry persists under its old key (`Ax25HfPacketDecodeDialogGeometry`), so saved positions carry over. - The minimum height grows from 680 to 720 px, the extra height of the header that replaces the title bar, so the tabs' fields keep their height. - It sits under the `canon/aetherModem` theme scope. Its stylesheet and the D-STAR page's are rewritten in `color.canon.*` tokens: - selection and focus are canon cyan (maintainer's choice); - status dots keep green, amber and red; - the log's rich-text colours resolve through the window's scope. **2. Canon check boxes and radio buttons** (`gui/CanonIndicators.h`), now the style guide's building block: - **Look:** 18 px; ModemChrome's own indicator rules are removed from its canon look, so only the painted images draw. A checked box is cyan with an `onAccent` tick, a selected radio is a cyan ring with a centre dot, and each has a muted disabled variant. Before, a checked box only changed fill and a selected radio was only an outline, so it was hard to see what was selected. - **Painted:** they're drawn at 1x, 2x and 3x, because a stylesheet border can draw neither a tick nor a clean translucent circle. - **Cached safely:** they're cached through the new `gui/ThemedImageCache.h`, which `ComboStyle.h`'s combo arrow now uses too (maintainer's call to fold it in). It writes to the user's own cache directory, never the shared temp directory, keyed by every colour painted. Writes are atomic, and it fails closed: if the images can't be written, the rules are left out and Qt's own indicators and arrow show rather than blank ones. Paths are quoted in `url("...")`. - **Theme:** `applyCanonSheet()` rebuilds the sheet on a theme change, so the images follow the theme. - **Everywhere:** every canon window uses them: AetherModem, AetherRX, AetherTX, Network Diagnostics, Waveforms and Remote Access. **ModemChrome gains a canon look** (`ModemChrome::Look::Canon`): - **Slots:** every colour in its sheet is now a named slot filled per look. - **Modem look:** what the docked applets wear, and byte-identical to `main` (13,481 bytes at each scale, compared against the merge base). - **Canon look:** canon surfaces and ink, cyan selection, stage tabs in ink, the canon indicators and a list card. - **Who uses it:** AetherRX/AetherTX's stage column and AetherRX's AetherNR page (`AetherDspWidget` in dialog mode). Those had kept the old navy-and-green chrome. - **AetherNR page:** its hard-coded label colours become text tokens, and its painted parts resolve through the window's scope. In the docked applet, primary labels are unchanged, while the secondary and disabled labels move a shade to the theme's own values. **3. The AetherRX and AetherTX Settings dialogs** (the profile library behind the gear) are short-lived `CanonWindow` dialogs. They're still modal over their window, use the canon look, and are 40 px taller for the header. **Docs:** - `docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md` add the canon check box and radio button, ModemChrome's canon look, and the new canon windows. - The AetherModem user-guide page describes the window behaviour. **Touchpoints:** `core/ThemeManager.h` includers go 171 → 173, from `DStarModemPage.cpp` and the new `CanonIndicators.h`. Both are UI theming (ui-support), and the manifest is regenerated. **Stale screenshots, to recapture after merge:** `aethermodem-aprs.png` (`aethermodem-packet-radio.md`), `aetherrx-profiles.png` (`aetherial-audio.md`), and the AetherRX/AetherTX stage-column shots `aetherrx.png`, `aethertx.png`, `aethertx-eq.png`, `aethertx-compressor.png` and `first-session-aetherrx-nnr.png`. They show the old chrome and green indicators. **Known and unchanged from `main`:** the Mailbox tab's Listen callsign and Vanity alias fields clip their text the same way on `main`. ## Constitution principle honored Principle VIII (Evidence Over Assertion): each behavioural claim below has a test that fails without its fix, a byte comparison, or a render. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] `canon_window_test` samples the 3x indicator images: the checked box shows its tick, the selected radio its dot, and the unchecked ones neither. Removing the tick and the dot fails it. - [x] `modem_chrome_test`: both looks fill every `%N` argument and `@slot@`, only the canon look carries the canon indicators, and the canon look's only indicator rules are the painted ones. - [x] `canon_window_test` also covers the cache: - the key changes when `aqua` or `line` change; - an unwritable cache gives empty indicator rules, and they recover once it's writable; - the combo arrow's rule likewise disappears when blocked and names a quoted path otherwise. Each of these fails when its fix is removed. - [x] The modem look is byte-identical to the merge base at both scales: old and new `ModemChrome.cpp` built side by side, and their output compared. - [x] `canon_window_test`, `modem_chrome_test`, `stage_tab_bar_drag_test`, `theme_manager_test`, `theme_seed_test`, `compact_metrics_test`, `rx_chain_runner_test`, `rx_stage_reorder_test`, `window_show_state_test` and `comp_makeup_fader_test` pass locally. - [x] The static-check scripts pass locally. The touchpoint manifest is regenerated. - [x] The user docs site builds (`npm ci && npm run build`), and `gen_reference --check` passes. - [x] Rendered offscreen with the demo app on scratch settings: - all six AetherModem tabs; - AetherRX (AetherNR and Gate) and AetherTX; - Network Diagnostics (Overview and Application Logs); - Waveforms; - both Settings dialogs; - a 2x render of the indicators. - [x] The maintainer ran the AetherModem build against the FLEX-8600. The check box and radio fix came from that run. - [ ] Remote Access was not rendered. It only opens with a radio running the tailnet shim connected; its change is the same shared indicator rules. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls. Existing geometry keys are reused. - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/aethermodem-packet-radio.md`, `docs/style/*` - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, and every close path stops its clients and an armed beacon. Principle VI. (#6340) Builds on #6339, which has merged: AetherMap's check boxes use its canon indicators (`gui/CanonIndicators.h`, `applyCanonSheet()`, cached through `gui/ThemedImageCache.h`). ## Summary Part of #6239. The PSK Reporter map becomes **AetherMap**, a `CanonWindow` workspace, following the About window's recipe for tool windows as AetherRX, AetherTX (#6334) and AetherModem (#6339) do: - **Name:** retitled AetherMap in the window title, the canon header and the menu item (**Tools → AetherMap…**). - **Chrome:** no title bar, the ambient ground and the corner close button. Drag any empty area to move it, resize it from every edge, and close it with Esc or Ctrl+W. - **Kind:** `CanonWindow::Kind::Workspace`, an independent top-level window with its own taskbar entry. - **Geometry:** persists under its old key (`PskReporterMapGeometry`), so saved positions carry over. - **Size:** the minimum grows from 720×480 to 720×520 for the header. **Look:** it sits under the `canon/aetherMap` theme scope with a canon sheet in Network Diagnostics' vocabulary: - each sidebar section is a raised card, fields sit on control, and selection and focus are cyan; - check boxes use the canon indicators; - the connection dot uses the status tokens. The band-condition pills keep the palette they share with the Propagation Dashboard, and the map's hover card keeps its own colours. **Close-path fix (Principle VI).** Closing used to work only through `closeEvent()`, which stopped the PSK Reporter clients and the weather radar and cancelled an armed or transmitting WSPR beacon. Esc goes through `QDialog::reject()`, which hides the window without a close event. On `main`, pressing Esc in the PSK Reporter window leaves the clients polling and a WSPR beacon armed behind a hidden window. The logic now lives in `done()`, which every close path reaches: the close button and the window manager (through `QDialog::closeEvent()`), Esc and Ctrl+W (through `reject()`), and quitting the app. `MainWindow::closeEvent()` now closes a visible AetherMap before it disconnects the radio; before, Qt closed it only after the disconnect. `done()` uses the destructor's test, so a held TX request counts too. **Automation:** the sidebar's content widget is named `pskReporterSidebarContent`, so the automation bridge can reach it. **Docs:** - The user-guide page is titled AetherMap, at the same URL (`/psk-reporter-map`). - The menu reference, *Before you transmit*, the docs style guide's menu-path example and every link to the page use the new name. - `docs/user/screens.json`: the `psk-reporter-sidebar` capture recipe invokes `AetherMap...`, and its caption, alt text and the Tools-menu alt use the new name. `embed_screenshots.py --check` passes. - `docs/style/*` list AetherMap among the canon windows. - **Stale screenshots, to recapture after merge:** `psk-reporter-sidebar.png` (old title bar and indicators) and `menu-tools.png` (still shows "PSK Reporter..."). - The `versioned_docs` stable copies are untouched; they describe the shipped release. ## Constitution principle honored Principle VI (TX safety): every way of closing the window cancels an armed or transmitting WSPR beacon. Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] Quit ordering: a temporary log line in `done()`, with AetherMap open, then **Quit**. With the change the log reads `main_window.close_event begin` → `PROBE_AETHERMAP_DONE` → `radio.disconnect begin`; without it, `radio.disconnect` → `close_event end` → `PROBE_AETHERMAP_DONE`. - [x] Close path: a throwaway QtTest harness, linked against the app's objects, shows AetherMap and presses Esc. With the change: `visible=0 running=0`. With the stop logic skipped (the old behaviour): `visible=0 running=1`. The harness is not committed: constructing the window needs most of the desktop sources, which no test target links. - [x] `canon_window_test`, `modem_chrome_test`, `window_show_state_test` and `theme_manager_test` pass locally. - [x] The static-check scripts pass locally. The touchpoint manifest is unchanged. - [x] The user docs site builds (`npm ci && npm run build`), and `gen_reference --check` passes. - [x] Rendered offscreen with the demo app on scratch settings: the window, and the full sidebar grabbed as `pskReporterSidebarContent`. - [ ] Not exercised: transmitting a WSPR beacon and then closing with Esc (the bridge keeps TX verbs gated, and no live radio was used). The harness covers the shared stop path. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls. The existing geometry key is reused. - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/psk-reporter-map.md` and the pages that name it, `docs/style/*` - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… palette. Principle VIII. (#6341) Builds on #6339 and #6340, both merged. ## Summary Part of #6239. **Connect to Radio** becomes a `CanonWindow`, following the About window's recipe as AetherRX, AetherTX (#6334), AetherModem (#6339) and AetherMap (#6340) do: - **Chrome:** no title bar, the ambient ground, a canon header ("Connect to a Radio", replacing the in-body heading) and the corner close button. Drag any empty area to move it, resize it from every edge, and close it with Esc or Ctrl+W. - **Kind:** still a parented dialog (`Kind::Dialog`). MainWindow still anchors it above the status bar; the new `setOwnerPlaced()` stops CanonWindow from centering it on first show. - **Return:** the new `setReturnClicksDefault(false)` means Return in a field (an IP address, a SmartLink password) only does what that field does. Without it, QDialog would also press the first auto-default button. - **Frameless mode:** the window no longer follows **View → Frameless Window**. `setFramelessMode()`, the frameless title bar, the opaque `paintEvent()` and the framed branch of `screenFitFrameMargins()` are removed. The #4515 screen fitting and the #4725 xcb re-show path are unchanged. **Look:** it sits under the `canon/connection` theme scope, with a canon sheet in Network Diagnostics' vocabulary: - groups are raised cards with muted titles, the radio list is nested and fields sit on control; - mode cards are raised, and the selected one has a cyan edge; - selection and focus are cyan, and check boxes use the canon indicators; - hint, info and error labels use canon muted, canon cyan and `accent.danger`. **CanonWindow:** - gains `setReturnClicksDefault()` (the Workspace kind's Return handling, now a setting a dialog can turn off) and `setOwnerPlaced()`; - now owns the header helper `makeCanonHeader()`, moved from StagePage, so every canon window can reach it without the stage-page sources. **Touchpoint manifest:** StagePage drops its now-unused ThemeManager include, so the `ThemeManager.h` includer count goes from 173 to 172. There are no new touchpoints. **Docs:** - `first-connection.md` describes the window. - `docs/style/*` list Connect to Radio among the canon windows, and `dialog-patterns.md` documents the two new options. - **Stale screenshot, to recapture after merge:** `connect-dialog.png` (it still shows the old title bar and heading). ## Constitution principle honored Principle VIII (Evidence Over Assertion): the checks below, each mutation-verified. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] `canon_window_test`: a dialog with Return opted out ignores Return in a field, and an owner-placed window keeps its position on first show. Each check fails when its option is removed. - [x] `connection_panel_size_test`: the framed cases are dropped, since the window has no frame any more. The growth check now settles the layout over two event-loop turns, because the body sits one layout deeper. It still fails with `fitToScreen()`'s growth branch disabled. - [x] `startup_autoconnect_lockout_test` passes, now linking `CanonWindow.cpp` instead of `FramelessWindowTitleBar.cpp`. - [x] `startup_autoconnect_lockout_test` also checks that attaching an already signed-in SmartLink client replaces the sign-in hint with the account. No socket is opened. The check fails when the account line's state is read back from its stylesheet, as it was before the review fix. - [x] The static-check scripts pass locally, and the touchpoint manifest is regenerated. - [x] Built locally for manual testing: the window opens above the status bar; Esc, Ctrl+W and ✕ close it; it can be dragged; all three pages are canon. ## Checklist - [x] Commits are signed - [x] No new `AppSettings` keys - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/first-connection.md`, `docs/style/*` - [x] Security-sensitive changes: n/a (the SmartLink login fields are only restyled) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…inned to the canon palette. Principle VIII. (#6364) ## Summary Part of #6239. The GPS window becomes **AetherGPS**, a `CanonWindow` workspace. It follows the recipe AetherRX and AetherTX (#6334), AetherModem (#6339) and AetherMap (#6340) use: - **Name:** AetherGPS in the window title, the canon header, the menu item (**Tools → AetherGPS…**) and the tooltip on the status-bar GPS indicator. - **Chrome:** no title bar, the ambient ground, the canon header and the corner close button. Drag any empty area to move it, resize it from any edge, and close it with Esc or Ctrl+W. The footer **Close** button is gone. - **Kind:** `CanonWindow::Kind::Workspace`, an independent top-level window with its own taskbar entry. It no longer follows **View → Frameless Window**, so `showGpsLocationDialog()` creates it directly instead of through `showOrRaisePersistent()`, which would set frameless mode on it. - **Geometry:** persists under its old key (`GpsLocationDialogGeometry`), so saved positions carry over. - **Size:** the minimum grows from 860×600 to 860×640, and the first-open size from 1080×720 to 1080×760, to make room for the header. **Look:** it sits under the `canon/aetherGps` theme scope, with a canon sheet in AetherMap's vocabulary: - the top status strip and each section are raised cards; the metric tiles, the NTP tip and the clock-sync group are nested; - section titles are cyan, values ink and field titles muted; - buttons and the NTP server field are canon controls, and check boxes use the canon indicators; - the satellite bar keeps its green fill, now the `color.background.success` token instead of the `#17663f` and `#ffffff` literals. The object name `gpsLocationDialog` is unchanged because `IcomControls.cpp` refers to it. **Docs:** - The user guide, menu reference, Radio Setup page, in-app help and the Tools-menu alt text in `docs/user/screens.json` use the new name. - The GPS page has a "Using AetherGPS" section that describes the window. - `docs/style/*` list AetherGPS among the canon windows. - **Stale screenshot, to recapture after merge:** `menu-tools.png` still shows "GPS Dashboard...". ## Constitution principle honored Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] Rendered offscreen with the app on scratch settings and the radio disconnected, opened via **Tools → AetherGPS…** over the automation bridge: the canon header, the cards, the cyan section titles and the corner ✕ draw. - [x] The static-check scripts pass locally. The touchpoint manifest is unchanged. - [x] `embed_screenshots.py --check` and `gen_reference.py --check` pass. - [ ] Not exercised: the window with live GPS data, since no radio was connected. No test covers this window, and this PR adds none. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls. The existing geometry key is reused. - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/aetherclock-and-gps.md` and the pages that name the window, `docs/style/*` - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <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>
… and re-take the Stable snapshot. Principle VIII. (#6396) ## Summary This brings the user guide (docs.aethersdr.com) up to date with everything in v26.10.2 and re-takes the Stable snapshot so the default site shows it. Docs only, apart from the TCI page prose in `tools/docs/gen_reference.py`; no application source, test or CI changes. Every page was audited against the 106 PRs in the release. Every fact that was added or kept was checked against the source at `f753dbfc4`, the v26.10.2 release commit, not taken from the PR bodies alone. ## What changed - **Menu paths.** - AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on **Tools**. They are created on View and then moved to Tools (`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's example says **Tools → AetherMap...**. - **Show live CW contacts** is on **View**. - Waveforms is on **Tools**, not File. - The VUDU applet button was called TXDSP. - Labels now match the source's `...` vs `…`. - **Gaps from this cycle.** - **Windows and Network Diagnostics:** Windows Firewall rules and the Network Diagnostics Windows Firewall page (#6289); the `aether.network` and `aether.nvafx` log categories (#6289, #6308). - **Remote access:** Tailscale shim revocation semantics (#6325). - **Sleep, App Nap and power throttling (#6195):** per-OS notes. - **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level, multi-interface discovery and a discovery troubleshooting entry (#6259, #6254, #5975, #6009, #6293). - **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply voltage and link status (#6282, #6129, #6137, #6211). - **NB2** on the HL2 and ANAN-G2 (#5966). - **DVK:** WAV import requirements, the slot menu without Delete, status text and licensing (#6247, #6262, #6251). - **Copy Assist** from unprocessed audio (#6216). - **Live CW contacts** replace the decoder's contact card (#6268). - **Status-bar indicators** work from the keyboard (#6267). - **Volume:** controllers, hotkeys and TCI `volume` follow the active output (#6129, #6260). - **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes (#6031, #6038). - **RTL-SDR:** live PPM (#6082). - **Icom:** COMP and the MK2 squelch scale (#6174, #6184). - **Known issues.** Entries whose GitHub issues are closed are removed (each checked with `gh issue view`). As decided, this includes three closed today without a demonstrated fix: #5107, #4824 and #5665. - **US spelling.** `docs-style-guide.md` asked for British spelling; it now asks for US, by the maintainer's decision, and every page is converted. The generated TCI pages are converted at their source, the prose strings in `tools/docs/gen_reference.py`. Code spans, links and URLs were left alone. UI labels were checked against the source strings, and none of them are British. Two in-page anchors and two Log Analyzer rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings. - **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the release tree with these corrections, before `main` was merged in, so Stable carries no post-release docs. `docs/` also has `main`'s post-release pages, in US spelling. Stable alone keeps the IC-9700 waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so 26.10.2 still has it. The PDF manual already attached to the v26.10.2 release keeps the tag's docs. ## Validation - `npm ci && npm run build` in `docs/user`: success. This is the broken-link and broken-anchor check. - `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules. - `python3 tools/docs/gen_reference.py --check`: up to date (8 pages). - `git diff --check`: clean. - Menu paths on a build of this branch (offscreen, isolated settings, no radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools → Waveforms...`, `View → Show live CW contacts`. ## Screenshots now out of date (not recaptured) These need a recapture pass on the radio: - **Old panadapter title strip (#6386):** `panadapter`, `main-window`, `first-session-*`, `multi-slice-two-slices`, `split-pair`, `tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`, `default-light-theme`. - **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`, `aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`, `connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`, `rx-agc-calibration`. - **Other:** - `title-bar` (lowercase tab states, #6281); - `menu-help` (#6294); - `display-panel` (#6371); - `support-diagnostics-dialog`; - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0); - `about-window` (v26.10.1); - `radio-setup-qrz`. ## Found during the audit, not fixed here - **App string bug:** `TailnetShimDialog.cpp:628` tells the user to "Connect remotely with Connect → Manual"; the real path is **File → Connect to Radio... → Connect by IP**. - **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the slider position as dB, which is wrong on ANAN. #6282 lists this as a follow-up. - **MIDI Master Volume on non-Flex radios:** it is refused while PC Audio is off, whereas hotkeys, FlexControl and HID drive the line out. The docs describe the code as it is. - **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the source strings are `FMN` and `CWR`. Left as is until someone confirms what the mode menu displays. - **IC-705 COMP readback:** inferred from the MK2 (#6174) and not verified on an IC-705. - **Not yet documented:** the #6230 Radio Health row "Forward responses skipped", and the Store/MSIX package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Part of #6239 (listed there under "Picked: tool windows"). AetherRX and AetherTX move to
CanonWindow(the visual canon, RFC #6226), using the About window's recipe the way Network Diagnostics and Waveforms do, and their contents are re-skinned to the canon palette.Chrome. Each window gets:
FramelessResizeron every edge.Saved positions carry over through
setGeometryKey()on the existing keys (AetherRxDialogGeometry2,AetherialStripGeometry2) and are flushed whenever the window hides, including on Escape. Both windows still pin their size at 720×480 on every open, through the newCanonWindow::setLaunchSize().AetherTX loses its inline 18 px title bar, its minimize/maximize buttons, and its own geometry and frameless-toggle code. Neither window follows View → Frameless Window any more. Closing AetherTX still hides it rather than deleting it, because MainWindow keeps feeding it state.
Independent workspace windows. Both use the new
CanonWindow::Kind::Workspace. It's an independentQt::Window, so AetherTX keeps its own taskbar entry (#2301), minimises on its own and can live on another monitor.WA_QuitOnCloseis off. In a workspace, Return and Enter in a field never click a default button. QDialog makes every push button an auto-default, so Return in a knob's value field had also pressed the Gate tab.Kind::Dialog(About, Waveforms, Network Diagnostics) is unchanged.Re-skin through a theme scope. A new
canonscope in both bundled themes maps these tokens tocolor.canon.*:color.background.0–3color.text.primary/secondary/labelcolor.border.subtle/strongcolor.accent,color.accent.brightcolor.knob.*,color.slider.*The windows sit under
canon/aetherRxandcanon/aetherTx. Content they share with other surfaces (the AetherNR widget, the curve, meter and knob canvases) turns canon inside these two windows only. Docked applets and floating editors look the same as before.The painted widgets involved now look colours up through the widget's scope:
color(this, …), plus a new widget-awaretheme::withAlpha(widget, …)overload. This changes nothing outside a scope. None of the converted files reads the three tokens that theapplet/tx|rx|compscopes override. TheStrip*Panels, constructed only in these two windows, trade their hard-coded hex colours for tokens.Startup fix. AetherTX's startup restore now waits for MainWindow's first show. Shown before its parent was mapped, a tiling compositor (Hyprland) took the parentless dialog for a top-level window and tiled it, and CanonWindow then saved the tiled size.
Final Output layout. The waveform's title row now sits directly on top of the plot. The scope has a fixed height, so the spare room goes above the title row instead of between the row and the plot.
Visible changes to call out
color.canvas.background.docs/user/docs/aetherial-audio.mddescribes the new window behaviour. Thedocs/userscreenshots of these windows (aethertx*.png,aetherrx.png,first-session-aetherrx-nnr.png) show the old chrome and can be recaptured in a follow-up.Constitution principle honored
Principle VIII (Evidence Over Assertion): every behavioural claim here has a test, a mutation that breaks it, or a runtime reproduction, listed below. The visual direction is RFC #6226's.
Test plan
theme_manager_test: a new block pins everycanonscope mapping to itscolor.canon.*token in both bundled themes, plus a check that root is untouched. Dark repeats the canon hex values in the scope, because scope values can only alias primitives. Changing one dark value by one fails the test.canon_window_testalso pins bothCanonWindowkinds: window type,WA_QuitOnClose, and Return or keypad Enter in a field (a workspace ignores them; a dialog clicks its default). Disabling the guard fails it. A throwaway harness linked against the app's objects reproduces the review's scenario on the real AetherTX window: Return in a Compressor knob field switches to Gate with the guard off and stays on Compressor with it on.canon_window_testalso covers two geometry behaviours. Escape flushes the saved geometry to disk (it survivesAppSettings::load()). A pinned launch size wins over a saved 1420×900 and centres when the saved position is on no screen. Each test fails when its fix is removed.npm ci && npm run build), andgen_reference --checkandtest_log_rulespass.canon_window_test,theme_seed_test,stage_tab_bar_drag_test,modem_chrome_test,compact_metrics_test,comp_makeup_fader_test,rx_chain_runner_test,rx_stage_reorder_testandwindow_show_state_testpass locally.main: the new readability problems are none, and the light-theme knob value text was already near-invisible onmain.canon_window_testcovers the move fallback offscreen.Checklist
AppSettingscalls. Existing keys are reused.MeterSmoother(unchanged)docs/user/docs/aetherial-audio.md,docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md🤖 Generated with Claude Code