Repository navigation
[RFC] Radio tab: AT chip and quality-coloured status dot while adaptive frame-rate throttle is on #6790
Description
Status: RFC, so the colour changes in #6787 can be ruled on before merge, as GOVERNANCE.md asks for visual-design changes. #6787 is already open and implements this; it waits on this ruling.
Problem
Since the unified title bar (#6198), you can barely tell from the title bar whether adaptive frame-rate throttle is on or capping:
- Before feat(gui): unified 52 px title bar on Qt 6.12 window chrome — native captions on macOS/Windows, frameless on Linux (RFC #4764) #6198: the standalone heartbeat lamp flashed green. While a cap was in force it flashed in the cap's colour instead (
setThrottleFlashColor(fpsCapColor(fpsCap))): red at 4 fps, amber at 8 fps, blue above that. Those are the same tiers the status bar uses for network quality. - After feat(gui): unified 52 px title bar on Qt 6.12 window chrome — native captions on macOS/Windows, frameless on Linux (RFC #4764) #6198: the lamp became the active radio tab's status dot, and the throttle survives only as a faint tint on the dot's heartbeat glow. Nothing at all shows that the throttle is armed but not capping.
Proposal
On the connected radio's tab:
- "AT" chip after the radio's name while "Enable adaptive frame-rate throttle" is ticked. Outlined while armed; filled in the cap's colour while a cap is in force.
- Quality-coloured status dot. While, and only while, the chip shows, the dot takes the network-quality colour (the status bar's palette), so the operator can see the conditions the throttle is reacting to. With the throttle off, the dot stays the plain green "connected" colour. A recoloured dot always comes with the chip that explains it.
- Words for the same state in the tab's tooltip and accessible description (WCAG 1.4.1).
Items 1 and 3 restore what #6198 lost, and could stand on the "bug fix with a clear root cause" exemption. Item 2 is new: the old lamp showed the cap's tier, not the live grade. Three palette decisions need a ruling:
- (a) the dot taking the quality palette while the chip shows;
- (b) the chip's fill in the cap's colour;
- (c) the dot's heartbeat glow following the dot's colour. This also means the amber discovery pulse now glows amber.
Contrast (measured by the PR review, dot against the active tab's effective background):
| Grade | light #ddebf2 |
dark #0c2636 |
|---|---|---|
Excellent/Very Good #00cc66 |
1.75:1 | 7.30:1 |
Good #00b4d8 |
2.02:1 | 6.33:1 |
Fair #cc9900 |
2.12:1 | 6.03:1 |
Poor #cc3333 |
4.22:1 | 3.04:1 |
replaced: color.titlebar.status.connected |
4.10:1 (#1a8040) |
8.48:1 (#4dd87a) |
Dark passes everywhere. In the light theme three of four grades are below the 3:1 non-text minimum (docs/a11y.md, WCAG 1.4.11), because networkQualityColor() returns fixed hex values rather than theme tokens. The tooltip's spelled-out grade covers 1.4.1, but not 1.4.11.
If (a) is approved, the proposal is to merge with the hex values (docs/a11y.md allows that as an intermediate state) and file a follow-up to tokenise the quality palette for both the status bar and the tab dot, with light-theme values that pass 3:1.
Cross-platform impact
Everything is in shared Qt widget code (RadioTabBar, TitleBar, MainWindow), painted the same way on Linux, macOS and Windows. The only platform branches in RadioTabBar.cpp (macOS focus and drag handling) are untouched. There is no native chrome integration, no new dependency, and no new settings key; the chip reads the existing AdaptiveThrottleEnabled setting. Tested live on Windows only.
Alternatives considered
- Chip only, dot stays green (items 1 and 3, no 2). No palette ruling needed, and could merge on the regression exemption. But the operator then can't see the link quality the throttle is reacting to without opening the status-bar tooltip.
- Dot shows the cap's tier, as the old lamp did. A closer restoration, but it says nothing while the throttle is armed and not capping. The chip already carries the cap's colour.
- Quality colour on the dot always, AT or not. Rejected by the operator: a colour change with no visible reason. Recolouring only while the chip shows keeps the cause next to the effect.
- Tokenise the quality palette in this change. Fixes the light-theme contrast, but drags the status bar's colours into this change; better as the follow-up above.
Environment
AetherSDR 26.10.3 (Windows), FLEX-6600 4.2.20. Implemented in #6787.
Activity
aethersdr-agent commentedon Oct 26, 2026
Triage:
maintainer-review— the report is accurate, and the visual surface needs a ruling before #6787 landsRoot cause, verified in tree
The throttle indicator still exists; it just has nowhere visible to land.
MainWindow.cpp:2162-2166maps the cap to a colour (fpsCapColor:#cc3333≤ 4 fps,#cc9900≤ 8, else#00b4d8) and pushes it viaTitleBar::setThrottleFlashColor()(TitleBar.cpp:1687). That colour reaches the tab through exactly one path —currentBeatColor()(TitleBar.cpp:1666) →RadioTabBar::pulseLink()→RadioTab::m_beatColor— andm_beatColoris consumed at exactly one place:RadioTabBar.cpp:373, asglowBasefor the heartbeat glow. So post-#6198 the cap's tier is a tint on a glow that swells and decays in ~850 ms per discovery packet. "Barely tell" is a fair description.The "armed but not capping" gap is also real, and it is not a #6198 regression.
setThrottleFlashColoris only ever called from theadaptiveThrottleChanged(active, fpsCap)handler, andRadioModel::applyAdaptiveFrameRate()(RadioModel.cpp:8989) only emits that signal on a cap tier transition. Merely ticking the box has never produced any indication, before or after #6198 — so item 1's outlined state is new surface, not a restoration. Worth stating plainly so the ruling's scope is accurate: item 1 is "restore the cap colour somewhere legible" plus "add an armed state that never existed".What #6787 (open, not merged) does
RadioTab::setThrottleChip()paints an "AT" chip after the name (kChipSizePx8.5,kChipPadX4,kChipGap6,kChipRadius3): outlined when the tint is invalid (armed), filled at 22 % alpha in the cap colour otherwise. The name gives up width to the chip rather than the chip eliding.RadioTab::dotColor()gains a quality branch below the existing override branch, so discovery amber and link-lost red still speak first.RadioTabBar::applyLinkVisuals()gates it onours && m_throttleEnabled, so a recoloured dot always arrives with the chip.descriptionLine()appendsNetwork <grade>and the chip text, which feeds both the tooltip and the accessible name.fpsCapColormoves toMainWindowHelpers::adaptiveFpsCapColor()and routes through the existingnetworkQualityColor(), so the throttle tiers and the status bar can't drift apart.currentBeatColor()now returns an invalid colour when unthrottled, so the glow follows the dot instead of forcing green over a red dot.radios.linkQuality/linkQualityColor/adaptiveThrottle/adaptiveFpsCap/tabs[].throttleChip, plus 81 lines intests/unified_title_bar_test.cpp.The PR's claim that unticking the box does not lift a cap already in force is correct:
applyAdaptiveFrameRate()returns at its first line whenAdaptiveThrottleEnabled != "True", so noadaptiveThrottleChanged(false, 0)is ever emitted after an untick. Keeping the chip up onenabled || m_adaptiveFpsCap > 0is the right call — hiding it would hide a live cap.One defect to fix before merge
refreshAdaptiveThrottleChip()is called from three places: construction, theadaptiveThrottleChangedhandler, and theConnectionPanel::adaptiveThrottleEnabledChangedsignal. It is not called from the disconnect handler, which zeroes the state atMainWindow.cpp:7156-7157:m_adaptiveFpsCap = 0, but nothing pushes that toRadioTabBar, which still holdsm_throttleEnabled = true,m_throttleFpsCap = 8. The chip is hidden only becauseapplyLinkVisuals()'ssessiongate is false whilem_activeIdis empty.sessiongoes true and the stale chip paints filled, tooltip "Adaptive throttle: 8 fps cap", with the box unticked and no cap in force. Nothing clears it for the rest of the session:adaptiveThrottleChangedcan't fire (the setting gate), andnetworkQualityChangeddoesn't call the refresh.Fix is one line —
refreshAdaptiveThrottleChip();afterMainWindow.cpp:7157— plus a test that reconnect after untick-with-cap leavesradios.adaptiveFpsCapat 0 andtabs[].throttleChipfalse.Why this is on you and not on me
GOVERNANCE.md:97 reserves "any change to visual design — colors, fonts, spacing, theme, icons" to the maintainer, and :221 says the agent may not autonomously change visual design. The reporter flags item 2, and that is the sharpest question — the old lamp showed the cap's tier, the proposal shows the live grade, so a Fair link with no cap engaged would turn the dot amber where today it stays green. But the ruling needs to cover the whole visual surface, not just item 2: the AT chip is new title-bar chrome at a new type size, and item 2 is a new use of the quality palette on a status dot.
Items 1 and 3 are the parts with a clear regression story behind them (minus the armed state, which is new). Item 2 stands or falls on your call.
If the design is approved, the implementation in #6787 is sound modulo the stale-chip defect above, and
aetherclaude-eligiblehere would let me carry that fix in.🤖 Generated with Claude Code
🤖 aethersdr-agent · cost: $2.7619 · model: claude-opus-5
📍 Citation health check (Foundry Constitution Principle I): 1 resolved, 7 unresolved against
/Users/aetherclaude/workspace/AetherSDRat origin/main.Unresolved citations (7)
MainWindow.cpp:2162-2166MainWindow.cpp:7156-7157MainWindow.cpp:7157RadioModel.cpp:8989RadioTabBar.cpp:373TitleBar.cpp:1666TitleBar.cpp:1687[-]Radio tab: show when adaptive frame-rate throttle is armed or capping (lost with the unified title bar)[/-][+][RFC] Radio tab: AT chip and quality-coloured status dot while adaptive frame-rate throttle is on[/+]nigelfenton commentedon Oct 31, 2026
Some numbers for the ruling. I computed them from the PR's colour values and the two bundled themes at #6787's head (
11c57b80): the active-tab background composited over the title bar, and WCAG 2.1 relative luminance.The dot (non-text, needs 3:1). Your light-theme figures reproduce exactly: Excellent/Very Good 1.75, Good 2.02, Fair 2.12, Poor 4.22. In dark I get the same pass/fail (7.47 / 6.47 / 6.17 / 3.11), with small differences from a slightly different composite background.
The AT chip's text has its own problem, which the table doesn't cover. A capped chip draws the letters "AT" in the cap's colour, at 8.5 px bold, over a 22 % fill of that same colour. That's text, so
docs/a11y.mdasks for 4.5:1.adaptiveFpsCapColor()only ever yields Good, Fair or Poor, so these are the only cases that occur:#00b4d8(15 fps)#cc9900(8 fps)#cc3333(4 fps)None of the six passes, in either theme. The armed chip is fine: its outline and text use
color.text.secondary(5.31:1 light, 6.47:1 dark).A fix that keeps (b): keep the cap colour for the chip's outline and fill, but draw the letters in
color.text.primary. That gives 8.8–10.2:1 in light and 7.3–9.7:1 in dark for all three caps. The colour still says which tier, and the letters stay readable. The outline is then a non-text part (3:1), with the same light-theme shortfall as the dot.On merging with the hex values.
docs/a11y.md's allowance is for "concrete hex that meets the ratio", with a follow-up to tokenise it. That's the #3441 precedent, which landed hex that already passed. Three of the four light-theme grades don't meet it, so the doc's allowance as written doesn't cover merging them. That's a call for the ruling, not for me, but it seemed worth saying plainly.(c), the glow. With AT off, the glow now takes
color.titlebar.status.connectedrather thancolor.accent.success. Both bundled themes give those the same value (#1a8040/#4dd87a), so nothing visibly changes there. Only a user theme that sets them differently would notice. The visible part of (c) is the amber discovery pulse, as the RFC says.73, Nigel G0JKN/W3
fix(gui): AT chip letters in the primary text colour when capped, and…