Start a new task from a closed task

This commit is contained in:
Ian Gulliver
2026-10-22 09:05:24 -07:00
parent 88c0199845
commit fedd4ad673
13 changed files with 589 additions and 365 deletions
+18
View File
@@ -134,7 +134,17 @@ final class ChatModel {
member != nil
}
var closed: Bool {
guard let taskId, let task = store.findTask(taskId) else {
return false
}
return task.status != .active
}
var placeholder: String {
if closed {
return "Continue in a new task, started from main, whose agent picks up this conversation. Paste or drop images to attach them."
}
if team {
return "Post to the team's board; every agent of the team gets it."
}
@@ -468,6 +478,14 @@ final class ChatModel {
case .lead:
_ = try await api.messageLead(body: .json(.init(text: text, images: images, model: sessionId != nil ? model : firstModel, sendId: sendID))).accepted
case .task(let id):
if closed {
guard let sessionId else {
throw ApiError(status: 0, message: "This task has no agent conversation to continue.")
}
let task = try await api.continueSession(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: team ? nil : model, sendId: sendID))).created.body.json
store.focusTask(task.id)
return
}
if team {
if images != nil {
throw ApiError(status: 0, message: "The board takes text only; drop the images to post.")
+6 -5
View File
@@ -482,7 +482,7 @@ struct ComposerView: View {
}
private func modelState() -> ModelState {
let live = chat.liveSession
let live = chat.closed ? chat.session : chat.liveSession
let current = live.map { $0.model.isEmpty ? chat.builder.context[$0.id]?.model ?? "" : $0.model } ?? ""
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor.rawValue } } ?? models
let options = !current.isEmpty && !offered.contains(current) ? [current] + offered : offered
@@ -632,12 +632,13 @@ struct ComposerView: View {
.buttonStyle(WebButtonStyle(danger: true))
.disabled(!running)
.fixedSize()
Button("Send") {
Button(chat.closed ? "Continue" : "Send") {
submit(modelState())
}
.buttonStyle(WebButtonStyle())
.disabled(!shown)
.fixedSize()
.help(chat.closed ? "Start a new task from main whose agent continues this conversation with your message" : "Send the message")
}
}
@@ -668,7 +669,7 @@ struct ComposerView: View {
text = ""
let sent = drafts.startSend(draftKey, value, imgs.count)
let sendID = newID("snd")
let cancel = chat.team ? { true } : chat.builder.showPending(value, imgs, sendID)
let cancel = chat.team || chat.closed ? { true } : chat.builder.showPending(value, imgs, sendID)
let sessionId = m.live?.id
Task {
var ids: [String]? = nil
@@ -686,7 +687,7 @@ struct ComposerView: View {
return
} catch {
let refused = (rootError(error) as? ApiError).map { $0.status < 500 } ?? false
if chat.team || refused {
if (chat.team && !chat.closed) || refused {
guard cancel() else {
log.notice("send failed after the server confirmed it: \(errorText(error), privacy: .public)")
drafts.endSend(sent, true)
@@ -698,7 +699,7 @@ struct ComposerView: View {
overlays.showAlert(errorText(error))
return
}
guard chat.builder.unconfirmed(sendID) else {
guard chat.closed || chat.builder.unconfirmed(sendID) else {
drafts.endSend(sent, true)
return
}
+13 -2
View File
@@ -536,11 +536,16 @@ final class TranscriptBuilder {
continue
}
if ev.kind == "task.forked", let fork = str(p, "fork") {
if let row = ctx.snapshots[snapKey(int(p, "forked_at") ?? 0)] {
let at = int(p, "forked_at") ?? 0
if at == 0 {
put(Row(.note(Note(parts: [.text("\(ev.actor) continued this task as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
continue
}
if let row = ctx.snapshots[snapKey(at)] {
row.forks.append(fork)
continue
}
put(Row(.note(Note(parts: [.text("\(ev.actor) forked snapshot #\(jsString(p["forked_at"])) as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
put(Row(.note(Note(parts: [.text("\(ev.actor) forked snapshot #\(at) as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
continue
}
if ev.kind == "review.started" || ev.kind == "review.finished" || ev.kind == "review.finding" {
@@ -913,6 +918,12 @@ final class TranscriptBuilder {
return note([.text("\(jsString(p["error"])) (\(jsString(p["vendor"])) \(limit["rateLimitType"].map { jsString($0) } ?? "") limit resets \(resets))")], .error)
case "task.created":
if let from = str(p, "forked_from"), p["forked_at"] == nil {
if let task = rowTask, task != taskId {
return note([.text("\(ev.actor) continued "), .task(from), .text(" as "), .task(task), .text(" here")])
}
return note([.text("\(ev.actor) continued "), .task(from), .text(" here in a new task, started from main; everything above happened in that task, and its agent continues that conversation")])
}
if let from = str(p, "forked_from"), let task = rowTask, task != taskId {
return note([.text("\(ev.actor) forked "), .task(task), .text(" here from "), .task(from), .text(" at snapshot #\(jsString(p["forked_at"]))")])
}