Files
web-terminal/ios/App/WebTerm/Components/TelemetryChips.swift
Yaojia Wang 5098643355 feat(ios): W3 UI layer + integration CI + ntfy docs
T-iOS-11: TerminalScreen/KeyBar/TerminalViewModel + KeyByteMap (byte-for-byte keybar.ts, arrows excluded from UIKeyCommand to preserve DECCKM)
T-iOS-12: PairingScreen/VM — confirm-before-network (zero-call assertions), §5.4 four-tier warnings, Host construction per contract ruling
T-iOS-13: SessionListScreen/VM — Tunables-paced polling with leak-free teardown, optimistic kill+rollback, pending via overlay (LiveSessionInfo has no pending field)
T-iOS-14: GateBanner/PlanGateSheet/AwayDigestView/GateViewModel — three-way mapping from SessionCore Affordance single source, tap-epoch guard, per-epoch haptics
T-iOS-16: IntegrationTests vs real Node server (10 tests: origin guards, mirror, kill-close vs exit-frame differential, 16MiB+ESC/C0 replay) + ios.yml own-sources coverage gate (red-once demoed)
T-iOS-17: ios/README.md ntfy chapter (read-only verification, file:line cites)
Verified: 224 unit + 10 integration tests green; 5/5 semantic spot-checks; zero Owns violations
2026-07-05 00:13:14 +02:00

128 lines
4.6 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
var body: some View {
HStack(spacing: Metrics.chipSpacing) {
if let contextText = model.contextText {
Text(contextText)
.foregroundStyle(model.isContextWarning ? Color.orange : Color.secondary)
}
if let costText = model.costText {
chip(costText)
}
if let modelName = model.modelName {
chip(modelName)
}
prBadge
}
.font(.caption2.monospacedDigit())
.lineLimit(1)
.opacity(model.isStale ? Metrics.staleOpacity : 1)
.grayscale(model.isStale ? 1 : 0)
}
@ViewBuilder private var prBadge: some View {
if let prText = model.prText {
let label = model.prReviewState.map { "\(prText) · \($0)" } ?? prText
if let url = model.prURL {
Link(label, destination: url)
} else {
chip(label)
}
}
}
private func chip(_ text: String) -> some View {
Text(text)
.foregroundStyle(.secondary)
.padding(.horizontal, Metrics.chipHorizontalPadding)
.background(.quaternary, in: Capsule())
}
// MARK: - Named constants (no magic numbers, plan §4)
private enum Metrics {
static let chipSpacing: CGFloat = 6
static let chipHorizontalPadding: CGFloat = 5
static let staleOpacity = 0.45
}
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"
}
}