Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions Sources/Halos/Views/ActivityListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct ActivityListView: View {
} else {
ForEach(messages) { message in
ActivityRowView(message: message, resolveVeyraApproval: resolveVeyraApproval)
.equatable()
.id(message.id)
}
}
Expand Down Expand Up @@ -77,24 +78,15 @@ struct ActivityListView: View {
guard viewportHeight > 0 else { return }
isPinnedToBottom = bottomOffset <= viewportHeight + bottomFollowThreshold
}
.onChange(of: contentFingerprint) { _, _ in
.onChange(of: contentToken) { _, _ in
guard isPinnedToBottom else { return }
scrollToBottom(proxy, animated: true)
}
}
}

private var contentFingerprint: String {
messages
.map { message in
[
message.id,
message.title,
message.body,
message.streamState.rawValue,
].joined(separator: "\u{1F}")
}
.joined(separator: "\u{1E}")
private var contentToken: [ActivityListContentToken] {
messages.map(ActivityListContentToken.init)
}

private func scrollToBottom(_ proxy: ScrollViewProxy, animated: Bool) {
Expand Down Expand Up @@ -124,6 +116,18 @@ struct ActivityListView: View {
}
}

private struct ActivityListContentToken: Equatable {
let id: String
let bodyByteCount: Int
let streamState: MessageStreamState

init(message: ActivityMessage) {
self.id = message.id
self.bodyByteCount = message.body.utf8.count
self.streamState = message.streamState
}
}

private enum ActivityListCoordinateSpace {
static let name = "activity-list-scroll"
}
Expand Down
24 changes: 19 additions & 5 deletions Sources/Halos/Views/ActivityRowView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

struct ActivityRowView: View {
struct ActivityRowView: View, Equatable {
let message: ActivityMessage
let resolveVeyraApproval: ((String, VeyraApprovalDecision) -> Void)?
@Environment(\.accessibilityReduceMotion) private var reduceMotion
Expand All @@ -15,6 +15,10 @@ struct ActivityRowView: View {
self.resolveVeyraApproval = resolveVeyraApproval
}

nonisolated static func == (lhs: ActivityRowView, rhs: ActivityRowView) -> Bool {
lhs.message == rhs.message
}

var body: some View {
if let approval = message.veyraApproval {
veyraApprovalCard(approval)
Expand Down Expand Up @@ -295,8 +299,8 @@ struct ActivityRowView: View {

if isWorkSummaryExpanded {
VStack(alignment: .leading, spacing: 7) {
ForEach(workSummaryLines, id: \.self) { line in
markdownText(line)
ForEach(workSummaryLines) { line in
markdownText(line.text)
.font(.system(size: 11.5, weight: .medium))
.foregroundStyle(HalosTheme.tertiaryText)
.fixedSize(horizontal: false, vertical: true)
Expand All @@ -314,10 +318,11 @@ struct ActivityRowView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}

private var workSummaryLines: [String] {
private var workSummaryLines: [WorkSummaryLine] {
message.body
.split(separator: "\n", omittingEmptySubsequences: true)
.map(String.init)
.enumerated()
.map { index, line in WorkSummaryLine(index: index, text: String(line)) }
}

private func syncWorkSummaryExpansionIfNeeded() {
Expand All @@ -336,6 +341,15 @@ struct ActivityRowView: View {
}
}

private struct WorkSummaryLine: Identifiable {
let index: Int
let text: String

var id: Int {
index
}
}

struct HalosThinkingEyesView: View {
let isActive: Bool
@Environment(\.accessibilityReduceMotion) private var reduceMotion
Expand Down
Loading