ready to merge and ready to discard as states

This commit is contained in:
Ian Gulliver
2026-10-19 17:53:54 -07:00
parent fb79919ebb
commit 2bb527aca0
11 changed files with 394 additions and 293 deletions
+1 -1
View File
@@ -512,7 +512,7 @@ struct StateMark: View {
switch state.kind {
case .working, .merging:
Spinner(color: color)
case .answer:
case .answer, .ready:
StateDot(filled: Palette.warn)
default:
StateDot(filled: nil)
+6 -5
View File
@@ -98,16 +98,17 @@ private struct ProjectBlock: View {
}
}
private func nested(_ active: [ByreTask]) -> [(task: ByreTask, depth: Int)] {
@MainActor
private func nested(_ store: Store, _ active: [ByreTask]) -> [(task: ByreTask, depth: Int)] {
let ids = Set(active.map(\.id))
var out: [(task: ByreTask, depth: Int)] = []
func add(_ task: ByreTask, _ depth: Int) {
out.append((task, depth))
for child in active.filter({ $0.parentTaskId == task.id }).reversed() {
for child in store.byRank(active.filter({ $0.parentTaskId == task.id }).reversed()) {
add(child, depth + 1)
}
}
for task in active where task.parentTaskId == nil || !ids.contains(task.parentTaskId!) {
for task in store.byRank(active.filter { $0.parentTaskId == nil || !ids.contains($0.parentTaskId!) }) {
add(task, 0)
}
return out
@@ -125,7 +126,7 @@ private struct TaskRows: View {
.padding(.vertical, 6)
} else {
VStack(alignment: .leading, spacing: 0) {
ForEach(nested(active), id: \.task.id) { row in
ForEach(nested(store, active), id: \.task.id) { row in
TaskRow(task: row.task, depth: row.depth)
}
}
@@ -197,7 +198,7 @@ private struct TaskRow: View {
}
Text(ts.label)
.font(Fonts.font(11))
.foregroundStyle(ts.kind == .answer ? Palette.warn : Palette.muted)
.foregroundStyle(ts.needsYou ? Palette.warn : Palette.muted)
.help(ts.title)
}
}
+44 -2
View File
@@ -22,12 +22,16 @@ private let routeHash = Re(#"^#/(task|project)/(\w+)(?:/(\w+))?$"#)
struct TaskState {
enum Kind {
case answer, merging, held, working, idle
case answer, merging, held, working, ready, idle
}
let kind: Kind
let label: String
let title: String
var needsYou: Bool {
kind == .answer || kind == .ready
}
}
private let workingTitles: [ByreTask.StatePayload: (label: String, title: String)] = [
@@ -222,9 +226,42 @@ final class Store {
if let working = workingTitles[task.state] {
return TaskState(kind: .working, label: working.label, title: working.title)
}
switch task.ready {
case .merge:
return TaskState(kind: .ready, label: "Ready to merge", title: "The agent says the task is ready to merge")
case .discard:
return TaskState(kind: .ready, label: "Ready to discard", title: "The agent suggests discarding the task")
case nil:
break
}
return TaskState(kind: .idle, label: "Idle", title: "Waiting for a message")
}
func taskRank(_ task: ByreTask) -> Int {
if task.discarding {
return 4
}
switch taskState(task).kind {
case .answer: return 0
case .ready: return 1
case .idle: return 2
case .working: return 3
case .merging, .held: return 4
}
}
func byRank(_ tasks: [ByreTask]) -> [ByreTask] {
tasks.enumerated()
.sorted { a, b in
let ra = taskRank(a.element), rb = taskRank(b.element)
if ra != rb {
return ra < rb
}
return a.offset < b.offset
}
.map(\.element)
}
func currentLead() -> AgentSession? {
leadSessions.last(where: { $0.state != .completed }) ?? leadSessions.last
}
@@ -613,7 +650,7 @@ final class Store {
spawn("load projects") {
try await self.loadProjects()
}
case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state":
case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "snapshot.created":
if let projectId = ev.projectId {
spawn("load tasks") {
try await self.loadTasks(projectId)
@@ -634,6 +671,11 @@ final class Store {
spawn("load inbox") {
try await self.loadInbox()
}
if let projectId = ev.projectId {
spawn("load tasks") {
try await self.loadTasks(projectId)
}
}
case "inbox.updated":
clearInbox(try! decodePayload(ev.payload, as: InboxItem.self))
spawn("load inbox") {
+5 -5
View File
@@ -183,11 +183,11 @@ final class SwitcherModel {
}
.enumerated()
.map { i, row in
(i: i, project: row.project, task: row.task, count: store.inbox.filter { $0.taskId == row.task.id }.count, visited: row.task.id == store.selectedTaskId ? Double.infinity : visits[row.task.id] ?? 0)
(i: i, project: row.project, task: row.task, count: store.inbox.filter { $0.taskId == row.task.id }.count, rank: store.taskRank(row.task), visited: row.task.id == store.selectedTaskId ? Double.infinity : visits[row.task.id] ?? 0)
}
.sorted { a, b in
if (a.count > 0) != (b.count > 0) {
return a.count > 0
if a.rank != b.rank {
return a.rank < b.rank
}
if a.visited != b.visited {
return a.visited < b.visited
@@ -409,7 +409,7 @@ private struct SwitcherRowView: View {
case .task(let project, let task, let count):
let ts = store.taskState(task)
let color = taskPalette[task.color]
StateDot(filled: ts.kind == .working || ts.kind == .merging ? Palette.ok : ts.kind == .answer ? Palette.warn : nil)
StateDot(filled: ts.kind == .working || ts.kind == .merging ? Palette.ok : ts.needsYou ? Palette.warn : nil)
.help(ts.title)
Text(taskTitle(project, task, color))
.lineLimit(1)
@@ -427,7 +427,7 @@ private struct SwitcherRowView: View {
}
Text(ts.label)
.font(Fonts.font(11))
.foregroundStyle(ts.kind == .answer ? Palette.warn : Palette.muted)
.foregroundStyle(ts.needsYou ? Palette.warn : Palette.muted)
.help(ts.title)
case .project(let p):
StateDot(filled: nil)