Files
web-terminal/ios/App/WebTerm/Components/ReconnectBanner.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

97 lines
3.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
import WireProtocol
/// T-iOS-11 · Connection-state banner over the terminal (plan §1: the terminal
/// must NEVER "look connected but dead" every non-live state is explicit).
///
/// Render input is the `Model` enum only; the mapping from engine events to a
/// `Model` lives in `TerminalViewModel.bannerModel` (tested there).
struct ReconnectBanner: View {
/// What the banner says. Terminal phases (`failed`/`exited`) win over
/// transient connection states (mapping in `TerminalViewModel.bannerModel`).
enum Model: Equatable {
case connecting
case reconnecting(attempt: Int, retryIn: Duration)
/// Non-retryable failure: actionable copy, NO spinner, NO retry loop.
case failed(message: String)
/// Session over terminal is read-only from here on.
case exited(code: Int, reason: String?)
}
let model: Model
private enum Metrics {
static let contentSpacing: CGFloat = 8
static let horizontalPadding: CGFloat = 14
static let verticalPadding: CGFloat = 8
static let backgroundOpacity = 0.9
}
var body: some View {
HStack(spacing: Metrics.contentSpacing) {
if isSpinning {
ProgressView()
.controlSize(.small)
} else {
Image(systemName: iconName)
}
Text(text)
.font(.footnote)
.multilineTextAlignment(.leading)
}
.padding(.horizontal, Metrics.horizontalPadding)
.padding(.vertical, Metrics.verticalPadding)
.background(tint.opacity(Metrics.backgroundOpacity), in: Capsule())
.foregroundStyle(.white)
.accessibilityElement(children: .combine)
}
private var isSpinning: Bool {
switch model {
case .connecting, .reconnecting: return true
case .failed, .exited: return false
}
}
private var iconName: String {
switch model {
case .failed: return "exclamationmark.octagon.fill"
case .exited: return "flag.checkered"
case .connecting, .reconnecting: return "wifi"
}
}
private var tint: Color {
switch model {
case .connecting: return .gray
case .reconnecting: return .orange
case .failed: return .red
case .exited: return .indigo
}
}
private var text: String {
switch model {
case .connecting:
return "连接中…"
case .reconnecting(let attempt, let retryIn):
return "连接已断开 · 第 \(attempt) 次重连将在 \(retryIn.components.seconds)s 后…"
case .failed(let message):
return message
case .exited(let code, let reason):
return Self.exitText(code: code, reason: reason)
}
}
/// Exit copy: spawn failure (-1, M4) reads as an error; a normal exit reads
/// as a conclusion. `reason` is server-supplied display text (untrusted
/// rendered as plain text only).
private static func exitText(code: Int, reason: String?) -> String {
if code == WireConstants.spawnFailedExitCode {
return "启动 shell 失败:\(reason ?? "未知原因")"
}
let suffix = reason.map { "\($0)" } ?? ""
return "会话已退出 exit \(code)\(suffix) · 终端已只读"
}
}