stay and start at first attention item

This commit is contained in:
Ian Gulliver
2026-10-19 10:00:37 -07:00
parent a3ea056531
commit e08a32843e
3 changed files with 35 additions and 3 deletions
+33 -1
View File
@@ -28,6 +28,7 @@ final class ChatModel {
private(set) var hasUnmerged = false
@ObservationIgnored var stuck = true
@ObservationIgnored var prepended = false
@ObservationIgnored var released: Int?
@ObservationIgnored private var unmergedTimer: Task<Void, Never>?
@ObservationIgnored private var offs: [() -> Void] = []
@ObservationIgnored private var focusStart = true
@@ -335,6 +336,18 @@ final class ChatModel {
return groups
}
var held: Int? {
let ids = Set(attention().flatMap(\.rows))
guard let first = builder.root.rows.first(where: { ids.contains($0.id) })?.id, first != released else {
return nil
}
return first
}
func release() {
released = held ?? released
}
func cycle(_ g: AttentionGroup) {
let next = (cursors[g.id] ?? -1) + 1
let i = next < g.rows.count ? next : 0
@@ -770,16 +783,34 @@ private struct TranscriptScroll: View {
model.stuck = new.distance < 80
return
}
if model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) {
if !model.stuck || (new.content == old.content && new.visible == old.visible) {
return
}
if let id = model.held {
position.scrollTo(id: id, anchor: .top)
} else if new.distance > 1 {
position.scrollTo(edge: .bottom)
}
}
.onScrollPhaseChange { _, phase in
scrolling = phase.isScrolling
if phase == .interacting {
model.release()
}
if !scrolling {
model.prependOlder()
}
}
.onChange(of: model.held) { _, id in
if !model.stuck || scrolling {
return
}
if let id {
position.scrollTo(id: id, anchor: .top)
} else {
position.scrollTo(edge: .bottom)
}
}
.onChange(of: model.older == nil) {
if !scrolling {
model.prependOlder()
@@ -789,6 +820,7 @@ private struct TranscriptScroll: View {
guard let new, new > old ?? 0 else {
return
}
model.release()
model.stuck = true
position.scrollTo(edge: .bottom)
}