2026-10-17 09:33:27 -07:00
import ByreAPI
2026-10-17 07:26:18 -07:00
import SwiftUI
typealias Todo = Components . Schemas . Todo
typealias TodoItem = Components . Schemas . TodoItem
private let refreshKinds : Set < String > = [ "task.merged" , "project.created" ]
struct ProjectView : View {
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var todos : [ String : Result < Todo , ApiError >] = [:]
@ ViewState private var off : (() -> Void )?
2026-10-20 15:50:06 -07:00
@ FocusState private var keys : Bool
2026-10-17 07:26:18 -07:00
var body : some View {
2026-10-20 15:50:06 -07:00
ScrollViewReader { proxy in
ScrollView {
if let p = store . focusedProject () {
ProjectBlock ( project : p , todo : todos [ p . id ])
. padding (. horizontal , 16 )
. padding (. top , 8 )
. padding (. bottom , 16 )
} else {
Muted ( text : "No project selected." )
}
}
. onChange ( of : store . selectedTaskId ) {
if let id = store . selectedTaskId {
proxy . scrollTo ( id )
}
2026-10-17 07:26:18 -07:00
}
}
. frame ( maxWidth : . infinity , maxHeight : . infinity )
2026-10-20 15:50:06 -07:00
. focusable ()
. focusEffectDisabled ()
. focused ( $ keys )
. onKeyPress ( keys : [. upArrow , . downArrow ]) { press in
step ( press . key == . downArrow ? 1 : - 1 )
return . handled
}
2026-10-20 18:45:39 -07:00
. onKeyPress (. return ) {
guard let id = store . selectedTaskId , store . focusedProject () != nil else {
return . ignored
}
store . focusTask ( id )
return . handled
}
2026-10-17 07:26:18 -07:00
. onAppear {
2026-10-20 15:50:06 -07:00
if ! ( overlays . window ?. firstResponder is NSTextView ) {
keys = true
}
2026-10-17 07:26:18 -07:00
off = store . events . add { ev in
if refreshKinds . contains ( ev . kind ), let projectId = ev . projectId {
load ( projectId )
}
}
refresh ()
}
. onDisappear {
off ?()
off = nil
}
. onChange ( of : store . connected ) {
if store . connected {
refresh ()
}
}
. onChange ( of : store . projects . map ( \ . id )) {
refresh ()
}
. onChange ( of : store . focusedProject ()?. id ) {
if let p = store . focusedProject (), todos [ p . id ] == nil {
load ( p . id )
}
}
}
2026-10-20 15:50:06 -07:00
private func step ( _ by : Int ) {
guard let p = store . focusedProject () else {
return
}
2026-10-20 21:08:31 -07:00
let rows = nested ( store , ( store . tasks [ p . id ] ?? []). filter { $0 . status == . active && ! $0 . background }). map ( \ . task . id )
2026-10-20 15:50:06 -07:00
if rows . isEmpty {
return
}
let next : Int
if let i = rows . firstIndex ( where : { $0 == store . selectedTaskId }) {
next = min ( max ( i + by , 0 ), rows . count - 1 )
} else {
next = by > 0 ? 0 : rows . count - 1
}
store . focusTask ( rows [ next ], . project )
}
2026-10-17 07:26:18 -07:00
private func refresh () {
if let p = store . focusedProject () {
load ( p . id )
}
}
private func load ( _ projectId : String ) {
Task {
do {
todos [ projectId ] = . success ( try await store . api . client . getTodo ( path : . init ( projectId : projectId )). ok . body . json )
} catch {
todos [ projectId ] = . failure ( ApiError ( status : 0 , message : "Couldn't read TODO.md: \( errorText ( error )) " ))
}
}
}
}
2026-10-20 21:08:31 -07:00
private struct ProjectWorkflowMenu : View {
2026-10-20 18:52:04 -07:00
let project : Project
2026-10-20 21:08:31 -07:00
let audit : Bool
2026-10-20 18:52:04 -07:00
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
2026-10-20 21:08:31 -07:00
@ ViewState private var workflows : [ String ] = []
2026-10-20 18:52:04 -07:00
var body : some View {
2026-10-20 21:08:31 -07:00
let current = audit ? project . auditWorkflow : project . reviewWorkflow
2026-10-20 18:52:04 -07:00
Menu {
2026-10-20 21:42:28 -07:00
Button ( "Off" ) { set ( "" ) }
2026-10-20 21:08:31 -07:00
ForEach ( workflows , id : \ . self ) { name in
2026-10-20 18:52:04 -07:00
Button ( name ) { set ( name ) }
}
} label : {
HStack ( spacing : 6 ) {
2026-10-20 21:42:28 -07:00
Text ( current . isEmpty ? "Off" : current )
2026-10-20 18:52:04 -07:00
Image ( systemName : "chevron.up.chevron.down" )
. font (. system ( size : 8 , weight : . semibold ))
}
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
. contentShape ( Rectangle ())
}
. menuStyle (. button )
. buttonStyle (. plain )
. menuIndicator (. hidden )
. fixedSize ()
2026-10-20 21:08:31 -07:00
. help ( audit ? "The workflow byre runs in the background on \( project . defaultBranch ) 's newest commit to check the whole codebase" : "The review workflow whose reviewers review every snapshot of this project's tasks" )
2026-10-20 18:52:04 -07:00
. task {
do {
2026-10-20 21:08:31 -07:00
workflows = try await store . api . client . listWorkflows (). ok . body . json . map ( \ . name )
2026-10-20 18:52:04 -07:00
} catch {
2026-10-20 21:08:31 -07:00
workflows = []
2026-10-20 18:52:04 -07:00
}
}
}
private func set ( _ name : String ) {
Task {
do {
2026-10-20 21:08:31 -07:00
if audit {
_ = try await store . api . client . setAuditWorkflow ( path : . init ( projectId : project . id ), body : . json (. init ( workflow : name ))). ok . body . json
} else {
_ = try await store . api . client . setReviewWorkflow ( path : . init ( projectId : project . id ), body : . json (. init ( workflow : name ))). ok . body . json
}
2026-10-20 18:52:04 -07:00
} catch {
overlays . showAlert ( errorText ( error ))
}
}
}
}
2026-10-17 07:26:18 -07:00
private struct ProjectBlock : View {
let project : Project
let todo : Result < Todo , ApiError >?
@ Environment ( Store . self ) private var store
var body : some View {
let active = ( store . tasks [ project . id ] ?? []). filter { $0 . status == . active }
VStack ( alignment : . leading , spacing : 0 ) {
HStack ( alignment : . firstTextBaseline , spacing : 6 ) {
Text ( project . name ). font ( Fonts . font ( 13 , weight : 700 ))
Text ( project . defaultBranch ). font ( Fonts . font ( 11 )). foregroundStyle ( Palette . muted )
}
. padding (. top , 16 )
. padding (. bottom , 6 )
2026-10-20 21:42:28 -07:00
Grid ( alignment : . leading , horizontalSpacing : 12 , verticalSpacing : 4 ) {
GridRow {
Text ( "Audit \( project . defaultBranch ) " )
ProjectWorkflowMenu ( project : project , audit : true )
}
GridRow {
Text ( "Review tasks" )
ProjectWorkflowMenu ( project : project , audit : false )
}
}
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
2026-10-17 07:26:18 -07:00
heading ( "Tasks" )
2026-10-20 21:08:31 -07:00
TaskRows ( active : active . filter { ! $0 . background })
ForEach ( active . filter ( \ . background ), id : \ . id ) { task in
BackgroundRow ( task : task )
. id ( task . id )
}
2026-10-17 07:26:18 -07:00
heading ( "TODO" )
TodoBlock ( project : project , todo : todo , active : active )
}
. frame ( maxWidth : . infinity , alignment : . leading )
}
private func heading ( _ text : String ) -> some View {
Text ( text )
. font ( Fonts . font ( 12 , weight : 700 ))
. padding (. top , 14 )
. padding (. bottom , 4 )
}
}
2026-10-19 17:53:54 -07:00
@ MainActor
private func nested ( _ store : Store , _ active : [ ByreTask ]) -> [( task : ByreTask , depth : Int )] {
2026-10-17 07:26:18 -07:00
let ids = Set ( active . map ( \ . id ))
var out : [( task : ByreTask , depth : Int )] = []
func add ( _ task : ByreTask , _ depth : Int ) {
out . append (( task , depth ))
2026-10-19 17:53:54 -07:00
for child in store . byRank ( active . filter ({ $0 . parentTaskId == task . id }). reversed ()) {
2026-10-17 07:26:18 -07:00
add ( child , depth + 1 )
}
}
2026-10-19 17:53:54 -07:00
for task in store . byRank ( active . filter { $0 . parentTaskId == nil || ! ids . contains ( $0 . parentTaskId !) }) {
2026-10-17 07:26:18 -07:00
add ( task , 0 )
}
return out
}
private struct TaskRows : View {
let active : [ ByreTask ]
@ Environment ( Store . self ) private var store
var body : some View {
if active . isEmpty {
Text ( "No active tasks." )
. foregroundStyle ( Palette . muted )
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
} else {
VStack ( alignment : . leading , spacing : 0 ) {
2026-10-19 17:53:54 -07:00
ForEach ( nested ( store , active ), id : \ . task . id ) { row in
2026-10-17 07:26:18 -07:00
TaskRow ( task : row . task , depth : row . depth )
2026-10-20 15:50:06 -07:00
. id ( row . task . id )
2026-10-17 07:26:18 -07:00
}
}
. padding (. bottom , 8 )
}
}
}
struct HoverRow < Content : View >: View {
var selected = false
var indent : CGFloat = 0
let action : () -> Void
@ ViewBuilder let content : Content
@ ViewState private var hover = false
var body : some View {
Button ( action : action ) {
HStack ( spacing : 10 ) {
content
}
2026-10-17 10:54:06 -07:00
. padding (. leading , max ( 0 , 10 + indent ))
2026-10-17 07:26:18 -07:00
. padding (. trailing , 10 )
. padding (. vertical , 6 )
. frame ( maxWidth : . infinity , alignment : . leading )
. background ( selected ? Palette . accentTint : hover ? Palette . surface : . clear , in : RoundedRectangle ( cornerRadius : 6 ))
. contentShape ( Rectangle ())
}
. buttonStyle (. plain )
. onHover { hover = $0 }
}
}
private struct TaskRow : View {
let task : ByreTask
let depth : Int
@ Environment ( Store . self ) private var store
var body : some 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 ) {
2026-10-19 19:00:52 -07:00
store . focusTask ( task . id )
2026-10-17 07:26:18 -07:00
} content : {
if let parent = task . parentTaskId {
Text ( "↳" )
. foregroundStyle ( Palette . muted )
2026-10-20 08:57:37 -07:00
. help ( "Subtask of \( store . findTask ( parent ) ?. name ?? parent ) " )
2026-10-17 07:26:18 -07:00
}
StateMark ( state : ts , color : color )
2026-10-20 08:57:37 -07:00
Text ( task . name )
2026-10-17 07:26:18 -07:00
. foregroundStyle ( color )
. lineLimit ( 1 )
. truncationMode (. tail )
. frame ( maxWidth : . infinity , alignment : . leading )
if count > 0 {
2026-10-18 15:06:06 -07:00
Badge ( count : count ). help ( "open questions" )
2026-10-17 07:26:18 -07:00
}
2026-10-19 19:00:52 -07:00
if ts . kind == . idle && store . unreadSummary ( task ) {
2026-10-17 07:26:18 -07:00
Text ( "new summary" )
. font ( Fonts . font ( 11 , weight : 700 ))
. foregroundStyle ( Palette . accent )
. help ( "A turn summary you haven't read" )
2026-10-20 08:57:37 -07:00
. accessibilityLabel ( "New summary from \( task . name ) , unread" )
2026-10-19 19:00:52 -07:00
} else {
Text ( ts . label )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( ts . needsYou ? Palette . warn : Palette . muted )
. help ( ts . title )
2026-10-17 07:26:18 -07:00
}
}
}
}
2026-10-20 21:08:31 -07:00
private struct BackgroundRow : View {
let task : ByreTask
@ Environment ( Store . self ) private var store
var body : some View {
let ts = store . taskState ( task )
HoverRow ( selected : task . id == store . selectedTaskId ) {
store . focusTask ( task . id )
} content : {
Text ( task . name )
. lineLimit ( 1 )
. truncationMode (. tail )
. frame ( maxWidth : . infinity , alignment : . leading )
Text ( ts . label )
. help ( ts . title )
}
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
. help ( "Runs in the background until it asks for your attention" )
}
}
2026-10-17 07:26:18 -07:00
private struct TodoBlock : View {
let project : Project
let todo : Result < Todo , ApiError >?
let active : [ ByreTask ]
var body : some View {
switch todo {
case . none :
row ( "Loading…" )
case . failure ( let e ):
row ( e . message , error : true )
case . success ( let t ):
if ! t . exists {
row ( "No TODO.md on \( project . defaultBranch ) ." )
} else if t . items . isEmpty {
row ( "TODO.md has no items." )
} else {
VStack ( alignment : . leading , spacing : 0 ) {
ForEach ( Array ( t . items . enumerated ()), id : \ . offset ) { i , item in
if ! item . section . isEmpty && ( i == 0 || t . items [ i - 1 ]. section != item . section ) {
Text ( item . section )
. font ( Fonts . font ( 12 , weight : 700 ))
. foregroundStyle ( Palette . muted )
2026-10-17 10:54:06 -07:00
. padding (. top , i == 0 ? 10 : 2 )
2026-10-17 07:26:18 -07:00
. padding (. bottom , 4 )
}
TodoRow ( project : project , item : item , task : active . first { $0 . todoItem == item . title })
2026-10-17 10:54:06 -07:00
. padding (. bottom , i == t . items . count - 1 || t . items [ i + 1 ]. section != item . section ? 8 : 0 )
2026-10-17 07:26:18 -07:00
}
}
}
}
}
private func row ( _ text : String , error : Bool = false ) -> some View {
Text ( text )
. foregroundStyle ( error ? Palette . danger : Palette . muted )
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
}
}
private struct TodoRow : View {
let project : Project
let item : TodoItem
let task : ByreTask ?
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
var body : some View {
VStack ( alignment : . leading , spacing : 4 ) {
HStack ( spacing : 10 ) {
Text ( item . title )
. foregroundStyle ( task . map { taskPalette [ $0 . color ] } ?? Palette . text )
. frame ( maxWidth : . infinity , alignment : . leading )
. fixedSize ( horizontal : false , vertical : true )
action
}
if ! item . body . isEmpty {
MarkdownView ( blocks : parseMarkdown ( item . body ), ctx : MDContext ( color : Palette . muted ))
}
}
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
. background ( task != nil && task ?. id == store . selectedTaskId ? Palette . accentTint : . clear , in : RoundedRectangle ( cornerRadius : 6 ))
}
@ ViewBuilder
private var action : some View {
if let task {
let ts = store . taskState ( task )
Button {
store . focusTask ( task . id )
} label : {
HStack ( spacing : 6 ) {
StateMark ( state : ts , color : taskPalette [ task . color ])
Text ( ts . label )
}
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
. contentShape ( Rectangle ())
}
. buttonStyle (. plain )
. fixedSize ()
} else {
let busy = store . todoStarting . contains ( " \( project . id ) \n \( item . title ) " )
HStack ( spacing : 6 ) {
Button ( busy ? "Starting…" : "Start" ) {
store . startTodo ( project . id , item . title , discuss : false , overlays )
}
. help ( "Start a task that gets straight to work" )
Button ( "Discuss" ) {
store . startTodo ( project . id , item . title , discuss : true , overlays )
}
. help ( "Start a task that discusses it with you in chat before building anything" )
}
. buttonStyle ( WebButtonStyle ())
. disabled ( busy )
. fixedSize ()
}
}
}