diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 8247caf..ae2ab37 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -556,6 +556,17 @@ struct TranscriptList: View { guard transcriptSettling, transcriptContentHeight > 0 else { return } try? await Task.sleep(for: .milliseconds(80)) if Task.isCancelled { return } + // Pin to the true bottom before opening the gate. The settling layout lands a + // hair off the bottom, so the passive `.defaultScrollAnchor` alone leaves the + // scroll geometry reading "not at bottom" — and that stale reading latches + // `onScrollGeometryChange`'s tracked value at `false` while the gate above holds + // `isScrolledToBottom` at `true`. Because the callback only fires on a *change*, + // the user's first real scroll-up (still `false`) then never fires it: the chevron + // never appears and live output keeps yanking the view to the bottom, until a + // down-then-up round-trip finally re-emits the change. An explicit pin nudges the + // offset the last hair, forcing a fresh "at bottom" emission that re-syncs the + // tracker with the gate. Mirrors the Mac's reveal pin. + scrollToEnd(proxy, animated: false) transcriptSettling = false } // Coalescing means item count lags event count; key the autoscroll on the raw stream