2026-10-16 22:46:54 -07:00
|
|
|
import Foundation
|
|
|
|
|
import Observation
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class Listeners<T> {
|
|
|
|
|
private var fns: [(id: UUID, fn: (T) -> Void)] = []
|
|
|
|
|
|
|
|
|
|
func add(_ fn: @escaping (T) -> Void) -> () -> Void {
|
|
|
|
|
let id = UUID()
|
|
|
|
|
fns.append((id, fn))
|
|
|
|
|
return { [weak self] in
|
|
|
|
|
self?.fns.removeAll { $0.id == id }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func send(_ value: T) {
|
|
|
|
|
for entry in fns {
|
|
|
|
|
entry.fn(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 20:53:28 -07:00
|
|
|
struct Capture: Identifiable {
|
|
|
|
|
let id: String
|
|
|
|
|
let tool: String
|
|
|
|
|
let note: String
|
|
|
|
|
let when: Date
|
|
|
|
|
let base64: String
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:46:54 -07:00
|
|
|
@MainActor
|
|
|
|
|
@Observable
|
|
|
|
|
final class Feed {
|
|
|
|
|
private(set) var events: [ByreEvent] = []
|
2026-10-22 20:53:28 -07:00
|
|
|
private(set) var captures: [Capture] = []
|
2026-10-16 22:46:54 -07:00
|
|
|
private(set) var ready = false
|
2026-10-20 09:14:44 -07:00
|
|
|
private(set) var inherited = 0
|
2026-10-16 22:46:54 -07:00
|
|
|
let appended = Listeners<[ByreEvent]>()
|
|
|
|
|
let streamed = Listeners<ByreEvent>()
|
|
|
|
|
@ObservationIgnored private var last: Int64 = 0
|
|
|
|
|
@ObservationIgnored private var buffer: [ByreEvent] = []
|
|
|
|
|
@ObservationIgnored private var scheduled = false
|
2026-10-22 20:53:28 -07:00
|
|
|
@ObservationIgnored private var toolNames: [String: String] = [:]
|
2026-10-16 22:46:54 -07:00
|
|
|
@ObservationIgnored private let shown: () -> Bool
|
|
|
|
|
|
|
|
|
|
init(shown: @escaping () -> Bool) {
|
|
|
|
|
self.shown = shown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func push(_ ev: ByreEvent) {
|
|
|
|
|
if ev.seq == 0 && (!ready || !shown()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
buffer.append(ev)
|
|
|
|
|
if ready && !scheduled {
|
|
|
|
|
scheduled = true
|
|
|
|
|
Task { @MainActor in
|
|
|
|
|
self.flush()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 09:14:44 -07:00
|
|
|
func load(_ history: [ByreEvent], inherited: Int = 0) {
|
2026-10-16 22:46:54 -07:00
|
|
|
if ready {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ready = true
|
2026-10-20 09:14:44 -07:00
|
|
|
self.inherited = inherited
|
2026-10-16 22:46:54 -07:00
|
|
|
buffer = history + buffer
|
|
|
|
|
flush()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 09:14:44 -07:00
|
|
|
var own: ArraySlice<ByreEvent> {
|
|
|
|
|
events.dropFirst(inherited)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:46:54 -07:00
|
|
|
func prepend(_ older: [ByreEvent]) {
|
|
|
|
|
events.insert(contentsOf: older, at: 0)
|
2026-10-22 20:53:28 -07:00
|
|
|
captures.insert(contentsOf: findCaptures(older), at: 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func findCaptures(_ batch: [ByreEvent]) -> [Capture] {
|
|
|
|
|
var found: [Capture] = []
|
|
|
|
|
for ev in batch {
|
|
|
|
|
let p = ev.payload.value
|
|
|
|
|
if ev.kind == "agent.tool_call" {
|
|
|
|
|
toolNames[jsString(p["id"])] = toolLabel(jsString(p["name"]))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
guard ev.kind == "agent.tool_result", let list = p["content"] as? [(any Sendable)?] else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
let blocks = list.compactMap { $0 as? JSONDict }
|
|
|
|
|
let note = blocks.compactMap { str($0, "type") == "text" ? str($0, "text") : nil }.first ?? ""
|
|
|
|
|
for (i, b) in blocks.enumerated() {
|
|
|
|
|
guard str(b, "type") == "image", let source = dict(b, "source"), let data = str(source, "data") else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
found.append(Capture(id: "\(ev.seq)/\(i)", tool: toolNames[jsString(p["tool_use_id"])] ?? "", note: note, when: ev.createdAt, base64: data))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return found
|
2026-10-16 22:46:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func flush() {
|
|
|
|
|
scheduled = false
|
|
|
|
|
let pending = buffer
|
|
|
|
|
buffer = []
|
|
|
|
|
var stored: [ByreEvent] = []
|
|
|
|
|
for ev in pending {
|
|
|
|
|
if ev.seq != 0 {
|
|
|
|
|
stored.append(ev)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
append(stored)
|
|
|
|
|
stored = []
|
|
|
|
|
if shown() {
|
|
|
|
|
streamed.send(ev)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
append(stored)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func append(_ batch: [ByreEvent]) {
|
|
|
|
|
var fresh: [ByreEvent] = []
|
|
|
|
|
for ev in batch where ev.seq > last {
|
|
|
|
|
fresh.append(ev)
|
|
|
|
|
last = ev.seq
|
|
|
|
|
}
|
|
|
|
|
if fresh.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
events.append(contentsOf: fresh)
|
2026-10-22 20:53:28 -07:00
|
|
|
let found = findCaptures(fresh)
|
|
|
|
|
if !found.isEmpty {
|
|
|
|
|
captures.append(contentsOf: found)
|
|
|
|
|
}
|
2026-10-16 22:46:54 -07:00
|
|
|
if shown() {
|
|
|
|
|
appended.send(fresh)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|