Give each workflow run a parent task with no agent

This commit is contained in:
Ian Gulliver
2026-10-21 09:02:09 -07:00
parent d062d7ccae
commit a130b9556d
23 changed files with 661 additions and 474 deletions
+1 -1
View File
@@ -661,7 +661,7 @@ private struct MergeButton: View {
? "Waiting for the merge in progress in this project to finish"
: task.mergeAfterReview
? "Merges when the running review finishes with no findings; open to merge now instead (⌘S)"
: task.parentTaskId != nil
: task.parentTaskId != nil && task.workflowStage == nil
? "Merge this subtask into its parent task's branch, or into \(target) (⌘S)"
: "The agent merges \(target) in, then byre squashes into one commit on \(target) and pushes it to origin; held while another task in the project is merging (⌘S)"
Button(intoParent ? "Merging into parent" : held ? "Merge held" : task.mergeAfterReview ? "Merge after review" : keepOpen == false ? "Merge & complete" : "Merge") {
+1 -1
View File
@@ -91,7 +91,7 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp
return
}
let target = store.projects.first { $0.id == task.projectId }?.defaultBranch ?? "the default branch"
let parent = task.parentTaskId.flatMap { store.findTask($0) }
let parent = task.workflowStage == nil ? task.parentTaskId.flatMap { store.findTask($0) } : nil
let coAuthors: [Participant]
do {
let client = store.api.client
+7 -4
View File
@@ -195,7 +195,7 @@ private struct TaskPane: View {
@ViewBuilder
private var tabBody: some View {
switch store.focus.tab {
switch store.shownTab {
case .transcript:
ChatView(kind: .task(store.selectedTaskId))
.id("\(store.selectedTaskId ?? "")/\(store.selectedTaskId.flatMap { store.pickedMember($0) } ?? "")")
@@ -342,16 +342,19 @@ private struct TabStrip: View {
}
private func info() -> Info {
let tab = store.focus.tab
let workflow = store.selectedTaskId.flatMap { store.findTask($0) }?.workflowRun != nil
let tab = store.shownTab
let run = store.selectedTaskId.flatMap { store.findTask($0) }?.workflow != nil
let team = store.selectedTaskId.map { !store.team($0).isEmpty } ?? false
let order = taskTabs.filter { t in
if run {
return t == .workflow || t == .project
}
if t == tab {
return true
}
switch t {
case .board: return team
case .workflow: return workflow
case .workflow: return false
default: return true
}
}
+34 -17
View File
@@ -84,7 +84,7 @@ struct ProjectView: View {
return
}
let active = (store.tasks[p.id] ?? []).filter { $0.status == .active }
let rows = nested(store, active.filter { !$0.background }).map(\.task.id) + active.filter(\.background).map(\.id)
let rows = nested(store, active).map(\.task.id)
if rows.isEmpty {
return
}
@@ -200,11 +200,7 @@ private struct ProjectBlock: View {
.padding(.horizontal, 10)
.padding(.vertical, 6)
}
TaskRows(active: active.filter { !$0.background })
ForEach(active.filter(\.background), id: \.id) { task in
BackgroundRow(task: task)
.id(task.id)
}
TaskRows(active: active)
heading("TODO")
TodoBlock(project: project, todo: todo, active: active)
}
@@ -229,12 +225,17 @@ private func nested(_ store: Store, _ active: [ByreTask]) -> [(task: ByreTask, d
add(child, depth + 1)
}
}
for task in store.byRank(active.filter { $0.parentTaskId == nil || !ids.contains($0.parentTaskId!) }) {
let roots = active.filter { $0.parentTaskId == nil || !ids.contains($0.parentTaskId!) }
for task in store.byRank(roots.filter { !$0.background }) + roots.filter(\.background) {
add(task, 0)
}
return out
}
private func rowIndent(_ task: ByreTask, _ depth: Int) -> CGFloat {
task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0
}
private struct TaskRows: View {
let active: [ByreTask]
@Environment(Store.self) private var store
@@ -243,8 +244,13 @@ private struct TaskRows: View {
if !active.isEmpty {
VStack(alignment: .leading, spacing: 0) {
ForEach(nested(store, active), id: \.task.id) { row in
TaskRow(task: row.task, depth: row.depth)
.id(row.task.id)
if row.task.background {
BackgroundRow(task: row.task, depth: row.depth)
.id(row.task.id)
} else {
TaskRow(task: row.task, depth: row.depth)
.id(row.task.id)
}
}
}
.padding(.bottom, 8)
@@ -285,14 +291,10 @@ private struct TaskRow: View {
let ts = store.taskState(task)
let color = taskPalette[task.color]
let count = store.inbox.filter { $0.taskId == task.id }.count
HoverRow(selected: task.id == store.selectedTaskId, indent: task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0) {
HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) {
store.focusTask(task.id)
} content: {
if let parent = task.parentTaskId {
Text("↳")
.foregroundStyle(Palette.muted)
.help("Subtask of \(store.findTask(parent)?.name ?? parent)")
}
SubtaskMark(task: task)
StateMark(state: ts, color: color)
Text(task.name)
.foregroundStyle(color)
@@ -318,15 +320,30 @@ private struct TaskRow: View {
}
}
private struct BackgroundRow: View {
private struct SubtaskMark: View {
let task: ByreTask
@Environment(Store.self) private var store
var body: some View {
if let parent = task.parentTaskId {
Text("↳")
.foregroundStyle(Palette.muted)
.help("Subtask of \(store.findTask(parent)?.name ?? parent)")
}
}
}
private struct BackgroundRow: View {
let task: ByreTask
let depth: Int
@Environment(Store.self) private var store
var body: some View {
let ts = store.taskState(task)
HoverRow(selected: task.id == store.selectedTaskId) {
HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) {
store.focusTask(task.id)
} content: {
SubtaskMark(task: task)
Text(task.name)
.lineLimit(1)
.truncationMode(.tail)
+9 -1
View File
@@ -391,6 +391,14 @@ final class Store {
projects.first { $0.id == focus.projectId }
}
var shownTab: TaskTab {
let run = selectedTaskId.flatMap { findTask($0) }?.workflow != nil
if run {
return focus.tab == .project ? .project : .workflow
}
return focus.tab == .workflow ? .transcript : focus.tab
}
func setFocus(taskId: String?? = .none, projectId: String?? = .none, tab: TaskTab? = nil) async throws {
let task = taskId ?? focus.taskId
let project = task != nil ? findTask(task!)?.projectId : (projectId ?? focus.projectId)
@@ -527,7 +535,7 @@ final class Store {
}
func unreadSummary(_ task: ByreTask) -> Bool {
task.workflow == nil && task.summarySeq > (summaryReads[task.id] ?? 0)
task.workflowStage == nil && task.summarySeq > (summaryReads[task.id] ?? 0)
}
var unreadSummaries: Int {
+11 -8
View File
@@ -66,6 +66,7 @@ private func stateColor(_ state: String) -> Color {
struct WorkflowView: View {
@Environment(Store.self) private var store
@ViewState private var run: WorkflowRun?
@ViewState private var runId: String?
@ViewState private var error = ""
@ViewState private var target = 0
@ViewState private var loading = false
@@ -74,8 +75,8 @@ struct WorkflowView: View {
var body: some View {
let task = store.selectedTaskId.flatMap { store.findTask($0) }
VStack(spacing: 0) {
if let task, let runId = task.workflowRun {
if let run, run.run == runId {
if let task, task.workflow != nil {
if let run, runId == task.id {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(run.name).font(Fonts.font(13, weight: 700))
Text(run.title).foregroundStyle(Palette.muted)
@@ -114,13 +115,14 @@ struct WorkflowView: View {
EmptyText(text: "Loading…")
}
} else {
EmptyText(text: task != nil ? "This task isn't part of a workflow run." : "No task selected.")
EmptyText(text: task != nil ? "This task isn't a workflow run." : "No task selected.")
}
}
.frame(maxHeight: .infinity, alignment: .top)
.task(id: store.selectedTaskId) {
target += 1
run = nil
runId = nil
error = ""
await load()
let off = store.events.add { ev in
@@ -134,7 +136,7 @@ struct WorkflowView: View {
off()
target += 1
}
.onChange(of: task?.workflowRun) {
.onChange(of: task?.workflow) {
if run == nil && !loading && error.isEmpty {
Task {
await load()
@@ -144,13 +146,13 @@ struct WorkflowView: View {
}
private func relevant(_ ev: ByreEvent) -> Bool {
guard ev.seq != 0, let run else {
guard ev.seq != 0, let run, let runId else {
return false
}
if ev.kind == "task.created" {
return ev.payload.value["workflow_run"] as? String == run.run
return ev.payload.value["parent_task_id"] as? String == runId
}
let ids = Set(run.stages.flatMap { $0.attempts.map(\.taskId) })
let ids = Set(run.stages.flatMap { $0.attempts.map(\.taskId) } + [runId])
guard let taskId = ev.taskId else {
return false
}
@@ -158,7 +160,7 @@ struct WorkflowView: View {
}
private func load() async {
guard let taskId = store.selectedTaskId, store.findTask(taskId)?.workflowRun != nil else {
guard let taskId = store.selectedTaskId, store.findTask(taskId)?.workflow != nil else {
return
}
if loading {
@@ -172,6 +174,7 @@ struct WorkflowView: View {
let fresh = try await store.api.client.getWorkflowRun(path: .init(taskId: taskId)).ok.body.json
if started == target {
run = fresh
runId = taskId
error = ""
}
} catch {