Files
byre/mac/Sources/Byre/TranscriptBuilder.swift
T

1068 lines
38 KiB
Swift

import AppKit
import Foundation
import Observation
import OpenAPIRuntime
let quietReply = "NO_REPLY"
struct Usage {
var used: Int
var window: Int
var model: String
}
struct Call {
let name: String
let target: String
}
enum NotePart {
case text(String)
case task(String)
case preview(taskId: String, port: Int)
}
enum NoteStyle {
case plain, error, ok
}
struct Note {
var parts: [NotePart]
var when: Date?
var style: NoteStyle = .plain
var cancelMergeAfterReview: String?
}
enum SubagentState {
case running, done, error
}
enum RowKind {
case user(actor: String, when: Date, images: [String], md: [MDBlock]?)
case byrePrompt(brief: String, when: Date, md: [MDBlock])
case agent(md: [MDBlock])
case subagentMessage(when: Date, md: [MDBlock])
case stream
case thinking(md: [MDBlock])
case tool(name: String, brief: String, input: String)
case subagent(title: String, description: String, prompt: [MDBlock])
case reviewer(model: String)
case group
case orphan(String)
case turnEnd(error: String?)
case note(Note)
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], discard: Bool, keepOpen: Bool)
case handoff
case snapshot(Snapshot)
case question
}
@MainActor
private var rowIds = 0
@MainActor
@Observable
final class RowList {
var rows: [Row] = []
}
@MainActor
@Observable
final class Row: Identifiable {
let id: Int
let kind: RowKind
let taskId: String?
var hidden = false
var tag = false
var result: String?
var unfinished = false
var started: Date?
var status = "starting…"
var statusState = SubagentState.running
var active = false
weak var owner: Row?
var calls: [Call] = []
var summary: Note?
var review: Review?
var reviewOf: Int?
var forks: [String] = []
var question: InboxItem?
var text = ""
var md: [MDBlock] = []
var summarySeq: Int64?
let body: RowList?
var pending: Bool {
switch kind {
case .tool, .subagent:
return result == nil && !unfinished
case .group:
return body!.rows.contains { $0.pending }
default:
return false
}
}
var holdsActive: Bool {
if case .group = kind {
return body!.rows.contains { $0.active }
}
return active
}
var inActive: Bool {
var o = owner
while let r = o {
if r.active {
return true
}
o = r.owner
}
return false
}
var inReview: Bool {
var o = owner
while let r = o {
if case .reviewer = r.kind {
return r.active
}
o = r.owner
}
return false
}
func within(_ ancestor: Row) -> Bool {
var o = owner
while let r = o {
if r === ancestor {
return true
}
o = r.owner
}
return false
}
init(_ kind: RowKind, taskId: String?, body: Bool = false) {
rowIds += 1
id = rowIds
self.kind = kind
self.taskId = taskId
self.body = body ? RowList() : nil
}
}
struct PendingSend: Identifiable {
let id: Int
let text: String
let images: [PendingImage]
let imageIds: [String]
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
let container: RowList
var group: Row?
var calls: [Call]
let parent: String
init(first: Row, container: RowList, calls: [Call], parent: String) {
self.first = first
self.container = container
self.calls = calls
self.parent = parent
}
}
@MainActor
private final class Subagent {
let row: Row
var run: Run?
init(row: Row) {
self.row = row
}
}
@MainActor
private final class Ctx {
var tools: [String: Row] = [:]
var subagents: [String: Subagent] = [:]
var reviewers: [String: Row] = [:]
var snapshots: [String: Row] = [:]
var reviews: [String: Review] = [:]
var reviewRows: [String: [Row]] = [:]
var questions: [String: Row] = [:]
var run: Run?
var held: RowList?
var open = false
var drawn: [Row]?
var quiet = false
var handoff: Row?
var reply: Row?
}
@MainActor
@Observable
final class TranscriptBuilder {
let root = RowList()
private(set) var queued: [PendingSend] = []
private(set) var context: [String: Usage] = [:]
private(set) var open = false
private(set) var lastMergeAfterReview: Int?
private(set) var lastReady: Int?
@ObservationIgnored private var ctx = Ctx()
@ObservationIgnored private var sink: RowList?
@ObservationIgnored private var stream: Row?
@ObservationIgnored private var streamTimer: Task<Void, Never>?
@ObservationIgnored private let taskId: String?
@ObservationIgnored private var rowTask: String?
@ObservationIgnored private let worktree: () -> String
@ObservationIgnored private let running: () -> Bool
@ObservationIgnored private var pendingIds = 0
@ObservationIgnored private let local: LocalSends
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()
}
func append(_ events: [ByreEvent]) -> Bool {
let changed = render(events)
open = ctx.open
return changed
}
func prepend(_ events: [ByreEvent]) {
let live = ctx
let box = RowList()
ctx = Ctx()
sink = box
_ = render(events)
sink = nil
ctx = live
root.rows.insert(contentsOf: box.rows, at: 0)
}
// 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 { [local] in
guard let i = local.items.firstIndex(where: { $0.id == entry.id }) else {
return false
}
local.items.remove(at: i)
return true
}
}
func unconfirmed(_ sendID: String) -> Bool {
local.items.contains { $0.sendID == sendID }
}
func withdrawn(_ inputId: Int) {
queued.removeAll { $0.inputId == inputId }
}
private func release() {
guard let buffered = ctx.held else {
return
}
ctx.held = nil
ctx.open = true
(sink ?? root).rows.append(contentsOf: buffered.rows)
}
@discardableResult
private func put(_ row: Row, keepHeld: Bool = false) -> RowList {
if let handoff = ctx.handoff {
handoff.body!.rows.append(row)
return handoff.body!
}
if !keepHeld {
release()
}
ctx.drawn?.append(row)
if let held = ctx.held {
held.rows.append(row)
return held
}
let list = sink ?? root
list.rows.append(row)
return list
}
private func fold() {
let drawn = ctx.drawn ?? []
(sink ?? root).rows.removeAll { r in drawn.contains { $0 === r } }
ctx.drawn = nil
ctx.held = nil
ctx.open = false
ctx.quiet = false
}
private func subagentTag(_ parent: String) -> Bool {
!parent.isEmpty && ctx.subagents[parent] == nil
}
private func addToRun(_ row: Row, _ call: Call?, _ parent: String) {
if let sub = ctx.subagents[parent] {
sub.run = extendRun(sub.run, row, call, parent) { r in
sub.row.body!.rows.append(r)
return sub.row.body!
}
return
}
ctx.run = extendRun(ctx.run, row, call, parent) { r in
self.put(r, keepHeld: true)
}
}
private func extendRun(_ run: Run?, _ row: Row, _ call: Call?, _ parent: String, _ place: (Row) -> RowList) -> Run {
guard let run, run.parent == parent else {
let list = place(row)
return Run(first: row, container: list, calls: call.map { [$0] } ?? [], parent: parent)
}
if let call {
run.calls.append(call)
}
if run.group == nil {
let group = Row(.group, taskId: rowTask, body: true)
group.owner = ctx.subagents[parent]?.row
for list in [run.container, sink ?? root] {
if let i = list.rows.firstIndex(where: { $0 === run.first }) {
list.rows[i] = group
break
}
}
ctx.drawn?.append(group)
group.body!.rows.append(run.first)
run.group = group
}
run.group!.body!.rows.append(row)
run.group!.calls = run.calls
run.group!.tag = subagentTag(run.parent)
return run
}
private func dropStream() {
streamTimer?.cancel()
streamTimer = nil
guard let stream else {
return
}
root.rows.removeAll { $0 === stream }
self.stream = nil
}
func appendDelta(_ ev: ByreEvent) {
if ctx.handoff != nil {
return
}
if let sid = ev.agentSessionId, ctx.reviewers[sid] != nil {
return
}
let row: Row
if let stream {
row = stream
} else {
row = Row(.stream, taskId: rowTask)
row.hidden = true
stream = row
ctx.run = nil
put(row, keepHeld: true)
}
row.text += str(ev.payload.value, "text") ?? ""
if streamTimer != nil {
return
}
streamTimer = Task { [weak self] in
try? await Task.sleep(for: .milliseconds(100))
guard let self, !Task.isCancelled, let stream = self.stream else {
return
}
self.streamTimer = nil
stream.hidden = self.ctx.held != nil || quietReply.hasPrefix(trimmed(stream.text))
stream.md = parseMarkdown(stream.text)
}
}
private func openHandoff() {
let row = Row(.handoff, taskId: rowTask, body: true)
row.summary = Note(parts: [.text("writing a handoff…")])
ctx.run = nil
put(row)
ctx.handoff = row
}
private func closeHandoff(_ end: Row?) {
guard let handoff = ctx.handoff else {
return
}
ctx.handoff = nil
if let end, case .note(let note) = end.kind {
handoff.summary = note
} else {
handoff.summary = Note(parts: [.text("handoff not written")])
}
}
private func render(_ events: [ByreEvent]) -> Bool {
var contextChanged = false
defer {
rowTask = taskId
}
for ev in events {
let p = ev.payload.value
let parent = parentOf(p)
rowTask = ev.taskId ?? taskId
let own = rowTask == taskId
if ev.kind == "session.created" && str(p, "role") == "reviewer", let sid = ev.agentSessionId, !sid.isEmpty {
let row = Row(.reviewer(model: str(p, "model") ?? ""), taskId: rowTask, body: true)
row.status = "running"
row.active = true
ctx.reviewers[sid] = row
ctx.subagents[sid] = Subagent(row: row)
ctx.run = nil
put(row)
continue
}
if let sid = ev.agentSessionId, let reviewer = ctx.reviewers[sid] {
reviewerEvent(ev, reviewer, ctx.subagents[parent] == nil ? sid : parent)
continue
}
if ev.kind == "agent.vendor" {
claudeTaskStatus(p)
continue
}
if ev.kind == "session.subagents" {
if int(p, "subagents") == 0 {
for sub in ctx.subagents.values where sub.row.active && !ctx.reviewers.values.contains(where: { $0 === sub.row }) {
endSubagent(sub.row, .error, "ended with its process")
}
}
continue
}
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", let sendID = str(p, "send_id") {
queued.removeAll { $0.sendID == sendID }
}
if sink == nil && own && (ev.kind == "task.merged" || ev.kind == "task.discarded") {
queued.removeAll()
}
if ev.kind == "user.queued" {
if sink == nil && own {
let text = str(p, "text") ?? ""
pendingIds += 1
let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) }
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
}
if ev.kind == "user.unqueued" {
if sink == nil && own, let id = int(p, "id") {
withdrawn(id)
}
continue
}
if ev.kind == "user.message" && bool(p, "bounce") {
if let reply = ctx.reply {
(sink ?? root).rows.removeAll { $0 === reply }
ctx.held?.rows.removeAll { $0 === reply }
}
continue
}
if ev.kind == "session.context" && sink != nil {
continue
}
if ev.kind == "session.context", let sid = ev.agentSessionId, !sid.isEmpty {
let prev = context[sid]
let window = int(p, "window").flatMap { $0 == 0 ? nil : $0 } ?? prev?.window ?? 0
context[sid] = Usage(used: int(p, "used") ?? 0, window: window, model: str(p, "model") ?? "")
contextChanged = true
continue
}
if ev.kind == "snapshot.created" {
ctx.run = nil
let snap = try! decodePayload(ev.payload, as: Snapshot.self)
let row = Row(.snapshot(snap), taskId: rowTask)
ctx.snapshots[snapKey(snap.n)] = row
if ![.answer, .fork, .askHuman].contains(snap.trigger) && sink == nil && own {
lastReady = nil
}
put(row)
continue
}
if ev.kind == "task.forked", let fork = str(p, "fork") {
if let row = ctx.snapshots[snapKey(int(p, "forked_at") ?? 0)] {
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))
continue
}
if ev.kind == "review.started" || ev.kind == "review.finished" || ev.kind == "review.finding" {
let r = try! decodePayload(ev.payload, as: Review.self)
ctx.reviews[reviewKey(r.snapshotN, r.stage)] = r
for row in ctx.reviewRows[r.id] ?? [] {
row.review = r
}
if let reviewer = ctx.reviewers[r.agentSessionId] {
reviewerState(reviewer, r)
}
if ev.kind == "review.finished" {
let rows = renderReview(r, ev.createdAt)
if !rows.isEmpty {
ctx.run = nil
}
for row in rows {
put(row)
}
continue
}
}
if ev.kind == "task.ready_cleared" {
if sink == nil && own {
lastReady = nil
}
continue
}
if ev.kind == "inbox.updated" {
let item = try! decodePayload(ev.payload, as: InboxItem.self)
ctx.questions[item.id]?.question = item
continue
}
let sub = ctx.subagents[parent]
if ev.kind == "agent.tool_call" && isSubagentCall(p) {
let row = renderSubagent(p, parent)
row.started = ev.createdAt
if let sub {
sub.run = nil
sub.row.body!.rows.append(row)
continue
}
addToRun(row, Call(name: jsString(p["name"]), target: ""), parent)
continue
}
if ev.kind == "agent.tool_call" && sub == nil && str(p, "name") == "mcp__byre__respond_to_finding" {
let input = dict(p, "input") ?? [:]
let n = int(input, "snapshot") ?? 0
let row = Row(.findingResponse(snapshot: n, index: (int(input, "finding") ?? 0) - 1, response: str(input, "response") ?? "", reason: str(input, "reason") ?? ""), taskId: rowTask)
row.started = ev.createdAt
ctx.tools[jsString(p["id"])] = row
if let r = ctx.reviews[reviewKey(n, str(input, "reviewer") ?? "")], r.state == .done {
row.review = r
ctx.reviewRows[r.id, default: []].append(row)
}
ctx.run = nil
put(row, keepHeld: true)
continue
}
if ev.kind == "agent.tool_call" {
let name = toolLabel(jsString(p["name"]))
let row = renderToolCall(p, parent)
row.started = ev.createdAt
if name == "ToolSearch" {
continue
}
addToRun(row, Call(name: name, target: toolTarget(p["input"] ?? nil, worktree())), parent)
continue
}
guard let row = renderEvent(ev, parent) else {
continue
}
if let sub {
sub.run = nil
sub.row.body!.rows.append(row)
continue
}
let prompt = if case .byrePrompt = row.kind { true } else { false }
let agentNote = if case .note = row.kind, ev.actor == "agent" { true } else { false }
if !prompt && !agentNote {
ctx.run = nil
}
if ev.kind == "agent.message" && parent.isEmpty {
ctx.drawn = nil
ctx.reply = row
}
if ev.kind == "turn.ended" {
ctx.open = false
}
if ev.kind == "turn.ended" && bool(p, "handoff") && ctx.handoff != nil {
closeHandoff(row)
continue
}
if ev.kind == "turn.ended" || (ev.kind == "user.message" && !bool(p, "mid_turn")) {
closeHandoff(nil)
}
if ev.kind == "user.message" && bool(p, "handoff") {
openHandoff()
}
if ev.kind == "turn.ended" && ctx.quiet && ctx.drawn != nil {
fold()
continue
}
if ev.kind == "turn.ended" || ev.kind == "user.message" {
ctx.drawn = nil
}
if ev.kind == "user.message" && ev.actor == "byre" && !(ev.owner ?? "").isEmpty && !bool(p, "mid_turn") && !bool(p, "handoff") {
ctx.run = nil
ctx.held = RowList()
ctx.open = false
ctx.drawn = []
continue
}
if ev.kind == "user.message" && !bool(p, "mid_turn") {
ctx.open = true
}
if ev.kind == "turn.ended" && bool(p, "summary") && taskId != nil && own {
row.summarySeq = ev.seq
}
if ev.kind == "task.merge_after_review" && sink == nil && own {
lastMergeAfterReview = row.id
}
if (ev.kind == "task.ready" || ev.kind == "task.discard_suggested") && sink == nil && own {
lastReady = row.id
}
if ev.kind == "inbox.created" && sink == nil && own {
lastReady = nil
}
if prompt && !row.hidden || agentNote {
release()
addToRun(row, agentNote ? nil : Call(name: "byre", target: ""), parent)
continue
}
put(row, keepHeld: ev.kind == "agent.thinking")
}
return contextChanged
}
private func snapKey(_ n: Int) -> String {
"\(rowTask ?? "")/\(n)"
}
private func reviewKey(_ n: Int, _ stage: String) -> String {
"\(snapKey(n))/\(stage)"
}
private func claudeTaskStatus(_ p: JSONDict) {
guard str(p, "type") == "system", let sub = ctx.subagents[jsString(p["tool_use_id"])] else {
return
}
switch str(p, "subtype") {
case "task_started":
sub.row.active = true
sub.row.status = bool(p, "is_backgrounded") ? "running in the background" : "running"
case "task_progress":
sub.row.status = str(p, "description") ?? "running"
case "task_notification":
endSubagent(sub.row, str(p, "status") == "completed" ? .done : .error, jsString(p["status"]))
default:
break
}
}
private func endSubagent(_ row: Row, _ state: SubagentState, _ status: String) {
row.active = false
row.statusState = state
row.status = status
for tool in ctx.tools.values where tool.result == nil && tool.within(row) {
tool.unfinished = true
}
}
private func endTools() {
for row in ctx.tools.values where row.result == nil && !(row.active || row.inActive) {
row.unfinished = true
}
}
private func renderToolCall(_ p: JSONDict, _ parent: String) -> Row {
let row = Row(.tool(name: toolLabel(jsString(p["name"])), brief: toolBrief(p["input"] ?? nil, worktree()), input: prettyJSON(p["input"] ?? nil)), taskId: rowTask)
row.tag = subagentTag(parent)
row.owner = ctx.subagents[parent]?.row
ctx.tools[jsString(p["id"])] = row
return row
}
private func renderSubagent(_ p: JSONDict, _ parent: String) -> Row {
let input = dict(p, "input") ?? [:]
let id = jsString(p["id"])
let type = str(input, "subagent_type") ?? "subagent"
let row = Row(.subagent(title: "\(jsString(p["name"])) \(type)", description: str(input, "description") ?? "", prompt: parseMarkdown(str(input, "prompt") ?? "")), taskId: rowTask, body: true)
row.tag = subagentTag(parent)
row.owner = ctx.subagents[parent]?.row
ctx.tools[id] = row
ctx.subagents[id] = Subagent(row: row)
return row
}
private func reviewerEvent(_ ev: ByreEvent, _ reviewer: Row, _ parent: String) {
let p = ev.payload.value
let sub = ctx.subagents[parent]!
switch ev.kind {
case "agent.vendor":
claudeTaskStatus(p)
case "user.message":
addToRun(byrePrompt(str(p, "text") ?? "", ev.createdAt), Call(name: "byre", target: ""), parent)
case "agent.tool_call":
let name = toolLabel(jsString(p["name"]))
reviewer.text = "\(name) \(toolBrief(p["input"] ?? nil, worktree()))"
if isSubagentCall(p) {
let row = renderSubagent(p, parent)
row.started = ev.createdAt
if sub.row !== reviewer {
sub.run = nil
sub.row.body!.rows.append(row)
return
}
addToRun(row, Call(name: jsString(p["name"]), target: ""), parent)
return
}
let row = renderToolCall(p, parent)
row.started = ev.createdAt
if name != "ToolSearch" {
addToRun(row, Call(name: name, target: toolTarget(p["input"] ?? nil, worktree())), parent)
}
case "turn.ended":
for tool in ctx.tools.values where tool.result == nil && tool.within(reviewer) {
tool.unfinished = true
}
case "agent.message", "agent.thinking", "agent.tool_result", "session.state", "session.error", "session.rate_limited":
if ev.kind == "agent.message" && parent == ev.agentSessionId {
reviewer.text = trimmed(jsString(p["text"])).components(separatedBy: "\n").first ?? ""
}
guard let row = renderEvent(ev, parent) else {
return
}
sub.run = nil
sub.row.body!.rows.append(row)
default:
break
}
}
private func reviewerState(_ row: Row, _ r: Review) {
row.review = r
row.reviewOf = r.snapshotN
switch r.state {
case .done:
endSubagent(row, .done, reviewCounts(r))
row.text = ""
case .failed:
let superseded = r.error.hasPrefix("superseded")
endSubagent(row, superseded ? .done : .error, superseded ? "superseded" : "failed")
row.text = r.error
default:
break
}
}
private func renderReview(_ r: Review, _ when: Date) -> [Row] {
if r.state == .failed && r.agentSessionId.isEmpty {
return [Row(.note(Note(parts: [.text("\(r.stage) review of snapshot #\(r.snapshotN) failed: \(r.error)")], when: when, style: .error)), taskId: rowTask)]
}
let fs = findings(r)
if fs.isEmpty {
return []
}
let parts: [NotePart] = [.text("\(r.stage) review of snapshot #\(r.snapshotN): \(reviewCounts(r))")]
let head = Row(.review(note: Note(parts: parts, when: when), md: parseMarkdown(str(r.result?.value ?? [:], "summary") ?? "")), taskId: rowTask)
let rows = [head] + fs.indices.map { Row(.finding(index: $0), taskId: rowTask) }
for row in rows {
row.review = r
row.reviewOf = r.snapshotN
}
ctx.reviewRows[r.id, default: []] += rows
return rows
}
private func byrePrompt(_ text: String, _ when: Date) -> Row {
let first = text.components(separatedBy: "\n").first ?? ""
let brief = first.count > 120 ? "\(first.prefix(120))…" : first
return Row(.byrePrompt(brief: brief, when: when, md: parseMarkdown(text)), taskId: rowTask)
}
private func renderEvent(_ ev: ByreEvent, _ parent: String) -> Row? {
let p = ev.payload.value
let when = ev.createdAt
func note(_ parts: [NotePart], _ style: NoteStyle = .plain, time: Bool = true) -> Row {
Row(.note(Note(parts: parts, when: time ? when : nil, style: style)), taskId: rowTask)
}
switch ev.kind {
case "user.message":
ctx.quiet = false
if !bool(p, "mid_turn") {
endTools()
}
let text = str(p, "text") ?? ""
if ev.actor == "byre" {
let row = byrePrompt(text, when)
row.hidden = bool(p, "continuation") || bool(p, "findings")
return row
}
let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) }
return Row(.user(actor: ev.actor, when: when, images: images, md: trimmed(text).isEmpty ? nil : parseMarkdown(text)), taskId: rowTask)
case "agent.message":
let text = jsString(p["text"])
if !parent.isEmpty {
let row = Row(.subagentMessage(when: when, md: parseMarkdown(text)), taskId: rowTask)
row.tag = subagentTag(parent)
return row
}
if trimmed(trimmed(text).components(separatedBy: "\n").last ?? "") == quietReply {
ctx.quiet = true
return nil
}
ctx.quiet = false
return Row(.agent(md: parseMarkdown(text)), taskId: rowTask)
case "agent.thinking":
let row = Row(.thinking(md: parseMarkdown(jsString(p["text"]))), taskId: rowTask)
row.tag = subagentTag(parent)
return row
case "agent.tool_result":
let body = resultText(p["content"] ?? nil)
guard let target = ctx.tools[jsString(p["tool_use_id"])] else {
return Row(.orphan(body), taskId: rowTask)
}
target.result = body
if case .findingResponse = target.kind, bool(p, "is_error") {
target.hidden = true
}
return nil
case "turn.ended":
endTools()
if bool(p, "handoff") {
return note([.text("handoff written; continuing in a new session with a fresh context")])
}
return Row(.turnEnd(error: bool(p, "is_error") ? "turn ended: \(jsString(p["subtype"]).replacingOccurrences(of: "_", with: " "))" : nil), taskId: rowTask)
case "session.created":
let model = str(p, "model") ?? ""
return note([.text("\(ev.actor) started a \(jsString(p["vendor"])) session\(model.isEmpty ? "" : " (\(model))")")])
case "session.model":
return note([.text("\(ev.actor) switched the model to \(jsString(p["model"]))")])
case "session.state":
guard let error = str(p, "error"), !error.isEmpty else {
return nil
}
return note([.text("session \(jsString(p["state"])): \(error)")], .error, time: false)
case "inbox.created":
let row = Row(.question, taskId: rowTask)
let item = try! decodePayload(ev.payload, as: InboxItem.self)
row.question = item
ctx.questions[item.id] = row
return row
case "session.error":
return note([.text(jsString(p["error"]))], .error)
case "session.rate_limited":
let limit = dict(p, "rate_limit") ?? [:]
let resets = (limit["resetsAt"] as? Double ?? (limit["resetsAt"] as? Int).map(Double.init)).map { Date(timeIntervalSince1970: $0).formatted(date: .numeric, time: .standard) } ?? "unknown"
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"), 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"]))")])
}
if let from = str(p, "forked_from") {
return note([.text("\(ev.actor) forked this task here from "), .task(from), .text(" at snapshot #\(jsString(p["forked_at"])); everything above happened in that task, and its agent continues that conversation")])
}
if let parentTask = str(p, "parent_task_id") {
return note([.text("subtask of "), .task(parentTask), .text(", created by its agent")])
}
return nil
case "task.parent_merge_started":
return note([.text("\(ev.actor) asked "), .task(jsString(p["parent_task_id"])), .text("'s agent to merge this subtask into its branch")])
case "task.parent_merge_stopped":
return note([.text("the merge into the parent task didn't start")], .error)
case "task.title":
return note([.text("\(ev.actor) set the task title to \"\(jsString(p["title"]))\"")])
case "task.merge_title":
return note([.text("\(ev.actor) set the merge commit title to \"\(jsString(p["merge_title"]))\"")])
case "task.merge_queued":
return note([.text("\(ev.actor) asked to merge; held until the merge in progress in this project is done")])
case "task.merge_stopped":
return note([.text("\(ev.actor) stopped the merge for review: \(jsString(p["reason"]))")])
case "task.merge_after_review":
var n = Note(parts: [.text("\(ev.actor) asked to merge once the review of snapshot #\(jsString(p["snapshot_n"])) finishes with no findings")], when: when)
n.cancelMergeAfterReview = jsString(p["id"])
return Row(.note(n), taskId: rowTask)
case "task.merge_after_review_cancelled":
return note([.text("\(ev.actor) cancelled the merge after review")])
case "task.merge_after_review_dropped":
return note([.text("didn't merge after the review: \(jsString(p["reason"]))")])
case "preview.exposed":
return note([.text("\(ev.actor) exposed \"\(jsString(p["name"]))\" at "), .preview(taskId: ev.taskId ?? "", port: int(p, "port") ?? 0)])
case "task.merged":
let commit = str(p, "commit") ?? ""
if let parentTask = str(p, "parent_task_id") {
return note([.text("✓ \(ev.actor) merged the task into "), .task(parentTask), .text(", now at \(commit.prefix(7))")], .ok)
}
let pushed = str(p, "pushed") ?? ""
return note([.text("✓ \(ev.actor) merged the task\(commit.isEmpty ? "" : " as \(commit.prefix(7))")\(pushed.isEmpty ? "" : " and pushed it to \(pushed)")")], .ok)
case "task.merged_to_main":
return note([.text("\(ev.actor) merged the work so far as \(jsString(p["commit"]).prefix(7)), \"\(jsString(p["merge_title"]))\", pushed it to \(jsString(p["pushed"])) and kept the task open")])
case "task.summary":
return note([.text("\(ev.actor) set the task summary")])
case "task.ready":
return Row(.ready(when: when, md: parseMarkdown(jsString(p["overview"])), discard: false, keepOpen: str(p, "merge") == "continue"), taskId: rowTask)
case "task.discard_suggested":
return Row(.ready(when: when, md: parseMarkdown(jsString(p["reason"])), discard: true, keepOpen: false), taskId: rowTask)
case "task.discard_started":
return note([.text("\(ev.actor) asked to discard the task; the agent writes the task summary first")])
case "task.discarded":
return note([.text("\(ev.actor) discarded the task")])
default:
return nil
}
}
}
private let severities = ["critical", "high", "medium", "low", "info"]
func isReviewEvent(_ ev: ByreEvent) -> Bool {
ev.kind.hasPrefix("review.")
}
func findings(_ r: Review) -> [JSONDict] {
(r.result?.value["findings"] as? [(any Sendable)?] ?? []).map { $0 as? JSONDict ?? [:] }
}
func findingCount(_ r: Review) -> Int {
r.state == .done ? findings(r).indices.filter { findingOpen(r, $0) }.count : 0
}
func latestReviewedSnapshot(_ events: ArraySlice<ByreEvent>) -> Int? {
var best: Int?
for ev in events where isReviewEvent(ev) {
let r = try! decodePayload(ev.payload, as: Review.self)
best = max(best ?? r.snapshotN, r.snapshotN)
}
return best
}
func reviewCounts(_ r: Review) -> String {
severityCounts(findings(r))
}
func severityCounts(_ fs: [JSONDict]) -> String {
let parts = severities.map { s in (s, fs.filter { str($0, "severity") == s }.count) }.filter { $0.1 > 0 }
return parts.isEmpty ? "no findings" : parts.map { "\($0.1) \($0.0)" }.joined(separator: ", ")
}
private func parentOf(_ p: JSONDict) -> String {
str(p, "parent_tool_use_id") ?? ""
}
private func isSubagentCall(_ p: JSONDict) -> Bool {
let name = str(p, "name")
return name == "Agent" || name == "Task"
}
func toolLabel(_ name: String) -> String {
guard name.hasPrefix("mcp__"), let sep = name.dropFirst(5).range(of: "__") else {
return name
}
return name[sep.upperBound...].split(separator: "_", omittingEmptySubsequences: false).map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined(separator: " ")
}
private func toolBrief(_ input: (any Sendable)?, _ root: String) -> String {
guard let i = input as? JSONDict else {
return ""
}
let brief = ["command", "file_path", "pattern", "path", "url", "question", "description"].lazy.compactMap { i[$0] ?? nil }.first
let text = relative(brief.map { jsString($0) } ?? "", root)
return text.count > 120 ? "\(text.prefix(120))…" : text
}
private func toolTarget(_ input: (any Sendable)?, _ root: String) -> String {
let i = input as? JSONDict
guard let path = (i?["file_path"] ?? i?["path"]) as? String else {
return ""
}
return relative(path, root)
}
private func relative(_ path: String, _ root: String) -> String {
guard !root.isEmpty, let r = path.range(of: "\(root)/", options: [.anchored, .caseInsensitive]) else {
return path
}
return String(path[r.upperBound...])
}
private func resultText(_ content: (any Sendable)?) -> String {
if let s = content as? String {
return s
}
if let list = content as? [(any Sendable)?] {
return list.map { c in
if let d = c as? JSONDict, let text = d["text"] {
return jsString(text)
}
return compactJSON(c)
}.joined(separator: "\n")
}
return prettyJSON(content)
}