Merge nucleic/mellow-opal-heron-qmvv into dev

This commit is contained in:
2026-08-01 19:46:01 -07:00
parent fc8ee2f155
commit 1186f6f336
4 changed files with 283 additions and 31 deletions
+205 -28
View File
@@ -99,6 +99,16 @@ struct IntelligenceSlider: View {
/// Draft text from the mounting composer. Its changes pause the shimmer while the user is
/// typing; an empty draft or three seconds without another edit lets it run continuously.
var composerText: String = ""
/// The route this level currently resolves to ("Sonnet 4.6, reasoning high"), spoken as part
/// of the accessibility value. Sighted users read it in the subtext below the rail; folding it
/// in here means assistive tech doesn't have to go find a separate element to learn what a
/// level will actually run. `nil` while no prompt-specific decision exists yet.
var routeDescription: String? = nil
/// Whether that route is still being classified spoken in place of a stale choice.
var routePending: Bool = false
/// Why the rail can't be used, when `enabled` is false (no connected route, usage limits).
/// Dimming alone conveys this to sighted users; this carries the same fact to everyone else.
var unavailableReason: String? = nil
/// During a drag, reports the detent under the pointer before the binding commits on release.
/// `nil` means the gesture ended and the committed binding is authoritative again.
var onLevelPreviewChanged: ((IntelligenceLevel?) -> Void)? = nil
@@ -108,6 +118,8 @@ struct IntelligenceSlider: View {
@Environment(\.appPalette) private var palette
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
/// Non-nil while the nub is being dragged the displayed position, magnetized toward
/// detents. The binding only commits on release.
@State private var dragFraction: Double?
@@ -130,6 +142,10 @@ struct IntelligenceSlider: View {
/// shimmer's first phase and any level animation can sweep up the control's initial
/// layout and visibly slide it into place on window restore.
@State private var settled = false
/// Set when the user themselves moves the rail, so the route that lands a moment later is
/// announced. Route changes the user didn't cause the classifier re-running as they type
/// must stay silent, or VoiceOver would narrate the subtext throughout composition.
@State private var announcesNextRoute = false
@FocusState private var focused: Bool
private let trackHeight: CGFloat = 8
@@ -142,6 +158,10 @@ struct IntelligenceSlider: View {
/// Orchestra activates only near its dot, protecting Max despite the intentionally compact gap.
private let orchestraActivationProgress = 0.90
private let orchestraRumbleSpacing = 0.16
/// Click radius around the Orchestra node sized to the midpoint of the Max Orchestra gap
/// rather than to the 2pt resting dot, so imprecise pointing still lands it without taking a
/// single pixel that would otherwise have gone to Max.
private let orchestraClickRadius: CGFloat = 12
private let clickSlop: CGFloat = 3
/// The nub center's travel inset from the track ends (half the nub, so it never
/// overhangs the capsule).
@@ -173,44 +193,155 @@ struct IntelligenceSlider: View {
private var isInteractive: Bool { enabled }
private var isBright: Bool { enabled || orchestraTier != nil }
/// At rest the rail's structure track, unreached detents, the dormant constellation is
/// deliberately faint decoration that sits well under the 3:1 non-text contrast floor. Either
/// contrast accommodation raises all of it to a legible weight and pins the nub on screen, so
/// the level stays readable without hunting for the fill's end.
private var highContrast: Bool {
reduceTransparency || colorSchemeContrast == .increased
}
/// Picks between the resting opacity and its high-contrast counterpart.
private func contrasted(_ base: Double, _ raised: Double) -> Double {
highContrast ? raised : base
}
// MARK: - Accessibility
/// Orchestra occupies one stop past Max on the represented slider. It is a distinct backend
/// mode rather than a sixth capability level, but the rail is a single focusable control and
/// dragging is the only other way to reach it so an assistive-tech user has to be able to
/// increment onto it, or the mode is simply unreachable for them.
private var accessibilityStopCount: Int {
IntelligenceLevel.allCases.count + (orchestraAvailable ? 1 : 0)
}
private var orchestraAccessibilityStop: Double {
Double(IntelligenceLevel.allCases.count + 1)
}
private var accessibilitySliderValue: Double {
if orchestraAvailable, orchestraTier != nil { return orchestraAccessibilityStop }
return Double(level.rawValue)
}
private func commitAccessibilityStop(_ raw: Double) {
let stop = Int(raw.rounded())
if orchestraAvailable, stop == IntelligenceLevel.allCases.count + 1 {
setOrchestraTier(.orchestra)
return
}
guard let next = IntelligenceLevel(rawValue: stop) else { return }
setLevel(next)
}
/// Everything the rail's dimming and subtext convey visually, spoken: the stop, whether it
/// carries orchestration, and what it will actually run. Carried on the *value* rather than
/// the hint so it survives the disabled state, where hints commonly go unread.
private var accessibilityValueText: String {
var parts = [displayedName]
if displayedOrchestraTier != nil { parts.append("multi-agent orchestration") }
if !isInteractive {
parts.append(unavailableReason ?? "no route available")
} else if routePending {
parts.append("choosing model and effort")
} else if let routeDescription {
parts.append("routing to \(routeDescription)")
}
return parts.joined(separator: ", ")
}
private var accessibilityHintText: String {
guard isInteractive else {
return unavailableReason ?? "No route is available for this level right now."
}
var hint = "Sets how much model capability this chat gets. "
+ "Higher levels give more thorough results and cost more."
if orchestraAvailable {
hint += " The stop past Max is Orchestra, which fans work out to parallel subagents."
}
return hint
}
/// Phrased for every input method rather than for the pointer the same tooltip reaches
/// keyboard users through the control's accessibility help.
private var helpText: String {
guard isInteractive else {
let reason = unavailableReason ?? "no route is available right now"
return "Intelligence: \(displayedName)\(reason)"
}
var text = "Intelligence: \(displayedName) — higher levels route to more capable models "
+ "and cost more. Use the arrow keys, or drag the rail."
if orchestraAvailable {
text += " The node past Max is Orchestra: parallel subagents."
}
return text
}
var body: some View {
track
.frame(width: Self.preferredWidth, height: controlHeight)
.opacity(isBright ? 1 : 0.5)
.focusable(isInteractive)
// Keep arrow-key adjustment without macOS drawing its persistent blue focus
// rectangle around this custom-shaped control after a pointer click.
// rectangle around this custom-shaped control after a pointer click. The rail
// draws its own ring instead (see `track`) suppressing the system one without
// replacing it would leave keyboard focus with no visible indicator at all.
.focusEffectDisabled()
.focused($focused)
.onMoveCommand { direction in
guard isInteractive else { return }
// Vertical arrows adjust a horizontal slider on macOS too, so honor all four.
switch direction {
case .left: step(-1)
case .right: step(+1)
case .left, .down: step(-1)
case .right, .up: step(+1)
default: break
}
}
.onKeyPress(.home) {
guard isInteractive else { return .ignored }
setLevel(.quick)
return .handled
}
.onKeyPress(.end) {
guard isInteractive else { return .ignored }
if orchestraAvailable {
setOrchestraTier(.orchestra)
} else {
setLevel(.max)
}
return .handled
}
.accessibilityRepresentation {
// The value, hint, and identifier are attached inside the representation so they
// land on the substituted element itself; applied outside they describe a view
// the accessibility tree has already replaced.
Slider(
value: Binding(
get: { Double(level.rawValue) },
set: {
guard let next = IntelligenceLevel(rawValue: Int($0.rounded())) else {
return
}
setLevel(next)
}
get: { accessibilitySliderValue },
set: { commitAccessibilityStop($0) }
),
in: 1...Double(IntelligenceLevel.allCases.count), step: 1
in: 1...Double(accessibilityStopCount), step: 1
) {
Text("Intelligence")
}
.disabled(!isInteractive)
.accessibilityValue(Text(accessibilityValueText))
.accessibilityHint(Text(accessibilityHintText))
.accessibilityIdentifier("intelligence-slider")
}
.help(helpText)
.onChange(of: IntelligenceRouteAnnouncement(
description: routeDescription, pending: routePending)
) { _, route in
// The route resolves a beat after the level does. Announce the one the user just
// caused; consume the flag either way once the classifier settles, so a stale
// request can't attach itself to a later, unrelated route change.
guard announcesNextRoute, !route.pending else { return }
announcesNextRoute = false
guard let description = route.description else { return }
AccessibilityNotification.Announcement("Routing to \(description)").post()
}
.accessibilityValue(Text(displayedName))
.help(
"Intelligence: \(displayedName) — drag the main rail for Quick through Max, "
+ "then pull across the short gap to the Orchestra constellation.")
.onAppear { DispatchQueue.main.async { settled = true } }
.onDisappear {
onLevelPreviewChanged?(nil)
@@ -263,8 +394,12 @@ struct IntelligenceSlider: View {
let deepGlowStrength = deepGlowProgress * (1.25 - 0.25 * deepGlowProgress)
let animationEligible = settled && animationsSettled && isInteractive
let reversingFromOrchestra = dragStartedFromOrchestra && dragLane == .orchestra
let nubVisible = (dragLane != nil || hoveringTrack)
// Keyboard focus has to show the nub: it is this control's only position indicator,
// and the system focus ring is suppressed above. High contrast pins it on for the
// same reason the fill's end is otherwise the only cue to where the level sits.
let nubVisible = (dragLane != nil || hoveringTrack || focused || highContrast)
&& (activeTier == nil || reversingFromOrchestra)
let ringWidth = orchestraAvailable ? orchestraGuideWidth : mainRailWidth
ZStack(alignment: .topLeading) {
// A faint capsule underneath hints that the control continues past Max.
@@ -272,7 +407,7 @@ struct IntelligenceSlider: View {
// so Orchestra reads as an extension rather than a sixth point on the rail.
if orchestraAvailable {
Capsule()
.fill(Color.secondary.opacity(0.15))
.fill(Color.secondary.opacity(contrasted(0.15, 0.34)))
.frame(width: orchestraGuideWidth, height: trackHeight)
.position(
x: railLeading + orchestraGuideWidth / 2,
@@ -280,15 +415,25 @@ struct IntelligenceSlider: View {
}
Capsule()
.fill(Color.secondary.opacity(0.18))
.fill(Color.secondary.opacity(contrasted(0.18, 0.42)))
.frame(width: mainRailWidth, height: trackHeight)
.position(x: railLeading + mainRailWidth / 2, y: trackY)
// The keyboard focus indicator. Traces the rail's own capsule geometry rather
// than boxing the control, so focus reads as part of the rail's language.
if focused && isInteractive {
Capsule()
.strokeBorder(palette.accent.opacity(0.9), lineWidth: 2)
.frame(width: ringWidth + 6, height: nubSize + 6)
.position(x: railLeading + ringWidth / 2, y: trackY)
.allowsHitTesting(false)
}
ForEach(IntelligenceLevel.allCases) { stop in
let dotSize: CGFloat = stop == .max && orchestraAvailable ? 4 : 2
Circle()
.fill(Color.secondary.opacity(
stop.fraction <= standardFraction ? 0 : 0.4))
stop.fraction <= standardFraction ? 0 : contrasted(0.4, 0.85)))
.frame(width: dotSize, height: dotSize)
.position(
x: inset + (maxX - inset) * stop.fraction,
@@ -340,6 +485,7 @@ struct IntelligenceSlider: View {
value: nubStretch)
.animation(PanelMotion.hover(reduceMotion), value: nubVisible)
.animation(PanelMotion.hover(reduceMotion), value: hoveringTrack)
.animation(PanelMotion.hover(reduceMotion), value: focused)
.animation(PanelMotion.hover(reduceMotion), value: orchestraTier)
.animation(
dragLane == nil && settled
@@ -361,7 +507,7 @@ struct IntelligenceSlider: View {
.stroke(
selected
? AppTheme.orchestra.opacity(0.34)
: Color.secondary.opacity(0.10),
: Color.secondary.opacity(contrasted(0.10, 0.45)),
style: StrokeStyle(
lineWidth: selected ? 1 : 0.75,
lineCap: .round))
@@ -376,7 +522,8 @@ struct IntelligenceSlider: View {
Circle()
.fill(selected
? AnyShapeStyle(AppTheme.orchestra)
: AnyShapeStyle(Color.secondary.opacity(0.30)))
: AnyShapeStyle(Color.secondary
.opacity(contrasted(0.30, 0.70))))
.frame(
width: satelliteSize,
height: satelliteSize)
@@ -390,9 +537,10 @@ struct IntelligenceSlider: View {
Circle()
.fill(selected
? AnyShapeStyle(AppTheme.orchestra)
: AnyShapeStyle(Color.secondary.opacity(0.52)))
: AnyShapeStyle(Color.secondary
.opacity(contrasted(0.52, 0.90))))
.overlay(Circle().strokeBorder(
.white.opacity(selected ? 0.75 : 0.18),
.white.opacity(selected ? 0.75 : contrasted(0.18, 0.45)),
lineWidth: 0.5))
.frame(
width: selected ? 5.5 : 2,
@@ -510,11 +658,13 @@ struct IntelligenceSlider: View {
selection.fraction >= orchestraActivationProgress
{
let tier = IntelligenceOrchestraTier.orchestra
if orchestraTier != tier { announcesNextRoute = true }
orchestraTier = tier
} else {
let landed = selection.lane == .orchestra
? IntelligenceLevel.max
: IntelligenceSliderMath.nearestLevel(fraction: selection.fraction)
if orchestraTier != nil || level != landed { announcesNextRoute = true }
orchestraTier = nil
level = landed
}
@@ -570,7 +720,7 @@ struct IntelligenceSlider: View {
let dy = point.y - trackY
let gap = orchestraX - maxX
let dotDX = point.x - orchestraX
if isClick, dotDX * dotDX + dy * dy <= 64 {
if isClick, dotDX * dotDX + dy * dy <= orchestraClickRadius * orchestraClickRadius {
return 1
}
guard gap > 0,
@@ -607,12 +757,14 @@ struct IntelligenceSlider: View {
guard next != level || orchestraTier != nil else { return }
orchestraTier = nil
level = next
announcesNextRoute = true
performLevelHaptic()
}
private func setOrchestraTier(_ next: IntelligenceOrchestraTier) {
guard orchestraTier != next else { return }
orchestraTier = next
announcesNextRoute = true
performLevelHaptic()
}
@@ -660,6 +812,13 @@ struct IntelligenceSlider: View {
}
}
/// The two facts the rail's route announcement depends on, folded into one `onChange` identity
/// so a resolution and the pending flag that precedes it can't race as separate observations.
private struct IntelligenceRouteAnnouncement: Equatable {
var description: String?
var pending: Bool
}
/// The route chosen for the current prompt, tucked directly below the Intelligence rail. The
/// fixed footprint keeps model-name changes from shifting the surrounding composer controls.
/// While the classifier is waiting/running, a quiet horizontal sweep replaces the stale choice.
@@ -668,12 +827,20 @@ struct IntelligenceRoutePreview: View {
var effort: String?
var isPending: Bool
/// The row's height tracks its own text style: at accessibility text sizes a fixed 12pt
/// frame clips the model name it exists to show.
@ScaledMetric(relativeTo: .caption2) private var rowHeight: CGFloat = 12
var body: some View {
ZStack(alignment: .trailing) {
if isPending {
IntelligenceRoutePlaceholder()
.transition(.opacity)
// The stripes are pure skeleton collapse them into one element that says
// what they stand for, rather than leaving bare shapes in the tree.
.accessibilityElement(children: .ignore)
.accessibilityLabel("Choosing model and effort")
.accessibilityAddTraits(.updatesFrequently)
} else if let model, let effort {
Text(
"\(ModelCatalog.displayName(model)) · "
@@ -690,7 +857,7 @@ struct IntelligenceRoutePreview: View {
}
.font(.caption2.weight(.medium))
.padding(.trailing, 16)
.frame(width: IntelligenceSlider.preferredWidth, height: 12, alignment: .trailing)
.frame(width: IntelligenceSlider.preferredWidth, height: rowHeight, alignment: .trailing)
.animation(.easeInOut(duration: 0.18), value: isPending)
}
}
@@ -708,16 +875,26 @@ private struct IntelligenceRoutePlaceholder: View {
/// text arrives. Each stripe owns its own clipped sweep so light never bridges the gap.
private struct IntelligenceRoutePlaceholderStripe: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
@State private var sweep = false
var width: CGFloat
/// Tracks the row it stands in for, so the skeleton keeps the text's proportions at
/// accessibility text sizes instead of shrinking away from it.
@ScaledMetric(relativeTo: .caption2) private var stripeHeight: CGFloat = 7
private var highlightWidth: CGFloat { min(42, width * 0.44) }
private var fillOpacity: Double {
reduceTransparency || colorSchemeContrast == .increased ? 0.30 : 0.12
}
var body: some View {
Capsule()
.fill(Color.secondary.opacity(0.12))
.frame(width: width, height: 7)
.fill(Color.secondary.opacity(fillOpacity))
.frame(width: width, height: stripeHeight)
.overlay(alignment: .leading) {
if !reduceMotion {
Capsule()
@@ -729,7 +906,7 @@ private struct IntelligenceRoutePlaceholderStripe: View {
],
startPoint: .leading,
endPoint: .trailing))
.frame(width: highlightWidth, height: 7)
.frame(width: highlightWidth, height: stripeHeight)
.offset(x: sweep ? width : -highlightWidth)
}
}
+14
View File
@@ -379,6 +379,10 @@ struct NewChatComposer: View {
orchestraAvailable: isControlProject,
animationsSettled: layoutAnimationsEnabled,
composerText: draft,
routeDescription: routedAccessibilityDescription,
routePending: routedPreviewPending,
unavailableReason: routedRouteAvailable
? nil : routedResolution.reason,
onLevelPreviewChanged: { preview in
if intelligenceLevelPreview != preview {
intelligenceLevelPreview = preview
@@ -742,6 +746,16 @@ struct NewChatComposer: View {
return routedPreviewResolution.effort
}
/// The resolved route spoken as part of the rail's own accessibility value. The subtext below
/// the rail already states this for sighted users; carrying it on the control means assistive
/// tech learns what a level will run without having to navigate to a separate element.
private var routedAccessibilityDescription: String? {
guard let model = routedPreviewModel, let effort = routedPreviewEffort else { return nil }
return "\(ModelCatalog.displayName(model)), "
+ "\(ModelCatalog.effortNoun(for: model)) "
+ ModelCatalog.effortDisplayName(effort)
}
/// Preserve the routed subtext's exact rendering and place a transparent menu hit target
/// over it. The resolved words themselves become the control without acquiring button chrome,
/// an indicator, or different spacing.
@@ -891,6 +891,14 @@ struct SessionDetailView: View {
return effectiveEffort
}
/// The resolved route spoken as part of the rail's accessibility value the same fact the
/// subtext below it renders, carried on the control so assistive tech needn't go find it.
private var sessionRouteAccessibilityDescription: String {
"\(ModelCatalog.displayName(sessionPreviewModel)), "
+ "\(ModelCatalog.effortNoun(for: sessionPreviewModel)) "
+ ModelCatalog.effortDisplayName(sessionPreviewEffort)
}
/// The same fixed-footprint subtext as the new-chat composer, with a transparent Menu over
/// the resolved words so becoming interactive adds no chrome, indicator, or spacing change.
private var sessionRouteSelectionPreview: some View {
@@ -2771,6 +2779,10 @@ struct SessionDetailView: View {
orchestraAvailable: isControlProject,
animationsSettled: !transcriptSettling,
composerText: draft,
routeDescription: sessionRouteAccessibilityDescription,
unavailableReason: isArchived
? "This chat is archived — its Intelligence level can't be changed."
: nil,
onLevelPreviewChanged: { preview in
if sliderIntelligenceLevelPreview != preview {
sliderIntelligenceLevelPreview = preview
+52 -3
View File
@@ -1097,14 +1097,59 @@ Screens (parity targets from `Sources/NucleicApp` and `ios/.../Views`):
| Home | `HomeView.swift`, activity grid, streaks, idea inbox (`TodoView.swift`) | Pure projections — data comes from host. |
| Project view / Control panel | `ProjectView.swift`, `ControlPanelView.swift` | Container status/usage cards use `controlContainerStatus/Usage` wire data. |
| Session detail + transcript | `SessionDetailView.swift` (2,407), `TranscriptRow.swift` (2,343), `HostExecCard.swift` | `ItemsRepeater` with incremental virtualization driven by `np_projection_apply` diffs; tool rows color-coded by risk; collapsible thinking. |
| Composer | `NewChatComposer.swift`, `ChatInputField.swift`, `ComposerAttachments.swift` | Model/effort/Orchestra picker (SKU-routed backends), attachments. |
| Composer | `NewChatComposer.swift`, `ChatInputField.swift`, `ComposerAttachments.swift`, `IntelligenceSlider.swift` | Model/effort/Orchestra picker (SKU-routed backends), attachments. The Intelligence rail is custom-drawn — its accessibility contract must be ported explicitly (§7.1). |
| Approvals | approval cards in session view; `ApprovalCardView` (iOS) | Allow / deny / modify-and-allow / always-allow-in-session; first-responder-wins collapse on `approvalResolved`. |
| Terminal panel | `Panels/TerminalPanel.swift` (SwiftTerm) | **In-container shell** (D2): hostd intent → broker `proc.exec(tty:true)` of `agentShellArgv` (nash) in the session container; bytes ride a per-terminal loopback side-channel vended by hostd; rendered in the Windows Terminal control; `proc.resize` on layout. No host ConPTY needed. |
| Editor / diff | `Panels/EditorPanel.swift`, `SyntaxHighlighting.swift`, diff tab | Monaco in WebView2 (read-only diff-first); native later if desired. |
| Build & Run | `BuildRunPanel.swift` | Host process spawn via hostd intent (ChildProcess + Job Objects). |
| Settings | `SettingsView.swift` (2,995), `RemoteAccessView.swift`, `ModelCatalog.swift`, `QuotaIndicator.swift` | Sections that apply: agents/accounts (OAuth via loopback listener), sandbox (CPU/memory, restart-shared), remote access (QR pairing display, transports), updates (channel display; App Installer drives updates). |
| Attention | `DockBounce.swift`, sound hooks (`AppStore.swift:668-681`) | `AppNotificationManager` toasts + `FlashWindowEx` + optional sound, wired to the same AppStore hook events over the wire. |
| Accessibility/theming | 4 color-vision palettes + 5 text sizes (app-wide) | Resource dictionaries keyed off host-synced settings (`SyncedSettings.swift`); honor Windows high-contrast. |
| Accessibility/theming | 4 color-vision palettes + 5 text sizes (app-wide) | Resource dictionaries keyed off host-synced settings (`SyncedSettings.swift`); honor Windows high-contrast. **Custom-drawn controls need more than this — see §7.1.** |
---
### 7.1 Custom-drawn controls need an explicit UI Automation contract
Most rows in the table above are ordinary controls, and WinUI hands those a UI Automation
peer for free. The composer's **Intelligence rail**
(`Sources/NucleicApp/IntelligenceSlider.swift`) is not one of those: it is a hand-drawn
capsule, detent dots, and Orchestra constellation driven by a `DragGesture`. On macOS it
therefore carries an *explicit* accessibility contract, and a naive port to a
`Canvas`/`Path` composition would silently drop every part of it — the control would reach
UIA as an unnamed image with no value, no keyboard path, and no way to select Orchestra.
Port the contract, not just the pixels.
| Guarantee | macOS | WinUI 3 |
|---|---|---|
| Reads as a slider, not a drawing | `accessibilityRepresentation { Slider(…) }` | A `FrameworkElementAutomationPeer` subclass exposing `IRangeValueProvider`, with `GetAutomationControlTypeCore``Slider` |
| **Orchestra is reachable without a pointer** | represented range is `1...6` when available; stop 6 is Orchestra | Same — `Maximum` is 6, not 5 (see below) |
| Spoken value is the level name + resolved route, never a bare number | `.accessibilityValue("Balanced, routing to Sonnet 4.6, reasoning high")` | `IRangeValueProvider.Value` for the position **plus** the peer's `GetNameCore` for the words |
| Purpose and cost are explained | `.accessibilityHint` | `AutomationProperties.HelpText` |
| The disabled *reason* is spoken, not just dimmed | reason folded into the **value** — hints commonly go unread on disabled controls | Same: put it in the name/value, not only `HelpText`. `IsEnabled = false` on its own says nothing about why |
| Async route resolution is announced | `AccessibilityNotification.Announcement`, gated to user-caused changes only | `AutomationPeer.RaiseNotificationEvent(…)`, same user-caused-only gate — the classifier re-runs as the user types, and narrating that would be intolerable |
| Addressable by tests and inspectors | `.accessibilityIdentifier("intelligence-slider")` | `AutomationProperties.AutomationId` |
| ←/→/↑/↓ step, Home/End jump to the ends | `onMoveCommand` + `onKeyPress(.home/.end)` | `OnKeyDown` — a custom peer's range provider does not supply key handling |
| Keyboard focus is **visible** | custom ring, because the system ring is suppressed on this shape | Do not clear `UseSystemFocusVisuals` without drawing a replacement |
| Reduce Motion stops the shimmer | `accessibilityReduceMotion` | `UISettings.AnimationsEnabled` |
| High contrast raises the faint rail furniture | `accessibilityReduceTransparency` / `colorSchemeContrast` | `AccessibilitySettings.HighContrast` plus a `HighContrast` theme dictionary; `UISettings.AdvancedEffectsEnabled` for the transparency leg |
| The route subtext scales with text size | `@ScaledMetric(relativeTo: .caption2)` | `UISettings.TextScaleFactor` — do not hard-code the row height |
Two of these are load-bearing enough to state plainly:
- **Orchestra must sit on the slider's automation range.** It is a distinct backend mode
rather than a sixth capability level, and it is deliberately drawn *off* the rail across an
elastic gap — but the rail is a single focusable control, and dragging that gap is the only
other way to select it. An automation range that stops at Max makes Orchestra unreachable
for every screen-reader and keyboard-only user. That is a functional gap, not a cosmetic one.
- **The level is never conveyed by hue alone.** Fill length, detent dots, and the spoken
value each carry it independently, because the app ships 4 color-vision palettes — the gold
crest at Max is decoration, never the signal. Keep all three channels in the port.
Apply the same review to every other hand-drawn surface the renderer inherits —
`QuotaIndicator.swift`, `StreakBadge`/the activity grid (`HomeView.swift`,
`ActivityFeedView.swift`), and `OrchestraStyle.swift`'s glow. If a surface is drawn rather
than composed from controls, it needs a peer, a name, a keyboard path, and a high-contrast
story before it ships.
---
@@ -1294,7 +1339,11 @@ and safe to start immediately on macOS hardware.
Trusted Signing.
15. **naros multi-arch** — extend `os/` + `.github/workflows/naros.yml` to push an
amd64+arm64 manifest list to GHCR (D10).
16. **Phi Silica provider** (§9.2), accessibility/theming parity, polish.
16. **Phi Silica provider** (§9.2), accessibility/theming parity, polish. Accessibility
parity is not only the palettes and text sizes: every custom-drawn control needs its own
UI Automation peer, keyboard path, focus visual, and high-contrast treatment (§7.1).
Screens (item 12) should land these as they go rather than deferring them here — the
Intelligence rail in the Composer is the worked example.
Packaging details (item 14): MSIX Identities `BLAKESLEE.Nucleic[.Dev/.Canary/.Beta/.RC]`
mirroring the bundle-ID scheme (`Package.swift:27-35`); `runFullTrust`; payload = three