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:
@@ -19,15 +19,26 @@ import SwiftUI
|
||||
/// removes the gate server-side → `planGate` goes nil → sheet dismisses.
|
||||
struct TerminalContainerView: View {
|
||||
let controller: TerminalSessionController
|
||||
/// T-iOS-29 · pass-through to TerminalScreen's toolbar/exit-banner
|
||||
/// "在当前目录开新会话" action (RootView supplies the coordinator hop).
|
||||
var onNewSessionInCwd: (@MainActor () -> Void)? = nil
|
||||
/// Epoch of a plan gate the user swiped away — suppresses re-present for
|
||||
/// THAT gate only; a new epoch re-presents automatically.
|
||||
@State private var dismissedPlanGateEpoch: Int?
|
||||
/// T-iOS-24 (additive) · Non-nil while the full-timeline sheet is up; a
|
||||
/// FRESH VM per presentation (each open re-fetches /events).
|
||||
@State private var timelineViewModel: TimelineViewModel?
|
||||
/// T-iOS-25 (additive) · Quick-reply palette store — per-container over
|
||||
/// `.standard` defaults (non-secret UI prefs, plan §5.3 split).
|
||||
@State private var quickReplyStore = QuickReplyStore()
|
||||
|
||||
private enum Metrics {
|
||||
static let overlaySpacing: CGFloat = 8
|
||||
static let overlayHorizontalPadding: CGFloat = 12
|
||||
/// Clears TerminalScreen's own top-aligned ReconnectBanner zone.
|
||||
static let overlayTopPadding: CGFloat = 52
|
||||
/// Breathing room between the chip row and the keyboard/key-bar edge.
|
||||
static let quickReplyBottomPadding: CGFloat = 8
|
||||
}
|
||||
|
||||
private enum Copy {
|
||||
@@ -35,11 +46,33 @@ struct TerminalContainerView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TerminalScreen(viewModel: controller.terminalViewModel)
|
||||
.id(controller.generation)
|
||||
TerminalScreen(
|
||||
viewModel: controller.terminalViewModel,
|
||||
onNewSessionInCwd: onNewSessionInCwd
|
||||
)
|
||||
.id(controller.generation)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.overlay(alignment: .top) { topOverlays }
|
||||
.overlay(alignment: .bottom) { quickReplyOverlay }
|
||||
.sheet(isPresented: planGateBinding) { planGateSheet }
|
||||
.sheet(item: $timelineViewModel) { TimelineSheet(viewModel: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Quick-reply chips (T-iOS-25, additive)
|
||||
|
||||
/// Chips float at the bottom edge (above the keyboard's safe area) ONLY
|
||||
/// while a gate is waiting — visibility/read-only logic lives in
|
||||
/// `QuickReplyBar.isVisible`, driven by the SAME fan-out branches the two
|
||||
/// VMs already consume (no extra branch needed).
|
||||
@ViewBuilder private var quickReplyOverlay: some View {
|
||||
QuickReplyBar(
|
||||
terminalViewModel: controller.terminalViewModel,
|
||||
gateViewModel: controller.gateViewModel,
|
||||
store: quickReplyStore
|
||||
)
|
||||
.padding(.horizontal, Metrics.overlayHorizontalPadding)
|
||||
.padding(.bottom, Metrics.quickReplyBottomPadding)
|
||||
.animation(.default, value: controller.gateViewModel.currentGate)
|
||||
}
|
||||
|
||||
// MARK: - Digest + tool gate (top stack)
|
||||
@@ -51,7 +84,7 @@ struct TerminalContainerView: View {
|
||||
AwayDigestView(
|
||||
digest: digest,
|
||||
isExpanded: gateViewModel.isDigestExpanded,
|
||||
onExpand: { gateViewModel.expandDigest() },
|
||||
onExpand: { expandDigestAndPresentTimeline() },
|
||||
onDismiss: { gateViewModel.dismissDigest() }
|
||||
)
|
||||
}
|
||||
@@ -73,6 +106,21 @@ struct TerminalContainerView: View {
|
||||
.animation(.default, value: gateViewModel.digest)
|
||||
}
|
||||
|
||||
// MARK: - Timeline drill-down (T-iOS-24, additive)
|
||||
|
||||
/// The digest「展开」affordance does BOTH: inline expansion (which cancels
|
||||
/// the auto-fade, T-iOS-14 semantics — the digest must survive under the
|
||||
/// sheet) and the full-timeline sheet. No adopted sessionId yet →
|
||||
/// `forSession` returns nil → inline expand only (defensive; a digest
|
||||
/// only ever arrives after `attached`).
|
||||
private func expandDigestAndPresentTimeline() {
|
||||
controller.gateViewModel.expandDigest()
|
||||
timelineViewModel = TimelineViewModel.forSession(
|
||||
controller.terminalViewModel.sessionId,
|
||||
source: controller.timelineEventsSource
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Plan gate sheet
|
||||
|
||||
private var hasDismissedPendingPlanGate: Bool {
|
||||
|
||||
Reference in New Issue
Block a user