Match pending sends by a send id the server echoes
This commit is contained in:
@@ -60,7 +60,7 @@ final class ChatModel {
|
||||
case .task(let id): id.flatMap { store?.currentSession($0) }
|
||||
}
|
||||
}
|
||||
builder = TranscriptBuilder(taskId: taskId, worktree: { [weak store] in
|
||||
builder = TranscriptBuilder(taskId: taskId, local: store.sends(taskId ?? "lead"), worktree: { [weak store] in
|
||||
taskId.flatMap { store?.findTask($0)?.worktreePath } ?? ""
|
||||
}, running: {
|
||||
session()?.state == .running
|
||||
@@ -458,11 +458,11 @@ final class ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
func send(sessionId: String?, text: String, images: [String]?, model: String?, firstModel: String) async throws {
|
||||
func send(sessionId: String?, text: String, images: [String]?, model: String?, firstModel: String, sendID: String) async throws {
|
||||
let api = store.api.client
|
||||
switch kind {
|
||||
case .lead:
|
||||
_ = try await api.messageLead(body: .json(.init(text: text, images: images, model: sessionId != nil ? model : firstModel))).accepted
|
||||
_ = try await api.messageLead(body: .json(.init(text: text, images: images, model: sessionId != nil ? model : firstModel, sendId: sendID))).accepted
|
||||
case .task(let id):
|
||||
if team {
|
||||
if images != nil {
|
||||
@@ -472,10 +472,10 @@ final class ChatModel {
|
||||
return
|
||||
}
|
||||
if let sessionId {
|
||||
_ = try await api.sendMessage(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: model))).accepted
|
||||
_ = try await api.sendMessage(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: model, sendId: sendID))).accepted
|
||||
return
|
||||
}
|
||||
_ = try await api.createSession(path: .init(taskId: id ?? ""), body: .json(.init(prompt: text, model: firstModel, images: images))).created
|
||||
_ = try await api.createSession(path: .init(taskId: id ?? ""), body: .json(.init(prompt: text, model: firstModel, images: images, sendId: sendID))).created
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -519,16 +519,21 @@ struct ComposerView: View {
|
||||
let imgs = images.take(draftKey)
|
||||
text = ""
|
||||
let sent = drafts.startSend(draftKey, value, imgs.count)
|
||||
let cancel = chat.team ? {} : chat.builder.showPending(value, imgs)
|
||||
let sendID = UUID().uuidString
|
||||
let cancel = chat.team ? { true } : chat.builder.showPending(value, imgs, sendID)
|
||||
Task {
|
||||
do {
|
||||
let ids = try await uploadImages(store.api, imgs)
|
||||
try await chat.send(sessionId: m.live?.id, text: value, images: ids, model: m.selected != m.current ? m.selected : nil, firstModel: m.selected)
|
||||
try await chat.send(sessionId: m.live?.id, text: value, images: ids, model: m.selected != m.current ? m.selected : nil, firstModel: m.selected, sendID: sendID)
|
||||
modelChoices[m.modelKey] = nil
|
||||
pick = nil
|
||||
drafts.endSend(sent, true)
|
||||
} catch {
|
||||
cancel()
|
||||
guard cancel() else {
|
||||
log.notice("send failed after the server confirmed it: \(errorText(error), privacy: .public)")
|
||||
drafts.endSend(sent, true)
|
||||
return
|
||||
}
|
||||
drafts.endSend(sent, false)
|
||||
text = drafts.composerText(draftKey)
|
||||
images.restore(draftKey, imgs)
|
||||
|
||||
@@ -88,6 +88,7 @@ final class Store {
|
||||
@ObservationIgnored private var leadStream: EventStream?
|
||||
@ObservationIgnored private var stopped = false
|
||||
@ObservationIgnored var changesMemory = ChangesMemory()
|
||||
@ObservationIgnored private var localSends: [String: LocalSends] = [:]
|
||||
|
||||
init(api: API, images: ImageDrafts, forwards: Forwards) {
|
||||
self.api = api
|
||||
@@ -98,6 +99,22 @@ final class Store {
|
||||
remote = Remote(api: api)
|
||||
}
|
||||
|
||||
func sends(_ key: String) -> LocalSends {
|
||||
if let sends = localSends[key] {
|
||||
return sends
|
||||
}
|
||||
let sends = LocalSends()
|
||||
localSends[key] = sends
|
||||
return sends
|
||||
}
|
||||
|
||||
private func confirmSend(_ key: String, _ ev: ByreEvent) {
|
||||
guard ev.kind == "user.queued" || ev.kind == "user.message", let sendID = str(ev.payload.value, "send_id") else {
|
||||
return
|
||||
}
|
||||
localSends[key]?.items.removeAll { $0.sendID == sendID }
|
||||
}
|
||||
|
||||
func stop() async {
|
||||
stopped = true
|
||||
globalStream?.stop()
|
||||
@@ -569,6 +586,7 @@ final class Store {
|
||||
leadMore = page.more
|
||||
leadStream = EventStream(api: api, after: page.events.last?.seq ?? 0, lead: true, onEvent: { [weak self] ev in
|
||||
self?.leadFeed.push(ev)
|
||||
self?.confirmSend("lead", ev)
|
||||
}, onState: { _ in }, onReady: { [weak self] in
|
||||
self?.leadFeed.load(page.events)
|
||||
})
|
||||
@@ -603,6 +621,7 @@ final class Store {
|
||||
func handleEvent(_ ev: ByreEvent) {
|
||||
if let taskId = ev.taskId {
|
||||
taskFeeds[taskId]?.push(ev)
|
||||
confirmSend(taskId, ev)
|
||||
}
|
||||
if ev.seq == 0 {
|
||||
events.send(ev)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user