ios: render Agent history lazily
TestFlight / Build and upload (push) Successful in 1m49s

This commit is contained in:
2026-10-16 12:10:29 -07:00
parent bbaa5e8859
commit d5427926d5
4 changed files with 47 additions and 46 deletions
@@ -30,7 +30,6 @@ private struct AgentTranscriptView: View {
@State private var historyStartID: String?
@State private var scrollPosition = ScrollPosition()
@State private var scrollTracking = AgentScrollTracking()
@State private var restoreAnchor: AgentReadingAnchor?
@State private var appeared = false
private let bottomID = "agent-bottom"
@@ -51,16 +50,12 @@ private struct AgentTranscriptView: View {
let displayed = transcript.displayed
ScrollViewReader { proxy in
ScrollView {
// Exact eager geometry preserves reading/prepend offsets. Lazy
// estimates required repeated corrections and failed those anchors;
// isolate observation instead of adding a geometry feedback loop.
VStack(alignment: .leading, spacing: 15) {
LazyVStack(alignment: .leading, spacing: 15) {
if allMessages.count > displayed.count {
Button("Load earlier messages") {
if var anchor = visibleReadingAnchor {
anchor.expectedCount = min(allMessages.count, displayed.count + 100)
restoreAnchor = anchor
}
// Loading older history may move the viewport as lazy
// row estimates settle; do not chase the old position.
scrollTracking.rowFrames = [:]
atLatest = false
historyLimit += 100
historyStartID = allMessages[max(0, startIndex - 100)].id
@@ -104,7 +99,6 @@ private struct AgentTranscriptView: View {
.scrollDismissesKeyboard(.interactively)
.onScrollPhaseChange { _, phase in
isScrolling = phase.isScrolling
if phase == .interacting || phase == .tracking { restoreAnchor = nil }
}
.onScrollGeometryChange(for: AgentScrollMetrics.self) { geometry in
AgentScrollMetrics(offset: geometry.contentOffset.y,
@@ -119,7 +113,7 @@ private struct AgentTranscriptView: View {
scrollTracking.metrics = next
if scrollTracking.atBottom != next.atBottom { scrollTracking.atBottom = next.atBottom }
if isScrolling || next.contentHeight <= next.height { atLatest = next.atBottom }
if atLatest, !isScrolling, restoreAnchor == nil,
if atLatest, !isScrolling,
previous.height != next.height || previous.contentHeight != next.contentHeight || previous.bottomInset != next.bottomInset {
proxy.scrollTo(bottomID, anchor: .bottom)
}
@@ -128,10 +122,10 @@ private struct AgentTranscriptView: View {
#if DEBUG
AgentRenderDiagnostics.record("rowPreferences")
#endif
let anchor = restoreAnchor ?? (!atLatest && !isScrolling ? visibleReadingAnchor : nil)
let anchor = !atLatest && !isScrolling ? visibleReadingAnchor : nil
if let anchor, let frame = frames[anchor.id], abs(frame.minY - anchor.offset) > 1 {
// Restore the exact pixel offset after insertion or text
// reflow. Normalize the saved frames to
// Preserve ordinary reading position after new messages or
// text reflow. Normalize the saved frames to
// the requested offset so the next layout cannot undo it.
let requested = scrollTracking.metrics.offset + frame.minY - anchor.offset
let target = min(max(requested, -scrollTracking.metrics.topInset), scrollTracking.metrics.maximumOffset)
@@ -142,16 +136,13 @@ private struct AgentTranscriptView: View {
// zero at the top inset, including the safe area.
scrollPosition.scrollTo(y: target + scrollTracking.metrics.topInset)
}
restoreAnchor = nil
} else {
scrollTracking.rowFrames = frames
if let expected = restoreAnchor?.expectedCount, frames.count >= expected { restoreAnchor = nil }
}
}
.overlay(alignment: .bottomTrailing) {
if !atLatest {
Button {
restoreAnchor = nil
atLatest = true
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.24)) { proxy.scrollTo(bottomID, anchor: .bottom) }
} label: {
@@ -176,7 +167,6 @@ private struct AgentTranscriptView: View {
if atLatest { proxy.scrollTo(bottomID, anchor: .bottom) }
}
.onChange(of: latestRequest) { _, _ in
restoreAnchor = nil
atLatest = true
proxy.scrollTo(bottomID, anchor: .bottom)
}
@@ -407,7 +397,6 @@ private struct AgentScrollMetrics: Equatable {
private struct AgentReadingAnchor {
var id: String
var offset: CGFloat
var expectedCount: Int? = nil
}
private struct AgentRowFrames: PreferenceKey {
static let defaultValue: [String: CGRect] = [:]