feat(ios): P1-B — W7 UI wave: deep links, timeline, quick-reply, diff, projects, session switcher, thumbnails, lock-screen push
T-iOS-22: DeepLinkRouter (full-field whitelist, cold-start stash, route(from:) for push-tap reuse), 21 tests T-iOS-24: TimelineSheet mirroring web render() order; disabled→empty-state; reuses AwayDigest onExpand T-iOS-25: QuickReply chips (built-ins mirror quick-reply.ts via KeyByteMap; visible iff live gate && !readOnly) T-iOS-27: read-only DiffScreen + App-layer DiffFetcher (RO no-Origin; APIClient fold-in noted for T-iOS-38 owner) T-iOS-26: Projects list/detail — grouping byte-identical to web group keys (prefs-shared collapse state), prefs-clobber defenses (no blind PUT on empty base; adopt server echo), claude\r bootstrap T-iOS-23: UnreadLedger + TitleSanitizer (SessionCore, +15 tests), lastOutputAt decode, list-boundary re-sanitize T-iOS-29: new-in-cwd (untrusted cwd, no bootstrap re-injection) + exited-session reopen; fixes stale-controller SwiftTerm view bug via .id(controller.id) T-iOS-28: offscreen SwiftTerm thumbnail pipeline (LRU 32, concurrency gate 2, 256KiB cap, grid clamp) T-iOS-21: PushRegistrar (WEBTERM_GATE category: Allow=.authenticationRequired, no .foreground) + NotificationActionHandler (whitelisted payload, token never persisted, bg-task-wrapped POST, 403 fallback) CRITICAL fix (verify-found boot crash): @Sendable literals on UN completion closures — MainActor-inherited closures trapped Swift 6 executor check on UN's background queue; boot re-verified (no new crash reports, permission prompt reached, privacy shade correctly covering during system alert) Verified: 261 pkg + 247 app + 10 integration tests green; 7/7 semantics checks; Owns audit clean
This commit is contained in:
@@ -20,26 +20,51 @@ import UIKit
|
||||
/// T-iOS-15 — this screen only renders and routes.
|
||||
struct TerminalScreen: View {
|
||||
let viewModel: TerminalViewModel
|
||||
/// T-iOS-29 · "在当前目录开新会话":toolbar 常驻入口 + exit 横幅上的
|
||||
/// "开新会话"(同一动作 —— close→open fresh spawn,cwd 解析在
|
||||
/// `AppCoordinator.openNewSessionInCurrentCwd`)。nil = 两处入口都隐藏
|
||||
/// (预览/无 coordinator 的测试环境)。
|
||||
var onNewSessionInCwd: (@MainActor () -> Void)? = nil
|
||||
|
||||
private enum Metrics {
|
||||
static let bannerHorizontalPadding: CGFloat = 12
|
||||
static let bannerTopPadding: CGFloat = 8
|
||||
}
|
||||
|
||||
private enum Copy {
|
||||
static let newSessionInCwd = "在当前目录开新会话"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TerminalHostView(viewModel: viewModel)
|
||||
.ignoresSafeArea(.container, edges: .bottom)
|
||||
.overlay(alignment: .top) {
|
||||
if let model = viewModel.bannerModel {
|
||||
ReconnectBanner(model: model)
|
||||
ReconnectBanner(model: model, onNewSession: onNewSessionInCwd)
|
||||
.padding(.horizontal, Metrics.bannerHorizontalPadding)
|
||||
.padding(.top, Metrics.bannerTopPadding)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.animation(.default, value: viewModel.bannerModel)
|
||||
.toolbar { newSessionToolbarItem }
|
||||
.onAppear { viewModel.start() }
|
||||
}
|
||||
|
||||
/// Mirrors web `tabs.ts newTab()` (M6): the + affordance opens a fresh
|
||||
/// session in the active session's cwd, if known.
|
||||
@ToolbarContentBuilder private var newSessionToolbarItem: some ToolbarContent {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
if let onNewSessionInCwd {
|
||||
Button {
|
||||
onNewSessionInCwd()
|
||||
} label: {
|
||||
Label(Copy.newSessionInCwd, systemImage: "plus.rectangle.on.folder")
|
||||
}
|
||||
.accessibilityIdentifier("terminal.newInCwdButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftTerm bridge
|
||||
@@ -112,9 +137,15 @@ private struct TerminalHostView: UIViewRepresentable {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
// Title/cwd surface in the session list via the server (T-iOS-13/23),
|
||||
// not from the local emulator — deliberate no-ops.
|
||||
func setTerminalTitle(source: TerminalView, title: String) {}
|
||||
/// OSC 0/2 title (T-iOS-23): hostile input — the VM sanitizes
|
||||
/// (TitleSanitizer) before any UI/registry use, then the wiring
|
||||
/// surfaces it on the session-list row.
|
||||
func setTerminalTitle(source: TerminalView, title: String) {
|
||||
viewModel.setTerminalTitle(title)
|
||||
}
|
||||
|
||||
// cwd surfaces in the session list via the server (T-iOS-13), not
|
||||
// from the local emulator — deliberate no-op.
|
||||
func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {}
|
||||
func scrolled(source: TerminalView, position: Double) {}
|
||||
func rangeChanged(source: TerminalView, startY: Int, endY: Int) {}
|
||||
|
||||
Reference in New Issue
Block a user