ios: experiment with inverted Agent transcript layout

This commit is contained in:
2026-10-16 14:27:01 -07:00
parent ff48a25186
commit 9d862112b5
@@ -45,25 +45,9 @@ private struct AgentTranscriptView: View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 15) {
if allMessages.count > displayed.count {
Button("Load earlier messages") {
// Loading older history may move the viewport as lazy
// row estimates settle; do not chase the old position.
atLatest = false
historyLimit += 100
historyStartID = allMessages[max(0, startIndex - 100)].id
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
if room.isLoading { ProgressView("Loading messages…").frame(maxWidth: .infinity) }
ForEach(Array(displayed.enumerated()), id: \.element.id) { index, message in
AgentTranscriptRow(message: message, room: room,
startsDay: index == 0 || !Calendar.current.isDate(message.createdAt, inSameDayAs: displayed[index - 1].createdAt))
.equatable()
.modifier(AgentEntrance(animate: appeared && rendered.entranceIDs.contains(message.id), reduceMotion: reduceMotion))
.id(message.id)
}
// The native scroll origin is the visual bottom. Keep model
// and cursor order ascending; reverse only presentation.
Color.clear.frame(height: 17).id(Self.bottomID)
if rendered.isTyping {
HStack(spacing: 9) {
AgentAvatar()
@@ -72,33 +56,49 @@ private struct AgentTranscriptView: View {
}
.foregroundStyle(SybilTheme.textMuted)
.accessibilityIdentifier("agent-typing")
.scaleEffect(x: 1, y: -1)
}
ForEach(Array(displayed.enumerated().reversed()), id: \.element.id) { index, message in
AgentTranscriptRow(message: message, room: room,
startsDay: index == 0 || !Calendar.current.isDate(message.createdAt, inSameDayAs: displayed[index - 1].createdAt))
.equatable()
.modifier(AgentEntrance(animate: appeared && rendered.entranceIDs.contains(message.id), reduceMotion: reduceMotion))
.scaleEffect(x: 1, y: -1)
.id(message.id)
}
if room.isLoading {
ProgressView("Loading messages…").frame(maxWidth: .infinity)
.scaleEffect(x: 1, y: -1)
}
if allMessages.count > displayed.count {
Button("Load earlier messages") {
atLatest = false
historyLimit += 100
historyStartID = allMessages[max(0, startIndex - 100)].id
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.scaleEffect(x: 1, y: -1)
}
// Include bottom padding in the target so Latest reaches
// the actual content end before native keyboard avoidance.
Color.clear.frame(height: 17).id(Self.bottomID)
}
// Keep native lazy-row targets without binding scroll position.
.scrollTargetLayout()
.padding(.horizontal, 16)
// Match normal chat: content clears the header, while the
// viewport extends beneath the shared navigation fade.
.padding(.top, 16 + topInset)
.padding(.bottom, 16 + topInset)
.frame(maxWidth: SybilLayout.webContentMaxWidth)
.frame(maxWidth: .infinity)
}
.scaleEffect(x: 1, y: -1)
.accessibilityIdentifier("agent-transcript")
.defaultScrollAnchor(.bottom, for: .initialOffset)
// Let SwiftUI follow size changes, including keyboard avoidance,
// without competing scrollTo calls from geometry updates.
.defaultScrollAnchor(atLatest ? .bottom : nil, for: .sizeChanges)
.defaultScrollAnchor(.bottom, for: .alignment)
.scrollDismissesKeyboard(.interactively)
.onScrollPhaseChange { _, phase in
scrollPhase = phase
}
.onScrollGeometryChange(for: Bool.self) { geometry in
geometry.containerSize.height > 0 &&
geometry.contentSize.height + geometry.contentInsets.bottom - geometry.visibleRect.maxY <= 24
geometry.contentOffset.y + geometry.contentInsets.top <= 24
} action: { _, atBottom in
#if DEBUG
AgentRenderDiagnostics.record("scrollGeometry")
@@ -114,7 +114,7 @@ private struct AgentTranscriptView: View {
if !atLatest {
Button {
atLatest = true
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.24)) { proxy.scrollTo(Self.bottomID, anchor: .bottom) }
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.24)) { proxy.scrollTo(Self.bottomID, anchor: .top) }
} label: {
Label(room.unreadCount > 0 ? "\(room.unreadCount) unread" : "Latest", systemImage: "arrow.down")
.font(.sybil(.caption, weight: .semibold))
@@ -130,22 +130,17 @@ private struct AgentTranscriptView: View {
appeared = true
if !room.isLoading {
historyStartID = displayed.first?.id
proxy.scrollTo(Self.bottomID, anchor: .bottom)
}
}
.onChange(of: room.isLoading) { _, loading in
if !loading && historyStartID == nil {
historyStartID = displayed.first?.id
proxy.scrollTo(Self.bottomID, anchor: .bottom)
}
}
.onChange(of: allMessages.count) { _, _ in
if atLatest { proxy.scrollTo(Self.bottomID, anchor: .bottom) }
}
.onChange(of: latestRequest) { _, _ in
if !atLatest {
atLatest = true
proxy.scrollTo(Self.bottomID, anchor: .bottom)
proxy.scrollTo(Self.bottomID, anchor: .top)
}
}
.modifier(AgentReadAcknowledgement(room: room, tracking: scrollTracking,