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:
@@ -67,6 +67,10 @@ final class TerminalViewModel {
|
||||
/// Server-adopted session id (ALWAYS the server-issued one — persisting it
|
||||
/// per host is the T-iOS-15 wiring's job via `LastSessionStore`).
|
||||
private(set) var sessionId: UUID?
|
||||
/// Sanitized OSC title (T-iOS-23). Raw `setTerminalTitle` delegate input
|
||||
/// is HOST/ATTACKER-CONTROLLED and passes `TitleSanitizer` at THIS
|
||||
/// boundary; nil = no title (empty after sanitisation).
|
||||
private(set) var terminalTitle: String?
|
||||
|
||||
/// Read-only = no input reaches the PTY (exit / terminal failure). Resize
|
||||
/// is NOT gated here — the engine owns terminal-state dropping.
|
||||
@@ -121,6 +125,17 @@ final class TerminalViewModel {
|
||||
/// Test tap, called after each event is applied (state already coherent).
|
||||
@ObservationIgnored var onEventApplied: (@MainActor (SessionEvent) -> Void)?
|
||||
|
||||
// MARK: - OSC title surface (T-iOS-23; wired by TerminalSessionController)
|
||||
|
||||
/// List-side registry hook: fires with the ADOPTED sessionId and the
|
||||
/// SANITIZED title ("" = title cleared — the registry drops the entry).
|
||||
/// Titles arriving before adoption are held and forwarded once on
|
||||
/// `.adopted` (defensive — `attached` always precedes output on the wire).
|
||||
@ObservationIgnored var onTitleChanged: (@MainActor (UUID, String) -> Void)?
|
||||
/// Latest sanitized title not yet delivered to `onTitleChanged` because
|
||||
/// no sessionId was known at the time. nil = nothing held.
|
||||
@ObservationIgnored private var heldTitleForward: String?
|
||||
|
||||
private struct CountWaiter {
|
||||
let target: Int
|
||||
let continuation: CheckedContinuation<Void, Never>
|
||||
@@ -194,6 +209,19 @@ final class TerminalViewModel {
|
||||
enqueueSend(.input(data: data))
|
||||
}
|
||||
|
||||
/// SwiftTerm reported an OSC 0/2 title (`setTerminalTitle` delegate).
|
||||
/// Sanitize FIRST — the raw string is untrusted terminal output — then
|
||||
/// surface locally and forward to the list registry (T-iOS-23).
|
||||
func setTerminalTitle(_ raw: String) {
|
||||
let sanitized = TitleSanitizer.sanitize(raw)
|
||||
terminalTitle = sanitized.isEmpty ? nil : sanitized
|
||||
guard let sessionId else {
|
||||
heldTitleForward = sanitized
|
||||
return
|
||||
}
|
||||
onTitleChanged?(sessionId, sanitized)
|
||||
}
|
||||
|
||||
/// Terminal geometry changed (SwiftTerm `sizeChanged`). Always forwarded —
|
||||
/// the engine validates bounds and owns terminal-state dropping. Valid
|
||||
/// dims are remembered in `lastSentDims` so the T-iOS-15 wiring can feed
|
||||
@@ -214,6 +242,10 @@ final class TerminalViewModel {
|
||||
applyConnection(state)
|
||||
case .adopted(let id):
|
||||
sessionId = id // ALWAYS adopt the server-issued id
|
||||
if let held = heldTitleForward {
|
||||
heldTitleForward = nil
|
||||
onTitleChanged?(id, held)
|
||||
}
|
||||
case .output(let data):
|
||||
deliverOutput(data)
|
||||
case .exited(let code, let reason):
|
||||
|
||||
Reference in New Issue
Block a user