fix: fresh connect uses resize+snapshot instead of full history replay; wire onInput
This commit is contained in:
parent
044a4920bb
commit
994b450fe4
|
|
@ -35,6 +35,10 @@ public final class SessionConnection: ObservableObject {
|
|||
/// Emits every JSON frame received from the server.
|
||||
public let stateEvents = PassthroughSubject<ServerToClient, Never>()
|
||||
|
||||
/// Emits snapshot content ready to feed directly into SwiftTerm:
|
||||
/// ESC[H + ESC[2J (clear+home) followed by the pane's current content.
|
||||
public let snapshots = PassthroughSubject<Data, Never>()
|
||||
|
||||
/// Tracks the WebSocket lifecycle.
|
||||
@Published public private(set) var connectionState: ConnectionState = .disconnected
|
||||
|
||||
|
|
@ -97,24 +101,41 @@ public final class SessionConnection: ObservableObject {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// JSON frames → `stateEvents` subject.
|
||||
// JSON frames → `stateEvents` + snapshot converter.
|
||||
ws.incomingJSON
|
||||
.sink { [weak self] frame in
|
||||
self?.stateEvents.send(frame)
|
||||
guard let self else { return }
|
||||
if case .snapshot(_, let base64) = frame {
|
||||
// Decode base64 → text, prepend clear+home, normalise line endings.
|
||||
if let raw = Data(base64Encoded: base64),
|
||||
let text = String(data: raw, encoding: .utf8) {
|
||||
let header = "\u{1B}[H\u{1B}[2J" // cursor home + clear screen
|
||||
let body = text.replacingOccurrences(of: "\n", with: "\r\n")
|
||||
self.snapshots.send(Data((header + body).utf8))
|
||||
}
|
||||
}
|
||||
self.stateEvents.send(frame)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Once connected, send the resume frame.
|
||||
// Once connected, send the appropriate opening frame.
|
||||
//
|
||||
// • lastSeq == nil → fresh attach: live output already flows; caller
|
||||
// will send resize then snapshotRequest. No frame sent here.
|
||||
// • lastSeq != nil → reconnect after gap: replay missed output.
|
||||
ws.connectionState
|
||||
.filter { $0 == .connected }
|
||||
.first()
|
||||
.sink { [weak self, weak ws, lastSeq] _ in
|
||||
guard let self, let ws else { return }
|
||||
Task { @MainActor [self, ws, lastSeq] in
|
||||
try? await ws.send(.resume(lastSeq: lastSeq))
|
||||
_ = self // silence unused-capture warning
|
||||
if let seq = lastSeq {
|
||||
Task { @MainActor [self, ws, seq] in
|
||||
try? await ws.send(.resume(lastSeq: seq))
|
||||
_ = self
|
||||
}
|
||||
}
|
||||
// fresh connect: caller drives resize → snapshotRequest
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
ws.connect(url: url)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ struct MainTerminalView: View {
|
|||
statusText = "Connecting to \(sessionId)…"
|
||||
let conn = SessionConnection(id: sessionId, credential: credential)
|
||||
|
||||
// Wire stream → terminal
|
||||
// Wire live ANSI stream → terminal
|
||||
conn.stream
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [terminalVC] data in
|
||||
|
|
@ -70,6 +70,14 @@ struct MainTerminalView: View {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Wire snapshots → terminal (already contains ESC[H + ESC[2J prefix)
|
||||
conn.snapshots
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [terminalVC] data in
|
||||
terminalVC.feed(data: data)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Wire connection state → status text
|
||||
conn.$connectionState
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
|
@ -88,17 +96,38 @@ struct MainTerminalView: View {
|
|||
Task { try? await conn.send(.resize(cols: cols, rows: rows)) }
|
||||
}
|
||||
|
||||
connection = conn
|
||||
await conn.resume(from: conn.scrollback.sizeBytes > 0
|
||||
? ResumeCursor().lastSeq(for: sessionId)
|
||||
: nil)
|
||||
// Wire keyboard input from SwiftTerm → sidecar.
|
||||
terminalVC.onInput = { [weak conn] data in
|
||||
guard let conn,
|
||||
let text = String(data: data, encoding: .utf8) else { return }
|
||||
Task { try? await conn.send(.keys(data: text)) }
|
||||
}
|
||||
|
||||
// Send the current terminal size immediately after connecting
|
||||
// so tmux resizes before the first output frame arrives.
|
||||
// On first connection: resize → brief pause (SIGWINCH propagation)
|
||||
// → snapshot. This avoids replaying history at the wrong size.
|
||||
conn.$connectionState
|
||||
.filter { $0 == .connected }
|
||||
.first()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak conn, terminalVC] _ in
|
||||
Task { @MainActor in
|
||||
let (cols, rows) = terminalVC.terminalSize
|
||||
if cols > 0 && rows > 0 {
|
||||
try? await conn.send(.resize(cols: cols, rows: rows))
|
||||
try? await conn?.send(.resize(cols: cols, rows: rows))
|
||||
}
|
||||
// Let tmux propagate SIGWINCH before snapshotting.
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
try? await conn?.send(.snapshotRequest)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
connection = conn
|
||||
|
||||
let lastSeq: UInt64? = conn.scrollback.sizeBytes > 0
|
||||
? ResumeCursor().lastSeq(for: sessionId)
|
||||
: nil
|
||||
await conn.resume(from: lastSeq)
|
||||
} catch {
|
||||
statusText = "Error: \(error.localizedDescription)"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue