Let task agents suggest discarding their task, with reasoning

This commit is contained in:
Ian Gulliver
2026-10-19 17:14:13 -07:00
parent 0be09b3279
commit 0dcbfc57f5
7 changed files with 79 additions and 16 deletions
+13 -6
View File
@@ -310,6 +310,7 @@ final class ChatModel {
var questions: [Int] = []
var open: [Int] = []
var ready: [Int] = []
var discard = false
for row in builder.root.rows where !row.hidden {
switch row.kind {
case .question where row.question?.state == .open:
@@ -318,8 +319,9 @@ final class ChatModel {
if let r = row.review, r.id == latest?.id, findingOpen(r, i) {
open.append(row.id)
}
case .ready where row.id == builder.lastReady && active:
case .ready(_, _, let d) where row.id == builder.lastReady && active:
ready.append(row.id)
discard = d
default:
break
}
@@ -332,7 +334,9 @@ final class ChatModel {
groups.append(AttentionGroup(id: "findings", label: open.count == 1 ? "1 finding" : "\(open.count) findings", help: "Scroll to the next finding of the latest review that nobody has answered", rows: open))
}
if !ready.isEmpty {
groups.append(AttentionGroup(id: "ready", label: "ready to merge", help: "Scroll to the agent's ready-for-merge overview", rows: ready))
groups.append(discard
? AttentionGroup(id: "ready", label: "suggests discard", help: "Scroll to the agent's reasons to discard the task", rows: ready)
: AttentionGroup(id: "ready", label: "ready to merge", help: "Scroll to the agent's ready-for-merge overview", rows: ready))
}
return groups
}
@@ -1257,8 +1261,8 @@ struct RowView: View {
case .findingResponse(let snapshot, let index, let response, let reason):
FindingResponseView(row: row, model: model, snapshot: snapshot, index: index, response: response, reason: reason)
case .ready(let when, let blocks):
ReadyBubble(when: when, blocks: blocks, taskId: row.taskId, latest: row.id == model.builder.lastReady, unmerged: model.hasUnmerged)
case .ready(let when, let blocks, let discard):
ReadyBubble(when: when, blocks: blocks, discard: discard, taskId: row.taskId, latest: row.id == model.builder.lastReady, unmerged: model.hasUnmerged)
case .handoff:
DetailsBox(boxed: false) {
@@ -1388,6 +1392,7 @@ private struct FindingResponseView: View {
private struct ReadyBubble: View {
let when: Date
let blocks: [MDBlock]
let discard: Bool
let taskId: String?
let latest: Bool
let unmerged: Bool
@@ -1398,7 +1403,7 @@ private struct ReadyBubble: View {
let current = latest && task?.status == .active
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Text("ready for merge")
Text(discard ? "suggests discarding" : "ready for merge")
.font(Fonts.font(11, weight: current ? 700 : 400))
.foregroundStyle(current ? Palette.warn : Palette.muted)
WhenText(when: when)
@@ -1410,7 +1415,9 @@ private struct ReadyBubble: View {
let busy = store.taskBusy(task.id)
HStack(spacing: 8) {
Spacer(minLength: 0)
MergeButton(task: task, busy: busy, unmerged: unmerged, small: true)
if !discard {
MergeButton(task: task, busy: busy, unmerged: unmerged, small: true)
}
DiscardButton(task: task, busy: busy, small: true)
}
}
+6 -3
View File
@@ -53,7 +53,7 @@ enum RowKind {
case review(note: Note, md: [MDBlock])
case finding(index: Int)
case findingResponse(snapshot: Int, index: Int, response: String, reason: String)
case ready(when: Date, md: [MDBlock])
case ready(when: Date, md: [MDBlock], discard: Bool)
case handoff
case snapshot(Snapshot)
case question
@@ -575,7 +575,7 @@ final class TranscriptBuilder {
if ev.kind == "task.merge_after_review" && sink == nil {
lastMergeAfterReview = row.id
}
if ev.kind == "task.ready" && sink == nil {
if (ev.kind == "task.ready" || ev.kind == "task.discard_suggested") && sink == nil {
lastReady = row.id
}
if ev.kind == "inbox.created" && sink == nil {
@@ -796,7 +796,10 @@ final class TranscriptBuilder {
return note([.text("\(ev.actor) set the task summary")])
case "task.ready":
return Row(.ready(when: when, md: parseMarkdown(jsString(p["overview"]))), taskId: taskId)
return Row(.ready(when: when, md: parseMarkdown(jsString(p["overview"])), discard: false), taskId: taskId)
case "task.discard_suggested":
return Row(.ready(when: when, md: parseMarkdown(jsString(p["reason"])), discard: true), taskId: taskId)
case "task.discard_started":
return note([.text("\(ev.actor) asked to discard the task; the agent writes the task summary first")])