Freeze a shared design system (DesignSystem/{Tokens,Typography,StatusStyle,
Primitives}.swift): indigo #7C8CFF accent (root .tint), semantic status colors,
2-4-8-12-16-20-24 spacing + sm8/md12/lg16 radii scale, SF Mono tabular numbers,
reduce-motion-gated animations, haptics, reusable StatusBadge/TelemetryChip/Card/
SectionHeader/DSButtonStyle/ContinueLastBanner. Every changed view consumes tokens
— no hardcoded colors/spacing.
Applied across all surfaces (visual only — zero behavior/logic change, all suites
green): session list rows + status system (shape+color, not color-alone) +
telemetry chips + thumbnail placeholders; terminal gate card (≥44pt approve/reject)
+ keybar + reconnect + quick-reply + digest + SwiftTerm accent theme; pairing hero
+ warning tiers + Projects cards + Timeline/Diff; nav chrome + iPad split placeholder
+ privacy shade + motion. Chinese gate copy. UX finding fixed: timeline class colors
now via DS.Palette (+timelineTool/timelineUser tokens).
Design review 8.5/10. Verified: iPhone 16 290 + iPad Pro 11 290 tests green;
packages + integration green; consistency audit ~clean; zero changes under
ios/Packages, src/, public/.
130 lines
4.9 KiB
Swift
130 lines
4.9 KiB
Swift
import SwiftUI
|
|
import WireProtocol
|
|
|
|
/// T-iOS-13 · Telemetry chips for a session-list row — the iOS mirror of the
|
|
/// web's `renderTelemetryGauge` (public/preview-grid.ts:197): context-usage %
|
|
/// (warn > 80), $cost to 4 places, model chip, PR badge.
|
|
///
|
|
/// Staleness: chips grey out when `StatusTelemetry.at` is STRICTLY older than
|
|
/// `Tunables.telemetryStaleTtlMs` (same `>` rule as the web's `tg-stale`).
|
|
/// The comparison lives in `Model`, computed from an injected `nowMs` so the
|
|
/// ViewModel tests are deterministic (task RED list).
|
|
///
|
|
/// Security (mirrors SEC-H5/SEC-L5): every telemetry string is server-supplied
|
|
/// UNTRUSTED input — rendered as plain `Text` only; the PR badge becomes a
|
|
/// tappable `Link` iff its URL parses as https, otherwise it renders as text.
|
|
struct TelemetryChips: View {
|
|
/// Immutable render input, derived once per list rebuild.
|
|
struct Model: Equatable, Sendable {
|
|
let contextUsedPct: Double?
|
|
/// Pre-formatted `$X.XXXX` (mirrors the web's `toFixed(4)`).
|
|
let costText: String?
|
|
let modelName: String?
|
|
/// Pre-formatted `PR #N`.
|
|
let prText: String?
|
|
let prReviewState: String?
|
|
/// Tappable PR destination — https URLs ONLY (SEC-L5 mirror), else nil.
|
|
let prURL: URL?
|
|
/// True when `at` is strictly older than `Tunables.telemetryStaleTtlMs`.
|
|
let isStale: Bool
|
|
|
|
/// nil when the session has no telemetry at all (row renders no chips).
|
|
init?(telemetry: StatusTelemetry?, nowMs: Int) {
|
|
guard let telemetry else { return nil }
|
|
isStale = nowMs - telemetry.at > Tunables.telemetryStaleTtlMs
|
|
contextUsedPct = telemetry.contextUsedPct
|
|
costText = telemetry.costUsd.map { String(format: Format.cost, $0) }
|
|
modelName = telemetry.model
|
|
if let pr = telemetry.pr {
|
|
prText = "PR #\(pr.number)"
|
|
prReviewState = pr.reviewState
|
|
prURL = Self.httpsOnlyURL(pr.url)
|
|
} else {
|
|
prText = nil
|
|
prReviewState = nil
|
|
prURL = nil
|
|
}
|
|
}
|
|
|
|
/// `ctx NN%` — same rounding as the web label (`Math.round`).
|
|
var contextText: String? {
|
|
contextUsedPct.map { "ctx \(Int($0.rounded()))%" }
|
|
}
|
|
|
|
/// Mirrors the web's `tg-ctx-warn` threshold (strictly > 80).
|
|
var isContextWarning: Bool {
|
|
(contextUsedPct ?? 0) > Format.contextWarnPct
|
|
}
|
|
|
|
var isEmpty: Bool {
|
|
contextUsedPct == nil && costText == nil && modelName == nil && prText == nil
|
|
}
|
|
|
|
/// SEC-L5 mirror: only https URLs are ever offered as link targets.
|
|
static func httpsOnlyURL(_ raw: String) -> URL? {
|
|
guard let url = URL(string: raw),
|
|
url.scheme?.lowercased() == Format.httpsScheme
|
|
else { return nil }
|
|
return url
|
|
}
|
|
}
|
|
|
|
let model: Model
|
|
|
|
/// Composed from the frozen `TelemetryChip` primitive — each chip is
|
|
/// mono-tabular, greys itself out when `isStale`, and the context chip goes
|
|
/// amber over the warn threshold. The PR chip becomes a tappable `Link`
|
|
/// only when the VM handed us an https URL (SEC-L5 boundary stays in Model).
|
|
var body: some View {
|
|
HStack(spacing: DS.Space.xs4) {
|
|
if let contextText = model.contextText {
|
|
TelemetryChip(
|
|
systemImage: Symbols.context,
|
|
text: contextText,
|
|
isStale: model.isStale,
|
|
isWarning: model.isContextWarning
|
|
)
|
|
}
|
|
if let costText = model.costText {
|
|
TelemetryChip(text: costText, isStale: model.isStale)
|
|
}
|
|
if let modelName = model.modelName {
|
|
TelemetryChip(
|
|
systemImage: Symbols.model, text: modelName, isStale: model.isStale
|
|
)
|
|
}
|
|
prBadge
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private var prBadge: some View {
|
|
if let prText = model.prText {
|
|
let label = model.prReviewState.map { "\(prText) · \($0)" } ?? prText
|
|
let chip = TelemetryChip(
|
|
systemImage: Symbols.pr, text: label, isStale: model.isStale
|
|
)
|
|
if let url = model.prURL {
|
|
Link(destination: url) { chip }
|
|
} else {
|
|
chip
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Chip icons (SF Symbols, no magic numbers/colors — DS owns those)
|
|
|
|
private enum Symbols {
|
|
static let context = "gauge.medium"
|
|
static let model = "cpu"
|
|
static let pr = "arrow.triangle.branch"
|
|
}
|
|
|
|
private enum Format {
|
|
/// Mirrors web `$${costUsd.toFixed(4)}`.
|
|
static let cost = "$%.4f"
|
|
/// Mirrors web `tg-ctx-warn` (`contextUsedPct > 80`).
|
|
static let contextWarnPct = 80.0
|
|
static let httpsScheme = "https"
|
|
}
|
|
}
|