2026-10-17 09:33:27 -07:00
import ByreAPI
2026-10-17 07:26:18 -07:00
import SwiftUI
typealias FindingState = Components . Schemas . FindingState
2026-10-19 09:14:21 -07:00
typealias FindingResponse = Components . Schemas . FindingResponse
2026-10-17 07:26:18 -07:00
private let findingHead = Re (# "^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@" #)
@ MainActor
2026-10-17 07:43:33 -07:00
private enum SnapshotPatches {
2026-10-17 07:26:18 -07:00
static var changes : [ String : Task < TaskChanges , any Error >] = [:]
static var patches : [ String : Task < String ?, any Error >] = [:]
static func patch ( _ api : API , _ taskId : String , _ n : Int , _ path : String ) async throws -> String ? {
2026-10-18 20:58:56 -07:00
let key = " \( api . origin . absoluteString ) / \( taskId ) / \( n ) / \( path ) "
2026-10-17 07:26:18 -07:00
if let cached = patches [ key ] {
return try await cached . value
}
let fetched = Task < String ?, any Error > {
2026-10-18 20:58:56 -07:00
let ck = " \( api . origin . absoluteString ) / \( taskId ) / \( n ) "
2026-10-17 07:26:18 -07:00
let pending = changes [ ck ] ?? Task {
2026-10-17 07:43:33 -07:00
try await api . client . getTaskChanges ( path : . init ( taskId : taskId ), query : . init ( snapshot : n )). ok . body . json
2026-10-17 07:26:18 -07:00
}
changes [ ck ] = pending
let c : TaskChanges
do {
c = try await pending . value
} catch {
changes [ ck ] = nil
throw error
}
guard let f = c . files . first ( where : { $0 . path == path }), ! f . binary else {
return nil
}
return try await api . client . getTaskDiff ( path : . init ( taskId : taskId ), query : . init ( base : c . base , tree : c . tree , path : f . path , oldPath : f . oldPath )). ok . body . json . patch
}
patches [ key ] = fetched
do {
return try await fetched . value
} catch {
patches [ key ] = nil
throw error
}
}
}
private func linesAround ( _ patch : String , _ line : Int , _ end : Int ) -> [( kind : PatchLine , text : String , at : Int )] {
var lines : [( kind : PatchLine , text : String , at : Int )] = []
var n = 0
var inHunk = false
for l in patch . components ( separatedBy : " \n " ) {
if let m = findingHead . exec ( l ) {
n = Int ( m [ 1 ] ! ) ?? 0
inHunk = true
continue
}
if ! inHunk {
continue
}
if l . hasPrefix ( "+" ) {
lines . append ((. add , l , n ))
n += 1
} else if l . hasPrefix ( "-" ) {
lines . append ((. del , l , n ))
} else if l . hasPrefix ( " " ) {
lines . append ((. ctx , l , n ))
n += 1
}
}
return lines . filter { $0 . at >= line - 3 && $0 . at <= end + 3 }
}
2026-10-19 09:14:21 -07:00
func findingResponse ( _ r : Review , _ index : Int ) -> FindingResponse ? {
r . findingResponses . additionalProperties [ String ( index )]
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
func findingOpen ( _ r : Review , _ index : Int ) -> Bool {
r . findingStates . additionalProperties [ String ( index )] == nil && findingResponse ( r , index ) == nil
}
2026-10-17 07:26:18 -07:00
2026-10-19 09:14:21 -07:00
func findingWhere ( _ f : JSONDict ) -> String {
let path = str ( f , "path" ) ?? ""
let line = max ( 0 , int ( f , "line" ) ?? 0 )
let endRaw = int ( f , "end_line" ) ?? 0
let end = endRaw > line ? endRaw : line
return path . isEmpty ? "" : " \( path )\( line > 0 ? ": \( line )\( end > line ? "– \( end ) " : "" ) " : "" ) "
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
struct StopReviewButton : View {
2026-10-17 10:54:06 -07:00
let reviewId : String
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var busy = false
var body : some View {
2026-10-19 09:14:21 -07:00
Button ( "stop" ) {
2026-10-17 10:54:06 -07:00
busy = true
Task {
do {
_ = try await store . api . client . stopReview ( path : . init ( reviewId : reviewId )). ok
} catch {
busy = false
overlays . showAlert ( errorText ( error ))
}
}
}
2026-10-19 09:14:21 -07:00
. buttonStyle ( SmallLinkButtonStyle ( color : Palette . danger ))
2026-10-17 10:54:06 -07:00
. disabled ( busy )
. help ( "End the reviewer and record this review as failed" )
}
}
2026-10-19 09:14:21 -07:00
struct FindingRow : View {
2026-10-17 07:26:18 -07:00
let review : Review
let index : Int
let taskId : String ?
var body : some View {
2026-10-19 09:14:21 -07:00
let f = findings ( review )[ index ]
2026-10-17 07:26:18 -07:00
let severity = jsString ( f [ "severity" ] ?? "" )
let path = str ( f , "path" ) ?? ""
let line = max ( 0 , int ( f , "line" ) ?? 0 )
let endRaw = int ( f , "end_line" ) ?? 0
let end = endRaw > line ? endRaw : line
2026-10-19 09:14:21 -07:00
let whereText = findingWhere ( f )
2026-10-17 07:26:18 -07:00
let md = MDContext ( taskId : taskId )
2026-10-19 09:14:21 -07:00
let response = findingResponse ( review , index )
2026-10-17 07:26:18 -07:00
VStack ( alignment : . leading , spacing : 4 ) {
HStack ( alignment : . firstTextBaseline , spacing : 8 ) {
2026-10-19 09:14:21 -07:00
Text ( " \( index + 1 ) ." )
. font ( Fonts . font ( 11 , weight : 700 ))
. foregroundStyle ( Palette . muted )
2026-10-17 07:26:18 -07:00
Text ( severity )
. font ( Fonts . font ( 11 , weight : 700 ))
. foregroundStyle ( severity == "critical" || severity == "high" ? Palette . danger : severity == "medium" ? Palette . warn : Palette . muted )
if let category = str ( f , "category" ), ! category . isEmpty {
Text ( category ). font ( Fonts . font ( 11 )). foregroundStyle ( Palette . muted )
}
Text ( jsString ( f [ "title" ] ?? "" ))
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
if ! whereText . isEmpty {
Text ( whereText ). font ( Fonts . font ( 11 )). foregroundStyle ( Palette . muted ). textSelection (. enabled )
}
Spacer ( minLength : 0 )
if let taskId {
2026-10-19 09:14:21 -07:00
FindingActions ( taskId : taskId , review : review , index : index )
2026-10-17 07:26:18 -07:00
}
}
2026-10-19 09:14:21 -07:00
if let response {
Text ( "agent \( response . response . rawValue ) : \( response . reason ) " )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
Group {
if let taskId , ! path . isEmpty , line > 0 {
FindingDiff ( taskId : taskId , n : review . snapshotN , path : path , line : line , end : end )
}
MarkdownView ( blocks : parseMarkdown ( jsString ( f [ "detail" ] ?? "" )), ctx : md )
ReviewImages ( value : f [ "images" ] ?? nil )
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
. opacity ( response == nil ? 1 : 0.5 )
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
. padding (. leading , 10 )
. overlay ( alignment : . leading ) {
Rectangle (). fill ( Palette . border ). frame ( width : 1 )
2026-10-17 07:26:18 -07:00
}
}
}
2026-10-19 09:14:21 -07:00
struct ReviewImages : View {
2026-10-17 07:26:18 -07:00
let value : ( any Sendable )?
var body : some View {
let imgs = ( value as ? [( any Sendable )?] ?? []). map { $0 as ? JSONDict ?? [:] }
if ! imgs . isEmpty {
FlowLayout ( spacing : 10 ) {
ForEach ( Array ( imgs . enumerated ()), id : \ . offset ) { _ , img in
let blob = jsString ( img [ "blob" ] ?? "" )
let caption = str ( img , "caption" ) ?? ""
VStack ( alignment : . leading , spacing : 2 ) {
ByreImage ( source : . blob ( blob ), src : "/api/blobs/ \( blob ) " , maxHeight : 360 )
if ! caption . isEmpty {
Text ( caption ). font ( Fonts . font ( 11 )). foregroundStyle ( Palette . muted )
}
}
}
}
. padding (. vertical , 8 )
}
}
}
private struct FindingDiff : View {
let taskId : String
let n : Int
let path : String
let line : Int
let end : Int
@ Environment ( Store . self ) private var store
@ ViewState private var rows : [ DiffRow ]?
@ ViewState private var failure : String ?
var body : some View {
Group {
if let failure {
Text ( "diff not available: \( failure ) " )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
. padding (. horizontal , 10 )
. padding (. vertical , 4 )
. frame ( maxWidth : . infinity , alignment : . leading )
. modifier ( DiffBox ())
} else if let rows , ! rows . isEmpty {
DiffLines ( rows : rows )
. modifier ( DiffBox ())
}
}
. task ( id : " \( taskId ) / \( n ) / \( path ) / \( line ) / \( end ) " ) {
2026-10-17 10:54:06 -07:00
failure = nil
2026-10-17 07:26:18 -07:00
do {
2026-10-17 07:43:33 -07:00
let patch = try await SnapshotPatches . patch ( store . api , taskId , n , path )
2026-10-17 07:26:18 -07:00
let lines = patch . map { linesAround ( $0 , line , end ) } ?? []
var out : [ DiffRow ] = []
for ( i , d ) in lines . enumerated () {
if i > 0 {
let prev = lines [ i - 1 ]
if d . kind != . del && prev . kind != . del && d . at > prev . at + 1 {
out . append ( DiffRow ( text : "⋯" , kind : . hunk ))
}
}
let mark = d . kind != . del && d . at >= line && d . at <= end
out . append ( DiffRow ( text : d . text , kind : d . kind , lineno : d . kind == . del ? "" : String ( d . at ), mark : mark ))
}
rows = out
} catch {
failure = errorText ( error )
}
}
}
}
private struct DiffBox : ViewModifier {
func body ( content : Content ) -> some View {
content
. background ( Palette . bg )
. clipShape ( RoundedRectangle ( cornerRadius : 6 ))
. overlay ( RoundedRectangle ( cornerRadius : 6 ). stroke ( Palette . border ))
. padding (. top , 2 )
}
}
2026-10-19 09:14:21 -07:00
struct FindingActions : View {
2026-10-17 07:26:18 -07:00
let taskId : String
let review : Review
let index : Int
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var busy = false
var body : some View {
2026-10-19 09:14:21 -07:00
let response = findingResponse ( review , index )?. response
let state = review . findingStates . additionalProperties [ String ( index )]
Group {
if let state {
Text ( state == . fixed ? "fix sent" : "ignored" )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
} else if response != . fixing {
HStack ( spacing : 6 ) {
Button ( "fix" ) {
fix ()
}
. buttonStyle ( SmallLinkButtonStyle ())
. help ( response == . ignoring ? "Overrule the agent: send this finding to it to fix" : "Send this finding to the task's agent to fix" )
if response == nil {
Button ( "ignore" ) {
ignore ()
}
. buttonStyle ( SmallLinkButtonStyle ())
. help ( "Mark this finding done without fixing it; Merge won't warn about it" )
}
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
. disabled ( busy )
2026-10-17 07:26:18 -07:00
}
2026-10-19 09:14:21 -07:00
}
. onChange ( of : state ) {
busy = false
2026-10-17 07:26:18 -07:00
}
}
private func mark ( _ s : FindingState ) async throws {
_ = try await store . api . client . setFindingState ( path : . init ( reviewId : review . id , index : index ), body : . json (. init ( state : s ))). ok
}
private func fix () {
2026-10-19 09:14:21 -07:00
let f = findings ( review )[ index ]
let whereText = findingWhere ( f )
let text = "Fix finding \( index + 1 ) from the review of snapshot # \( review . snapshotN ) : \n\n **[ \( jsString ( f [ "severity" ] ?? "" )) ] \( jsString ( f [ "title" ] ?? "" )) ** \( whereText . isEmpty ? "" : " (` \( whereText ) `)" ) \n\n \( jsString ( f [ "detail" ] ?? "" )) "
2026-10-17 07:26:18 -07:00
busy = true
Task {
do {
2026-10-18 07:30:00 -07:00
try await store . messageTask ( taskId , text )
2026-10-17 07:26:18 -07:00
} catch {
busy = false
overlays . showAlert ( errorText ( error ))
return
}
do {
try await mark (. fixed )
} catch {
2026-10-19 09:14:21 -07:00
busy = false
2026-10-17 07:26:18 -07:00
overlays . showAlert ( "fix sent, but not recorded: \( errorText ( error )) " )
}
}
}
private func ignore () {
busy = true
Task {
do {
try await mark (. ignored )
} catch {
busy = false
overlays . showAlert ( errorText ( error ))
}
}
}
}