76 lines
3.8 KiB
Swift
76 lines
3.8 KiB
Swift
import SwiftUI
|
|
import NucleicProtocol
|
|
|
|
/// The line beneath the Intelligence rail: the stop's name and what it resolves to.
|
|
///
|
|
/// **A readout, not a control.** The Mac lays a transparent menu over the same subtext so the
|
|
/// resolved words double as a model/effort picker. That doesn't earn its place here: with the rail
|
|
/// on, the rail *is* the control, and a second way to set the same thing — one that silently
|
|
/// suspends routing when used — is a contradiction on a surface this small. So this is plain text.
|
|
/// Manual model and effort selection remains available exactly where it always was, in the chat's
|
|
/// ⋯ menu, and only on a host that isn't routing.
|
|
///
|
|
/// It still carries three facts the Mac spreads across hover and tooltips, because a phone has
|
|
/// neither: the stop's **name** (the rail alone never has to be read for it), the concrete
|
|
/// **model · effort** it lands on, and — when quota has closed a lane — the fact that this stop
|
|
/// currently can't run.
|
|
///
|
|
/// The footprint is fixed so a model-name change can't shift the composer's rows under a thumb
|
|
/// that's mid-reach for the send button.
|
|
struct IntelligenceRouteLine: View {
|
|
/// The stop being described — the one under the finger during a drag, else the committed one.
|
|
var level: Int
|
|
var orchestra: Bool
|
|
var catalog: WireIntelligenceCatalog
|
|
/// The pair this stop lands on. For an open chat at rest that's the session's *actual* model
|
|
/// and effort (ground truth from the host); mid-drag, and in the new-chat composer, it's the
|
|
/// route the host would resolve. `nil` shows the stop alone — an empty draft has nothing to
|
|
/// route yet, and saying nothing beats promising a model.
|
|
var model: String?
|
|
var effort: String?
|
|
/// The router's own sentence, shown in place of the pair when this stop can't currently run.
|
|
var unavailableReason: String?
|
|
var modelCatalog: WireModelCatalog = .empty
|
|
|
|
/// Tracks its own text style, so at accessibility sizes the row grows with the model name it
|
|
/// exists to show rather than clipping it.
|
|
@ScaledMetric(relativeTo: .caption) private var rowHeight: CGFloat = 16
|
|
|
|
private var stopName: String {
|
|
if orchestra { return catalog.orchestraDisplayName }
|
|
let projected = catalog.displayName(forLevel: level)
|
|
return projected.isEmpty
|
|
? (IntelligenceLevel(rawValue: level)?.displayName ?? "") : projected
|
|
}
|
|
|
|
/// "Balanced · Sonnet 5 · High", or just the stop before anything resolves. Orchestra states
|
|
/// its mode rather than a pair — the supervisor model isn't the interesting fact there.
|
|
private var summary: String {
|
|
if orchestra { return "\(stopName) · parallel subagents" }
|
|
if let unavailableReason { return "\(stopName) · \(unavailableReason)" }
|
|
guard let model, let effort else { return stopName }
|
|
return "\(stopName) · \(modelCatalog.displayName(model)) · "
|
|
+ modelCatalog.effortDisplayName(effort).localizedCapitalized
|
|
}
|
|
|
|
var body: some View {
|
|
Text(summary)
|
|
.font(.caption.weight(.medium))
|
|
.foregroundStyle(lineStyle)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.frame(height: rowHeight)
|
|
// One element with the rail, not a second stop in the VoiceOver order: this restates
|
|
// what the rail's own value already says, and a swipe onto a duplicate readout is pure
|
|
// friction. The rail is the thing to focus.
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private var lineStyle: AnyShapeStyle {
|
|
if unavailableReason != nil { return AnyShapeStyle(.orange) }
|
|
if orchestra { return AnyShapeStyle(Palette.orchestra) }
|
|
return AnyShapeStyle(.secondary)
|
|
}
|
|
}
|