Fix panadapter layout and floating restore#2780
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves multi-panadapter UI persistence and stability by restoring saved layouts and floating pan state across restarts, correcting FFT sizing inputs after layout changes, and hardening the macOS GPU (QRhiWidget) reparent/shutdown lifecycle to avoid crashes.
Changes:
- Persist/restore panadapter layout and floating-pan IDs across sessions, with improved fallback behavior for 1–8 pan layouts.
- Use FFT-pane height (not total widget height) for radio
ypixels, and relock the auto noise-floor baseline after geometry/ypixels changes. - Add macOS GPU reparent/shutdown preparations (backing-store notification + QRhi/native resource teardown) and guard late callbacks during shutdown.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/gui/SpectrumWidget.h | Adds shutdown/reparent preparation APIs and FFT-pane height accessor used for dimension reporting. |
| src/gui/SpectrumWidget.cpp | Implements QRhiWidget lifecycle safeguards, FFT-pane height calculation, dimension change emission updates, and shutdown render/resource guards. |
| src/gui/PanadapterStack.h | Tracks seen pan IDs to preserve floating state for pans not present in the current run; adds shutdown signaling. |
| src/gui/PanadapterStack.cpp | Refines layout selection for docked vs floating scenarios, preserves/restores floating IDs, and adds QRhiWidget reparent preparation in layout/float/dock paths. |
| src/gui/MainWindow.h | Adds explicit panadapter UI shutdown preparation hook/state. |
| src/gui/MainWindow.cpp | Updates layout restore/persistence logic (incl. up to 8 pans), uses FFT-pane ypixels, reacquires noise-floor lock on dimension changes, and centralizes panadapter shutdown prep. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Debounced layout restore: after all pans are added on connect, | ||
| // rearrange to the saved layout (e.g. 2h instead of default vertical). | ||
| if (!m_layoutRestoreTimer) { | ||
| m_layoutRestoreTimer = new QTimer(this); | ||
| m_layoutRestoreTimer->setSingleShot(true); | ||
| m_layoutRestoreTimer->setInterval(1000); | ||
| connect(m_layoutRestoreTimer, &QTimer::timeout, this, [this]() { | ||
| m_layoutRestoreTimer->setProperty("fired", true); | ||
| if (m_shuttingDown || !m_panStack) { | ||
| return; | ||
| } | ||
| // The radio restores pans from the GUIClientID session. | ||
| // Accept whatever the radio gives and arrange based on count. | ||
| const int panCount = m_panStack->count(); | ||
| if (panCount > 1) { | ||
| // Pick a layout based on the number of pans the radio restored | ||
| const QString saved = AppSettings::instance() | ||
| .value("PanadapterLayout", "1").toString(); | ||
| // Only rearrange if the saved layout matches the pan count | ||
| static const QMap<QString, int> layoutPanCount = { | ||
| {"1", 1}, {"2v", 2}, {"2h", 2}, {"2h1", 3}, {"12h", 3}, {"3v", 3}, {"2x2", 4}, {"4v", 4} | ||
| }; | ||
| if (layoutPanCount.value(saved, 1) == panCount) | ||
| m_panStack->rearrangeLayout(saved); | ||
| else if (panCount == 2) | ||
| m_panStack->rearrangeLayout("2v"); // default 2-pan to vertical | ||
| else if (panCount == 3) | ||
| m_panStack->rearrangeLayout("2h1"); // default 3-pan | ||
| else if (panCount >= 4) | ||
| m_panStack->rearrangeLayout("2x2"); // default 4-pan | ||
| const QString layoutId = panCountForLayoutId(saved) == panCount | ||
| ? saved | ||
| : defaultPanLayoutForCount(panCount); | ||
| const QString floatingPanIds = AppSettings::instance() | ||
| .value("FloatingPanIds", "").toString(); | ||
| m_panStack->rearrangeLayout(layoutId); | ||
| AppSettings::instance().setValue("FloatingPanIds", floatingPanIds); | ||
|
|
||
| // Optimistically set local yPixels immediately so FFT frames | ||
| // arriving before the radio echoes back use correct scaling (#1511). | ||
| for (auto* a : m_panStack->allApplets()) { | ||
| auto* s = a->spectrumWidget(); | ||
| auto* p = m_radioModel.panadapter(a->panId()); | ||
| if (!s || !p || !p->panStreamId()) continue; | ||
| int y = s->height(); | ||
| if (y >= 100) | ||
| m_radioModel.panStream()->setYPixels(p->panStreamId(), y); | ||
| if (panPixelDimensionsReady(s)) { | ||
| m_radioModel.panStream()->setYPixels( | ||
| p->panStreamId(), panYpixelsFor(s)); | ||
| s->reacquireNoiseFloorLock(); | ||
| } | ||
| } | ||
|
|
||
| // Defensive re-push xpixels for all pans after layout settles. | ||
| // Covers race where radio hadn't finished pan init when first push arrived. | ||
| QTimer::singleShot(500, this, [this]() { | ||
| if (m_shuttingDown || !m_panStack) { | ||
| return; | ||
| } | ||
| for (auto* applet : m_panStack->allApplets()) { | ||
| auto* sw = applet->spectrumWidget(); | ||
| auto* pan = m_radioModel.panadapter(applet->panId()); | ||
| if (!sw || !pan) continue; | ||
| int xpix = qMax(sw->width(), 1024); | ||
| int ypix = qMax(sw->height(), 200); | ||
| const int xpix = panXpixelsFor(sw); | ||
| const int ypix = panYpixelsFor(sw); | ||
| m_radioModel.sendCommand( | ||
| QString("display pan set %1 xpixels=%2 ypixels=%3") | ||
| .arg(pan->panId()).arg(xpix).arg(ypix)); | ||
| if (pan->panStreamId()) | ||
| if (pan->panStreamId()) { | ||
| m_radioModel.panStream()->setYPixels(pan->panStreamId(), ypix); | ||
| sw->reacquireNoiseFloorLock(); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Restore floating-pan state saved from the previous session. | ||
| // Runs for any pan count so a single floated pan is also restored. | ||
| m_panStack->restoreFloatingState(); | ||
| }); | ||
| } | ||
| if (!m_layoutRestoreTimer->property("fired").toBool()) { | ||
| m_layoutRestoreTimer->start(); | ||
| } | ||
| m_layoutRestoreTimer->start(); | ||
| }); |
| QString defaultDockedLayoutForCount(int panCount) | ||
| { | ||
| static const QMap<int, QString> kDefaultLayouts = { | ||
| {1, QStringLiteral("1")}, | ||
| {2, QStringLiteral("2h")}, | ||
| {3, QStringLiteral("2h1")}, | ||
| {4, QStringLiteral("2x2")}, | ||
| {5, QStringLiteral("3h2")}, | ||
| {6, QStringLiteral("2x3")}, | ||
| {7, QStringLiteral("4h3")}, | ||
| {8, QStringLiteral("2x4")} | ||
| }; | ||
| return kDefaultLayouts.value(panCount, QStringLiteral("1")); |
|
Pushed 9d4af9f: removed the stale post-dBm-drag FFT preview offset so display scaling now relies on the immediate PanadapterStream range update plus the pending echo guard and FFT smoothing reset. Verified with git diff --check and cmake --build build. |
Persist selected and floating panadapter layouts across restarts while preserving saved state for pans that are unavailable in a given session. Use the actual FFT pane height for pan ypixels, clamp decoded FFT ranges, and stabilize dBm/noise-floor updates around range changes. Prepare QRhiWidget-backed spectrum widgets before top-level reparenting and shutdown so macOS GPU cleanup callbacks do not crash during float/dock/quit paths.
9d4af9f to
e72d7e4
Compare
|
Squashed the PR down to one commit: e72d7e4 (Fix panadapter layout persistence and dBm scaling). Re-ran git diff --check against upstream/main and cmake --build build after the squash. |
|
Claude here. First off — thanks for this work, Robbie. The panadapter persistence + macOS GPU lifecycle fixes are genuinely valuable and well-scoped, and the PR description is excellent. Before merging, I ran a stale-base audit and found that the Codex-generated commit (`e72d7e48`) silently reverts two recently-landed PRs — almost certainly an unintentional stale-snapshot pattern from how Codex captures source for editing. Specifically, the diff against current main shows:
I verified the reverts by running a three-way merge dry-run; the merge succeeded textually but the resulting tree had #2770 and #2772 undone. The same pattern (file-level stale snapshot) hit Claude Code earlier and is documented in ten9876/aetherclaude#34 — sounds like Codex has a similar capture-then-write pattern that doesn't reconcile with files changed between snapshot and patch. Since the unintentional reverts touch hot files and would have undone deliberate consolidation work, I extracted just your panadapter work into a fresh PR rather than try to merge-then-fix on top. PR #2786 contains the cherry-pick with you set as commit author so the attribution flows correctly in `git log` and the contributor stats. Setting this PR back to Draft so it stays as a clean reference point — feel free to update it (rebase onto current main) if you'd like a new round, or just leave the draft sitting if you'd rather work from #2786 as the merged result. The actual changes you proposed are all preserved in #2786:
Welcome to the project, and thanks again — really good first contribution. 73, Jeremy KK7GWY & Claude (AI dev partner) |
There was a problem hiding this comment.
Thanks @rfoust — the core panadapter work is solid: the PanadapterLayout/FloatingPanIds persistence rules, the up-to-8-pan default tables, the m_shuttingDown && m_panStack guards on the stream callbacks, the panPixelDimensionsReady/panXpixelsFor/panYpixelsFor helpers, the FFT-pane spectrumPixelHeight() accounting, and the WindowAboutToChangeInternal + prepareForShutdown() lifecycle around the QRhiWidget all look well-considered. A few things to flag:
Scope — please split the unrelated changes into their own PRs
The PR title is "Fix panadapter layout and floating restore," but several substantial changes are unrelated and should be reviewed on their own:
src/gui/RadioSetupDialog.{cpp,h}rewrites the dialog fromPersistentDialogto a hand-rolledQDialog+FramelessWindowTitleBar/FramelessResizer, and theRadio Setup...action inMainWindow.cppdrops theshowOrRaisePersistentsnapshot-with-wasFreshpattern that #2769 specifically introduced. None of this is mentioned in the description, it duplicates whatPersistentDialogis meant to provide centrally, and it risks regressing the audio-compression-snapshot behavior #2769 fixed. Recommend reverting from this PR and proposing it separately.src/core/tnc/AetherAx25LibmodemShim.cppandsrc/gui/Ax25HfPacketDecodeDialog.cppswaplcAx25(defined inLogManager.cppwithQtWarningMsgdefault) for two newQ_LOGGING_CATEGORY(lcAetherAx25Shim, "aether.ax25")/Q_LOGGING_CATEGORY(lcAetherAx25Dialog, "aether.ax25"). Neither carries theQtWarningMsgdefault, soqCDebug(...)calls that were previously suppressed by default will now emit — a behavior change. You also end up with three QLoggingCategory instances all bound to the string"aether.ax25"(the original inLogManager.cppplus the two new ones). TheCMakeLists.txttrimming forax25_libmodem_shim_testfalls out of that. If the goal is to break the shim'sLogManagerlink dependency, please do it in a dedicated PR and either add theQtWarningMsgdefault or move the single category somewhere shared.src/core/PanadapterStream.cpp(thekMinSpectrumDbmclamp,maxDbm >= minDbm+10widening, bin/pixel clamps, andrange <= 0early-out) is a reasonable defensive change but it's behavior shaping unrelated to layout/floating restore — worth landing on its own with its own justification.
Out-of-scope behavior change worth a second look
src/models/RadioModel.cpp:4484 raises the y_pixels acceptance threshold from > 0 to > kMinUsablePanYpixels (> 100), so any status with y_pixels between 1–100 is silently ignored. I assume the intent is to filter the radio's default 20, but the hard cliff at 100 also drops legitimate updates between 21–100 (e.g. a very compact split). Consider rejecting only the known-bogus default(s) or making the gate explicit (e.g. <= 20).
Copilot findings
MainWindow.cpp:2668(re-running layout restore on laterpanadapterAdded): false positive in practice.m_layoutRestoreUntilMsis reset tokPanLayoutRestoreWaitingForFirstPanonly insideonConnectionStateChanged(true), then locked tonowMs + 5000on the first pan and cleared to0on disconnect — pans added after the 5-second window won't re-arm the timer. The new mechanism does gate to the connect/restore phase, just by time-window instead of the oldfiredproperty.PanadapterStack.cpp:61(defaults differ for 2 pans): false positive — bothdefaultDockedLayoutForCount(2)anddefaultPanLayoutForCount(2)return"2v". That said, the two maps are now identical duplicates living in different TUs; a small follow-up to centralize them (e.g. a free function in a shared header, or havePanadapterStackconsumeMainWindow's helper) would prevent them drifting apart later.
Minor
SpectrumWidget::mouseReleaseEvent now calls refreshNoiseFloorTarget(true, /*persist=*/true) on every dBm drag release, which writes DisplayNoiseFloorPosition and AppSettings::save() on each release. Probably fine, but if the user fine-tunes by dragging repeatedly it'll thrash the settings file — consider deferring the save or only persisting on a debounce.
The macOS lifecycle work and the layout/floating persistence are the right shape — once the unrelated pieces are pulled out this should be in good shape to land.
…ust/#2780) (#2786) ## Summary Cherry-pick of @rfoust's panadapter persistence and macOS GPU lifecycle work from PR #2780, applied cleanly onto current main with the stale-snapshot reverts left out. ## Why a cherry-pick rather than merge of #2780 PR #2780 was based on commit \`a0512d99\` (PR #2772 merge), and its single Codex-generated commit included **stale-snapshot content for files outside the intended scope**. Squash-merging #2780 as-is would have silently reverted two recently-landed PRs: - **PR #2770** (\`aether.ax25\` Q_LOGGING_CATEGORY consolidation) — \`AetherAx25LibmodemShim.cpp\`, \`Ax25HfPacketDecodeDialog.cpp\`, plus the test-target linkage in \`CMakeLists.txt\` - **PR #2772** (RadioSetupDialog → PersistentDialog migration) — \`RadioSetupDialog.{cpp,h}\` Verified by three-way merge dry run: post-merge state showed \`class RadioSetupDialog : public QDialog\` (pre-#2772) and 5 occurrences of \`lcAetherAx25Shim\` (pre-#2770). The actual panadapter work in PR #2780 is real and valuable, so this PR extracts it onto a fresh branch from current main and skips the unintentional reverts. ## What's in this PR (= the intended scope of #2780) Cherry-picked verbatim from #2780: | File | Change | |---|---| | \`src/gui/MainWindow.{cpp,h}\` | Persist/restore PanadapterLayout + FloatingPanIds; expand layout count handling from 4 to 8 pans; preserve unseen floating IDs across reconnect | | \`src/gui/PanadapterStack.{cpp,h}\` | Sensible-fallback docked-splitter layouts when some pans are floating or saved layout count mismatches | | \`src/gui/SpectrumWidget.{cpp,h}\` | macOS QRhiWidget lifecycle: \`WindowAboutToChangeInternal\` before reparent; QRhi resource release during shutdown while parent backing stores are valid; guarded late callbacks; auto FFT floor relock after geometry/ypixels changes | | \`src/core/PanadapterStream.cpp\` | Defensive dBm-range clamping (-180 floor, ≥10 dB span); FFT decode clamps pixel input and dbm output to range | | \`src/models/RadioModel.cpp\` | Use actual FFT pane height for radio \`ypixels\` rather than total widget height | Net: **+548 / -215** across 8 files. ## What was NOT cherry-picked (stale-snapshot reverts) | File | Reason | |---|---| | \`CMakeLists.txt\` | -7 lines reverting #2770's test-target linkage I added to fix the link error | | \`src/core/tnc/AetherAx25LibmodemShim.cpp\` | Robbie's content has local \`Q_LOGGING_CATEGORY(lcAetherAx25Shim, ...)\` — pre-#2770. Main has the consolidated \`lcAx25\` from LogManager | | \`src/gui/Ax25HfPacketDecodeDialog.cpp\` | Same pattern — \`lcAetherAx25Dialog\` local, pre-#2770 | | \`src/gui/RadioSetupDialog.{cpp,h}\` | Robbie's diff was **100% pure revert** of #2772 — no new content, no panadapter-related additions. Confirmed by inspecting the full \`git diff a0512d9..e72d7e4 -- RadioSetupDialog.cpp\` output | ## Attribution Commit author is set to **Robbie Foust** so his contribution shows in \`git log\` and the contributor stats. PR #2780 is being reset to **Draft** (not closed) so Robbie has a clean reference point + can update with fresh work if he wants. ## Test plan - [x] Local build clean (402/402 link, only pre-existing warnings) - [x] Three-way merge dry-run against current main: clean - [ ] Manual on Linux: multi-pan layout, detached panadapter, restart, confirm layout + detached state restore - [ ] Manual on macOS GPU build: two detached panadapter windows, Cmd-Q, restart, confirm both restore without the QRhi cleanup callback crash - [ ] Manual: detach/redock panadapter and confirm auto FFT floor relocks ## Cross-references - Replaces #2780 (will be set to Draft after this PR opens) - Same stale-snapshot pattern as aetherclaude #34 (Claude Code), but from OpenAI Codex this time — worth a heads-up to whoever's watching agent-output quality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Robbie Foust <rfoust@duke.edu> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary Fix spectrum display jumps around dBm range changes. - Rebase a few incoming FFT frames after dBm scale release so stale radio-encoded frames do not briefly snap the trace back to the old display location. - Apply the same protection to vertical span / scale adjustments, not just moving the top dBm reference. - Split manual dBm-range echo handling from auto FFT floor echo handling so auto-floor can keep its local smooth animation running while waiting for the radio echo. - Avoid applying an older auto-floor echo if the local animation has already moved past it; reissue the current local range instead. ## Cause The likely regression came from the later dBm stabilization work in PR #2786 / #2780. That path made the stream decoder switch to the requested dBm range immediately while the radio could still send several FFT frames encoded against the old range. It also caused auto-floor movement to pause behind the pending echo guard, which made the smooth motion appear as steps. ## Test Plan - [x] git diff --check - [x] cmake --build build --parallel 8 - [ ] Manual: drag dBm reference line and release; confirm no snap-back - [ ] Manual: Ctrl-drag vertical dBm span/scale and release; confirm no snap-back - [ ] Manual: enable/open panadapter with auto FFT floor adjustment; confirm movement is smooth instead of stepping Build passes. Existing warnings remain unrelated.
… Principle XIII. (#2932) Restructures AetherSDR's contributor-onboarding documentation around the project's actual multi-agent contribution model: at least six distinct AI tools touch the codebase (AetherClaude orchestrator, Claude Code, OpenAI Codex, GPT 5.5 Pro, GitHub Copilot, contributor- side IDE agents). Each tool has its own well-known file at a different path, and the previous topology (CLAUDE.md as the sole canonical) left every other agent reading conventions by accident. Changes: - AGENTS.md (new): canonical project guide. All previous CLAUDE.md content moved here, retitled to be agent-neutral. Single source of truth for project-wide guidance. - CLAUDE.md: slimmed 311 → 68 lines. Now a thin pointer to AGENTS.md plus Claude-Code-specific notes (git ship alias, ~/.claude paths). - GEMINI.md (new): thin pointer for Gemini Code Assist with 6 inline must-knows. - .github/copilot-instructions.md (new): same pattern for GitHub Copilot. - All tool-specific files point directly to AGENTS.md — no pointer-to-pointer chains. Constitution v1.0.1 → 1.1.0 (MINOR amendment): - Adopts 7 new principles from Cisco's Foundry Constitution (https://github.com/CiscoDevNet/foundry-security-spec): VIII Evidence Over Assertion, IX Surface Only What Survives, X Claims Are Atomic And Mortal, XI Fixes Are Demonstrated, XII Sandbox By Infrastructure Not By Prompt, XIII The Operator Outranks Every Agent (promoted from Governance), XIV Persist Atomically. - Total principle count: 7 → 14. First 7 unchanged (AetherSDR- domain conventions); last 7 codify defensive engineering for the multi-agent contribution model. - CONSTITUTION.md (new): byte-identical root mirror of .specify/memory/constitution.md so AI tools reading the repo root find it without spec-kit path knowledge. - Each new principle is grounded in a real AetherSDR incident (PR #2780 stale-snapshot revert → Principle X; PR #2816 alloc- free claim verification → Principle VIII; etc.). New: Issue / PR Claim Protocol (AGENTS.md): - AI agents reviewing/commenting on or merging PRs must assign themselves via gh issue edit / gh pr edit --add-assignee @me. - Carve-out: AetherClaude (@aethersdr-agent) auto-triages every new issue/PR, so adding yourself alongside its assignment is expected — only avoid double-assigning when another non-AetherClaude agent is already engaged. - Operational implementation of Principle X (Claims Are Atomic And Mortal): assignees list is the visible cross-agent claim signal. New: docs/COMMIT-SIGNING.md: - Walks contributors through SSH or GPG signing setup on Windows / macOS / Linux / WSL / Raspberry Pi. - Top of doc has explicit AI Assistant Instructions section so an agent pointed at the doc can walk the contributor through setup in their actual environment without further prompting. - Per-OS copy-paste prompts also included for contributors using AI IDEs (Claude Code, Cursor, Copilot chat, ChatGPT) who want a ready-made invocation. Updated: CONTRIBUTING.md + CLAUDE.md / AGENTS.md pointers: - Constitution reference promoted to step 2 in the Submitting Code checklist. - Commit signing section rewritten to lead with the new doc and AI-assistant invocation, with SSH quick-reference inline. - Principle counts updated everywhere (7 → 14). Principle XIII (Operator Outranks Every Agent) is cited because the entire restructure exists to ensure the operator's authoritative voice reaches every agent reading the project, regardless of which tool the agent runs as. Foundry-spec conformance verified: all 14 structural elements (SYNC IMPACT REPORT, Field table, Purpose, Core Principles, "Why inviolable" rationales, Governance subsections Amendment/Precedence/ Scope/Versioning/Compliance review/Downstream artifacts, MAJOR/MINOR/ PATCH versioning, Foundry reference) present in CONSTITUTION.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Principle XIII. (aethersdr#2932) Restructures AetherSDR's contributor-onboarding documentation around the project's actual multi-agent contribution model: at least six distinct AI tools touch the codebase (AetherClaude orchestrator, Claude Code, OpenAI Codex, GPT 5.5 Pro, GitHub Copilot, contributor- side IDE agents). Each tool has its own well-known file at a different path, and the previous topology (CLAUDE.md as the sole canonical) left every other agent reading conventions by accident. Changes: - AGENTS.md (new): canonical project guide. All previous CLAUDE.md content moved here, retitled to be agent-neutral. Single source of truth for project-wide guidance. - CLAUDE.md: slimmed 311 → 68 lines. Now a thin pointer to AGENTS.md plus Claude-Code-specific notes (git ship alias, ~/.claude paths). - GEMINI.md (new): thin pointer for Gemini Code Assist with 6 inline must-knows. - .github/copilot-instructions.md (new): same pattern for GitHub Copilot. - All tool-specific files point directly to AGENTS.md — no pointer-to-pointer chains. Constitution v1.0.1 → 1.1.0 (MINOR amendment): - Adopts 7 new principles from Cisco's Foundry Constitution (https://github.com/CiscoDevNet/foundry-security-spec): VIII Evidence Over Assertion, IX Surface Only What Survives, X Claims Are Atomic And Mortal, XI Fixes Are Demonstrated, XII Sandbox By Infrastructure Not By Prompt, XIII The Operator Outranks Every Agent (promoted from Governance), XIV Persist Atomically. - Total principle count: 7 → 14. First 7 unchanged (AetherSDR- domain conventions); last 7 codify defensive engineering for the multi-agent contribution model. - CONSTITUTION.md (new): byte-identical root mirror of .specify/memory/constitution.md so AI tools reading the repo root find it without spec-kit path knowledge. - Each new principle is grounded in a real AetherSDR incident (PR aethersdr#2780 stale-snapshot revert → Principle X; PR aethersdr#2816 alloc- free claim verification → Principle VIII; etc.). New: Issue / PR Claim Protocol (AGENTS.md): - AI agents reviewing/commenting on or merging PRs must assign themselves via gh issue edit / gh pr edit --add-assignee @me. - Carve-out: AetherClaude (@aethersdr-agent) auto-triages every new issue/PR, so adding yourself alongside its assignment is expected — only avoid double-assigning when another non-AetherClaude agent is already engaged. - Operational implementation of Principle X (Claims Are Atomic And Mortal): assignees list is the visible cross-agent claim signal. New: docs/COMMIT-SIGNING.md: - Walks contributors through SSH or GPG signing setup on Windows / macOS / Linux / WSL / Raspberry Pi. - Top of doc has explicit AI Assistant Instructions section so an agent pointed at the doc can walk the contributor through setup in their actual environment without further prompting. - Per-OS copy-paste prompts also included for contributors using AI IDEs (Claude Code, Cursor, Copilot chat, ChatGPT) who want a ready-made invocation. Updated: CONTRIBUTING.md + CLAUDE.md / AGENTS.md pointers: - Constitution reference promoted to step 2 in the Submitting Code checklist. - Commit signing section rewritten to lead with the new doc and AI-assistant invocation, with SSH quick-reference inline. - Principle counts updated everywhere (7 → 14). Principle XIII (Operator Outranks Every Agent) is cited because the entire restructure exists to ensure the operator's authoritative voice reaches every agent reading the project, regardless of which tool the agent runs as. Foundry-spec conformance verified: all 14 structural elements (SYNC IMPACT REPORT, Field table, Purpose, Core Principles, "Why inviolable" rationales, Governance subsections Amendment/Precedence/ Scope/Versioning/Compliance review/Downstream artifacts, MAJOR/MINOR/ PATCH versioning, Foundry reference) present in CONSTITUTION.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Fix panadapter layout/floating persistence, FFT sizing after layout changes, and the macOS GPU panadapter reparent/shutdown crash path.
PanadapterLayoutand restore it after restart when the restored pan count matches.PanadapterLayoutwhen the app temporarily falls back after pan removal or radio resource limits.FloatingPanIdsand restore detached panadapters after restart.ypixelsinstead of total widget height.ypixelschanges so detach/redock does not reuse a stale floor baseline.macOS GPU crash fix
Fix the
QRhiWidgetlifecycle crash during panadapter float/dock/rearrange/shutdown on macOS GPU builds:WindowAboutToChangeInternalbefore top-level reparenting so Qt unregisters the widget from the old backing-store QRhi.MainWindowteardown.Testing
git diff --check upstream/main...HEADcmake --build build