Match pending sends by a send id the server echoes

This commit is contained in:
Ian Gulliver
2026-10-20 15:22:50 -07:00
parent b8c5331a0c
commit 8fec4b9040
29 changed files with 437 additions and 327 deletions
+33 -21
View File
@@ -151,9 +151,18 @@ struct PendingSend: Identifiable {
let local: Bool
let queued: Bool
let md: [MDBlock]
let sendID: String
var inputId: Int? = nil
}
// Sends not yet confirmed by the server, kept by the Store so they outlive the transcript showing them.
@MainActor
@Observable
final class LocalSends {
var items: [PendingSend] = []
@ObservationIgnored var ids = 0
}
@MainActor
private final class Run {
let first: Row
@@ -202,7 +211,7 @@ private final class Ctx {
@Observable
final class TranscriptBuilder {
let root = RowList()
private(set) var pending: [PendingSend] = []
private(set) var queued: [PendingSend] = []
private(set) var context: [String: Usage] = [:]
private(set) var open = false
private(set) var lastMergeAfterReview: Int?
@@ -216,14 +225,20 @@ final class TranscriptBuilder {
@ObservationIgnored private let worktree: () -> String
@ObservationIgnored private let running: () -> Bool
@ObservationIgnored private var pendingIds = 0
@ObservationIgnored private let local: LocalSends
init(taskId: String?, worktree: @escaping () -> String, running: @escaping () -> Bool) {
init(taskId: String?, local: LocalSends, worktree: @escaping () -> String, running: @escaping () -> Bool) {
self.taskId = taskId
self.local = local
rowTask = taskId
self.worktree = worktree
self.running = running
}
var pending: [PendingSend] {
queued + local.items
}
func stop() {
dropStream()
}
@@ -245,25 +260,23 @@ final class TranscriptBuilder {
root.rows.insert(contentsOf: box.rows, at: 0)
}
func showPending(_ text: String, _ images: [PendingImage]) -> () -> Void {
pendingIds += 1
let entry = PendingSend(id: pendingIds, text: text, images: images, imageIds: [], local: true, queued: running(), md: trimmed(text).isEmpty ? [] : parseMarkdown(text))
pending.append(entry)
// The returned closure drops the send and says whether it was still unconfirmed.
func showPending(_ text: String, _ images: [PendingImage], _ sendID: String) -> () -> Bool {
local.ids -= 1
let entry = PendingSend(id: local.ids, text: text, images: images, imageIds: [], local: true, queued: running(), md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: sendID)
local.items.append(entry)
ctx.run = nil
return { [weak self] in
self?.pending.removeAll { $0.id == entry.id }
return { [local] in
guard let i = local.items.firstIndex(where: { $0.id == entry.id }) else {
return false
}
local.items.remove(at: i)
return true
}
}
func withdrawn(_ inputId: Int) {
pending.removeAll { $0.inputId == inputId }
}
private func confirmPending(_ text: String, queued: Bool, local: Bool) {
let i = queued ? pending.firstIndex { !$0.local && $0.text == text } : nil
if let i = i ?? (local ? pending.firstIndex { $0.local && $0.text == text } : nil) {
pending.remove(at: i)
}
queued.removeAll { $0.inputId == inputId }
}
private func release() {
@@ -441,19 +454,18 @@ final class TranscriptBuilder {
if sink == nil && ((ev.kind == "agent.message" && parent.isEmpty) || ev.kind == "turn.ended" || ev.kind == "user.message") {
dropStream()
}
if sink == nil && own && ev.kind == "user.message" {
confirmPending(str(p, "text") ?? "", queued: true, local: ev.actor != "byre")
if sink == nil && own && ev.kind == "user.message", let sendID = str(p, "send_id") {
queued.removeAll { $0.sendID == sendID }
}
if sink == nil && own && (ev.kind == "task.merged" || ev.kind == "task.discarded") {
pending.removeAll { !$0.local }
queued.removeAll()
}
if ev.kind == "user.queued" {
if sink == nil && own {
let text = str(p, "text") ?? ""
confirmPending(text, queued: false, local: true)
pendingIds += 1
let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) }
pending.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, local: false, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text), inputId: int(p, "id")))
queued.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, local: false, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: str(p, "send_id") ?? "", inputId: int(p, "id")))
}
continue
}