2026-10-17 09:33:27 -07:00
import ByreAPI
2026-10-17 07:26:18 -07:00
import OpenAPIRuntime
import SwiftUI
typealias Attachment = Components . Schemas . Attachment
typealias InboxResponseCreate = Components . Schemas . InboxResponseCreate
private let delegateText = "Use your judgment for this decision and continue within the existing task instructions."
private let otherStop = "\u{0}other"
private let httpURL = Re (# "^https?://" #, caseless : true )
private func numberChoices ( _ blocks : [ MDBlock ], _ next : inout Int ) -> [ MDBlock ] {
blocks . map { block in
switch block {
case . list ( let ordered , let start , let items , let grid ):
return . list ( ordered : ordered , start : start , items : items . map { item in
var out = item
if item . choice != nil {
out . choiceIndex = next
next += 1
}
out . blocks = numberChoices ( item . blocks , & next )
return out
}, grid : grid )
case . quote ( let inner ):
return . quote ( numberChoices ( inner , & next ))
case . details ( let summary , let body ):
return . details ( summary : summary , body : numberChoices ( body , & next ))
default :
return block
}
}
}
private func firstChoices ( _ blocks : [ MDBlock ], _ out : inout [ String : Int ]) {
for block in blocks {
switch block {
case . list ( _ , _ , let items , _ ):
for item in items {
if let c = item . choice , out [ c ] == nil {
out [ c ] = item . choiceIndex
}
firstChoices ( item . blocks , & out )
}
case . quote ( let inner ):
firstChoices ( inner , & out )
case . details ( _ , let body ):
firstChoices ( body , & out )
default :
break
}
}
}
@ MainActor
@ Observable
private final class CardModel {
let item : InboxItem
let store : Store
let overlays : Overlays
let context : [ MDBlock ]
let firstChoice : [ String : Int ]
var busy = false
2026-10-19 17:46:44 -07:00
var again = false
2026-10-17 10:54:06 -07:00
@ ObservationIgnored var shown : Set < String > = []
2026-10-17 07:26:18 -07:00
init ( _ item : InboxItem , _ store : Store , _ overlays : Overlays ) {
self . item = item
self . store = store
self . overlays = overlays
var next = 0
context = numberChoices ( parseMarkdown ( jsString ( item . payload . value [ "context" ] ?? "" ), choices : true ), & next )
var first : [ String : Int ] = [:]
firstChoices ( context , & first )
firstChoice = first
}
var payload : JSONDict {
item . payload . value
}
var key : String {
"inbox: \( item . id ) "
}
var options : [ String ] {
( payload [ "options" ] as ? [( any Sendable )?] ?? []). compactMap { $0 as ? String }
}
var recommended : [ String ] {
( payload [ "recommended" ] as ? [( any Sendable )?] ?? []). compactMap { $0 as ? String }
}
var multiple : Bool {
bool ( payload , "multiple" )
}
2026-10-18 15:06:06 -07:00
var answered : Bool {
2026-10-19 17:46:44 -07:00
item . state != . open && ! again
}
var canAnswerAgain : Bool {
item . state == . answered
2026-10-18 15:06:06 -07:00
}
var chosen : [ String ] {
item . responses . flatMap { $0 . options ?? [] }
}
2026-10-17 07:26:18 -07:00
var checked : [ String ] {
2026-10-18 15:06:06 -07:00
answered ? chosen : store . drafts ?. checkedAnswer ( item . id ) ?? []
2026-10-17 07:26:18 -07:00
}
var hasImages : Bool {
! store . images . images ( key ). isEmpty
}
var typed : String {
trimmed ( store . drafts ?. textAnswer ( item . id ) ?? "" )
}
var editorOpen : Bool {
( store . drafts ?. hasTextAnswer ( item . id ) ?? false ) || hasImages
}
func toggle ( _ text : String , _ on : Bool ) {
var now = checked . filter { $0 != text }
if on {
now . append ( text )
}
store . drafts ?. setCheckedAnswer ( item . id , options . filter { now . contains ( $0 ) })
}
2026-10-17 10:54:06 -07:00
func pick ( _ text : String ) {
2026-10-18 15:06:06 -07:00
if busy || answered {
2026-10-17 10:54:06 -07:00
return
}
if multiple {
toggle ( text , ! checked . contains ( text ))
return
}
2026-10-19 16:35:09 -07:00
let notes = typed . isEmpty ? nil : store . drafts ?. textAnswer ( item . id )
submit ( InboxResponseCreate ( itemId : item . id , _type : . option , options : [ text ], text : notes ), withImages : true )
2026-10-17 10:54:06 -07:00
}
2026-10-17 07:26:18 -07:00
func submit ( _ response : InboxResponseCreate , withImages : Bool = false ) {
if busy {
return
}
busy = true
let imgs = withImages ? store . images . take ( key ) : []
Task {
do {
var r = response
r . images = try await uploadImages ( store . api , imgs )
2026-10-19 17:46:44 -07:00
if again {
let task = try await store . api . client . answerAgain ( body : . json ( r )). created . body . json
store . drafts ?. clearAnswer ( item . id )
store . paneFocus . setDraft ( key , false )
again = false
busy = false
store . focusTask ( task . id )
return
}
2026-10-17 07:26:18 -07:00
_ = try await store . api . client . respondInbox ( body : . json (. init ( responses : [ r ]))). ok
store . drafts ?. clearAnswer ( item . id )
store . paneFocus . setDraft ( key , false )
} catch {
store . images . restore ( key , imgs )
overlays . showAlert ( errorText ( error ))
busy = false
}
}
}
2026-10-19 16:36:46 -07:00
func dismiss () {
if busy {
return
}
busy = true
Task {
do {
_ = try await store . api . client . dismissInbox ( path : . init ( itemId : item . id )). ok
store . drafts ?. clearAnswer ( item . id )
store . paneFocus . setDraft ( key , false )
} catch {
overlays . showAlert ( errorText ( error ))
busy = false
}
}
}
2026-10-17 07:26:18 -07:00
func submitQuestion () {
let opts = multiple ? checked : []
if ! opts . isEmpty {
submit ( InboxResponseCreate ( itemId : item . id , _type : . option , options : opts , text : typed . isEmpty ? nil : store . drafts ?. textAnswer ( item . id )), withImages : true )
return
}
if ! typed . isEmpty || hasImages {
submit ( InboxResponseCreate ( itemId : item . id , _type : . other , text : store . drafts ?. textAnswer ( item . id ) ?? "" ), withImages : true )
}
}
}
2026-10-19 09:14:21 -07:00
struct QuestionCard : View {
2026-10-17 07:26:18 -07:00
let item : InboxItem
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var model : CardModel ?
@ FocusState private var focus : String ?
var body : some View {
2026-10-17 08:24:22 -07:00
VStack ( spacing : 0 ) {
2026-10-17 07:26:18 -07:00
if let model {
VStack ( alignment : . leading , spacing : 0 ) {
HStack ( alignment : . firstTextBaseline , spacing : 10 ) {
2026-10-18 15:06:06 -07:00
Text ( "question \( model . answered ? " · \( item . state . rawValue ) " : "" ) " )
2026-10-17 07:26:18 -07:00
Spacer ( minLength : 0 )
Text ( ago ( item . createdAt )). help ( isoText ( item . createdAt ))
}
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . muted )
CardBody ( model : model , focus : $ focus )
Thread ( item : item )
2026-10-18 15:06:06 -07:00
if ! model . answered {
CardActions ( model : model , focus : $ focus )
. padding (. top , 6 )
}
2026-10-17 07:26:18 -07:00
}
. onKeyPress ( keys : [. downArrow , . upArrow ]) { press in
move ( model , press . key == . downArrow ? 1 : - 1 )
}
}
}
2026-10-17 08:24:22 -07:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-10-17 07:26:18 -07:00
. onAppear {
if model == nil {
model = CardModel ( item , store , overlays )
}
}
}
private func move ( _ model : CardModel , _ by : Int ) -> KeyPress . Result {
2026-10-17 10:54:06 -07:00
var stops = model . firstChoice . filter { model . shown . contains ( $0 . key ) }. sorted { $0 . value < $1 . value }. map ( \ . key )
2026-10-17 07:26:18 -07:00
if ! model . editorOpen {
stops . append ( otherStop )
}
guard let current = focus , let at = stops . firstIndex ( of : current ) else {
return . ignored
}
focus = stops [ min ( max ( at + by , 0 ), stops . count - 1 )]
return . handled
}
}
2026-10-19 10:18:21 -07:00
struct QuestionDetails : View {
let item : InboxItem
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ ViewState private var model : CardModel ?
@ FocusState private var focus : String ?
var body : some View {
VStack ( alignment : . leading , spacing : 0 ) {
if let model {
CardBody ( model : model , focus : $ focus , question : false )
Thread ( item : item )
2026-10-19 17:46:44 -07:00
if model . again {
CardActions ( model : model , focus : $ focus )
} else if model . canAnswerAgain {
Button ( "Answer again" ) {
model . again = true
}
. buttonStyle ( WebButtonStyle ())
. help ( "Fork this task from the snapshot taken just before this answer reached the agent, and give the fork a different answer; this task carries on untouched" )
}
2026-10-19 10:18:21 -07:00
}
}
. frame ( maxWidth : . infinity , alignment : . leading )
. onAppear {
if model == nil {
model = CardModel ( item , store , overlays )
}
}
}
}
2026-10-17 07:26:18 -07:00
private struct CardBody : View {
let model : CardModel
var focus : FocusState < String ? > . Binding
2026-10-19 10:18:21 -07:00
var question = true
2026-10-17 10:54:06 -07:00
@ ViewState private var wide = true
private var contextCtx : MDContext {
MDContext ( taskId : model . item . taskId , choice : { md , first in
AnyView ( ChoiceBox ( model : model , item : md , first : first , ctx : self . contextCtx , focus : focus ))
}, gridColumns : wide ? 4 : 2 )
}
2026-10-17 07:26:18 -07:00
var body : some View {
let p = model . payload
let item = model . item
VStack ( alignment : . leading , spacing : 0 ) {
2026-10-18 15:06:06 -07:00
MarkdownView ( blocks : model . context , ctx : contextCtx )
. onGeometryChange ( for : Bool . self ) { $0 . size . width > 480 } action : { wide = $0 }
2026-10-17 07:26:18 -07:00
. padding (. vertical , 10 )
2026-10-19 10:18:21 -07:00
if question {
MarkdownView ( blocks : parseMarkdown ( jsString ( p [ "question" ] ?? "" )), ctx : MDContext ( taskId : item . taskId ))
. padding (. top , 10 )
. padding (. bottom , 4 )
}
2026-10-18 15:06:06 -07:00
WhyLine ( text : jsString ( p [ "why_blocked" ] ?? "" ), taskId : item . taskId )
. padding (. bottom , 8 )
Attachments ( list : attachments ( p [ "attachments" ] ?? nil ), taskId : item . taskId )
2026-10-17 07:26:18 -07:00
}
}
private func attachments ( _ v : ( any Sendable )?) -> [ Attachment ] {
guard let v else {
return []
}
do {
return try jsonDecoder (). decode ([ Attachment ]. self , from : try JSONEncoder (). encode ( try OpenAPIValueContainer ( unvalidatedValue : v )))
} catch {
log . error ( "decode attachments: \( String ( describing : error ) , privacy : . public ) " )
return []
}
}
}
private struct WhyLine : View {
let text : String
let taskId : String
@ Environment ( Store . self ) private var store
var body : some View {
var out = AttributedString ( "Blocked on: " )
out . foregroundColor = Palette . text
var rest = InlineRenderer ( store : store , size : 13 , taskId : taskId ). text ( parseInline ( text ))
rest . foregroundColor = rest . foregroundColor ?? Palette . muted
return Text ( out + rest )
. foregroundStyle ( Palette . muted )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
}
}
private struct ChoiceBox : View {
let model : CardModel
let item : MDItem
2026-10-17 10:54:06 -07:00
let first : Bool
let ctx : MDContext
2026-10-17 07:26:18 -07:00
var focus : FocusState < String ? > . Binding
@ Environment ( Store . self ) private var store
@ ViewState private var hover = false
var body : some View {
let text = item . choice ?? ""
if model . options . contains ( text ) && model . firstChoice [ text ] == item . choiceIndex {
box ( text )
} else {
HStack ( alignment : . firstTextBaseline , spacing : 6 ) {
Text ( "•" )
VStack ( alignment : . leading , spacing : 6 ) {
InlineView ( inlines : parseInline ( item . raw ), ctx : MDContext ( taskId : model . item . taskId ))
2026-10-17 10:54:06 -07:00
ItemBody ( item : item , ctx : ctx )
2026-10-17 07:26:18 -07:00
}
}
}
}
private func box ( _ text : String ) -> some View {
let rec = model . recommended . contains ( text )
let focused = focus . wrappedValue == text
2026-10-18 15:06:06 -07:00
let picked = model . answered && model . chosen . contains ( text )
let lit = ( hover && ! model . multiple && ! model . answered ) || focused || picked
2026-10-17 07:26:18 -07:00
let hasDetail = ! item . inline . isEmpty || ! item . blocks . isEmpty
let on = model . checked . contains ( text )
2026-10-17 10:54:06 -07:00
let keys : Set < KeyEquivalent > = model . multiple ? [. space ] : [. return , . space ]
2026-10-17 07:26:18 -07:00
return VStack ( alignment : . leading , spacing : 0 ) {
2026-10-19 17:40:07 -07:00
HStack ( alignment : model . multiple ? . center : . firstTextBaseline , spacing : 10 ) {
2026-10-17 07:26:18 -07:00
if model . multiple {
Toggle ( "" , isOn : Binding ( get : { on }, set : { model . toggle ( text , $0 ) }))
. toggleStyle (. checkbox )
. labelsHidden ()
. tint ( Palette . accent )
2026-10-18 15:06:06 -07:00
. disabled ( model . busy || model . answered )
2026-10-17 07:26:18 -07:00
}
2026-10-17 10:54:06 -07:00
InlineView ( inlines : parseInline ( text ), ctx : MDContext ( taskId : model . item . taskId , weight : 600 ))
2026-10-17 07:26:18 -07:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-10-18 15:06:06 -07:00
if picked {
Text ( "chosen" )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . accent )
. allowsHitTesting ( false )
}
2026-10-17 07:26:18 -07:00
if rec {
2026-10-17 10:54:06 -07:00
Text ( "recommended" )
. font ( Fonts . font ( 11 ))
. foregroundStyle ( Palette . ok )
. allowsHitTesting ( false )
2026-10-17 07:26:18 -07:00
}
}
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
2026-10-17 10:54:06 -07:00
. background {
( lit ? Palette . accentTint : rec ? Palette . ok . opacity ( 0.12 ) : Palette . surface )
. onTapGesture {
model . pick ( text )
}
}
2026-10-17 07:26:18 -07:00
if hasDetail {
2026-10-17 10:54:06 -07:00
ItemBody ( item : MDItem ( inline : item . inline , blocks : item . blocks ), ctx : ctx )
2026-10-17 07:26:18 -07:00
. padding (. horizontal , 10 )
. padding (. vertical , 8 )
. overlay ( alignment : . top ) {
Rectangle (). fill ( Palette . border ). frame ( height : 1 )
}
}
}
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : . topLeading )
2026-10-17 10:54:06 -07:00
. background {
Palette . raised
. onTapGesture {
model . pick ( text )
}
}
. environment ( \ . choiceTap , { [ model ] in
model . pick ( text )
})
2026-10-17 07:26:18 -07:00
. clipShape ( RoundedRectangle ( cornerRadius : 6 ))
. overlay ( RoundedRectangle ( cornerRadius : 6 ). stroke ( lit ? Palette . accent : rec ? Palette . ok . opacity ( 0.55 ) : Palette . border ))
. overlay {
if focused {
RoundedRectangle ( cornerRadius : 6 ). stroke ( Palette . accent , lineWidth : 2 ). padding ( - 2 )
}
}
. opacity ( model . busy && ! model . multiple ? 0.6 : 1 )
. contentShape ( Rectangle ())
2026-10-18 15:06:06 -07:00
. focusable ( ! model . busy && ! model . answered )
2026-10-17 07:26:18 -07:00
. focusEffectDisabled ()
. focused ( focus , equals : text )
2026-10-17 10:54:06 -07:00
. onKeyPress ( keys : keys ) { _ in
model . pick ( text )
2026-10-17 07:26:18 -07:00
return . handled
}
. onHover { hover = $0 }
2026-10-17 10:54:06 -07:00
. onAppear {
model . shown . insert ( text )
2026-10-17 07:26:18 -07:00
}
2026-10-17 10:54:06 -07:00
. onDisappear {
model . shown . remove ( text )
2026-10-17 07:26:18 -07:00
}
2026-10-17 10:54:06 -07:00
. padding (. top , first ? 0 : 6 )
2026-10-17 07:26:18 -07:00
}
}
private struct CardActions : View {
let model : CardModel
var focus : FocusState < String ? > . Binding
var body : some View {
2026-10-18 15:06:06 -07:00
let disabled = model . typed . isEmpty && ! model . hasImages && ( ! model . multiple || model . checked . isEmpty )
VStack ( alignment : . leading , spacing : 6 ) {
TextEntry ( model : model , focus : focus ) {
model . submitQuestion ()
2026-10-17 07:26:18 -07:00
}
2026-10-18 15:06:06 -07:00
HStack ( spacing : 8 ) {
Button ( "Let the agent decide" ) {
model . submit ( InboxResponseCreate ( itemId : model . item . id , _type : . other , text : delegateText ))
2026-10-17 07:26:18 -07:00
}
. buttonStyle ( WebButtonStyle ())
2026-10-19 17:46:44 -07:00
if model . again {
Button ( "Cancel" ) {
model . again = false
}
. buttonStyle ( WebButtonStyle ())
} else {
Button ( "Ignore" ) {
model . dismiss ()
}
. buttonStyle ( WebButtonStyle ())
. help ( "Dismiss this question without answering it; the agent is not told" )
2026-10-19 16:36:46 -07:00
}
2026-10-18 15:06:06 -07:00
Spacer ()
Button ( "Submit" ) {
model . submitQuestion ()
}
. buttonStyle ( WebButtonStyle ( primary : true ))
. disabled ( disabled )
2026-10-17 07:26:18 -07:00
}
2026-10-18 15:06:06 -07:00
. disabled ( model . busy )
2026-10-17 07:26:18 -07:00
}
}
}
private struct TextEntry : View {
let model : CardModel
var focus : FocusState < String ? > . Binding
let go : () -> Void
@ Environment ( Store . self ) private var store
@ Environment ( Overlays . self ) private var overlays
@ Environment ( \ . paneAccent ) private var accent
@ FocusState private var editing : Bool
@ ViewState private var hover = false
@ ViewState private var targeted = false
var body : some View {
let id = model . item . id
let key = model . key
let text = Binding ( get : { store . drafts ?. textAnswer ( id ) ?? "" }, set : { v in
store . drafts ?. setTextAnswer ( id , v )
store . paneFocus . setDraft ( key , ! trimmed ( v ). isEmpty )
})
let focused = focus . wrappedValue == otherStop
Group {
if model . editorOpen {
VStack ( alignment : . leading , spacing : 6 ) {
Thumbnails ( key : key )
TextEditor ( text : text )
. font ( Fonts . font ( 13 ))
. scrollContentBackground (. hidden )
2026-10-19 10:30:03 -07:00
. writingToolsBehavior (. disabled )
2026-10-17 07:26:18 -07:00
. focused ( $ editing )
. disabled ( model . busy )
. focusedValue ( \ . composer , ComposerTarget ( paste : { pb in store . images . add ( key , pasteboard : pb , overlays ) }))
. onKeyPress (. return , phases : . down ) { press in
let marked = ( NSApp . keyWindow ?. firstResponder as ? NSTextView )?. hasMarkedText () ?? false
if press . modifiers . contains (. shift ) || marked {
return . ignored
}
go ()
return . handled
}
. padding (. horizontal , 5 )
. padding (. vertical , 6 )
. frame ( height : 66 )
. background ( Palette . raised , in : RoundedRectangle ( cornerRadius : 6 ))
. overlay ( alignment : . topLeading ) {
if text . wrappedValue . isEmpty {
2026-10-19 16:35:09 -07:00
Text ( model . multiple ? "Your answer, a question back, or new instructions" : "Your answer, or notes to send with a choice you click" )
2026-10-17 07:26:18 -07:00
. foregroundStyle ( Palette . muted )
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
. allowsHitTesting ( false )
}
}
. overlay ( RoundedRectangle ( cornerRadius : 6 ). stroke ( targeted || editing ? Palette . accent : Palette . border ))
. overlay {
Color . clear
. contentShape ( Rectangle ())
. allowsHitTesting ( false )
. onDrop ( of : [. image , . fileURL ], isTargeted : $ targeted ) { providers in
store . images . add ( key , providers : providers , overlays )
return true
}
}
}
. onChange ( of : editing ) {
if ! editing {
if trimmed ( text . wrappedValue ). isEmpty && ! model . hasImages && ! model . busy {
store . drafts ?. dropTextAnswer ( id )
}
store . drafts ?. flush ()
}
}
} else {
2026-10-18 15:06:06 -07:00
Text ( "Other…" )
2026-10-17 07:26:18 -07:00
. foregroundStyle ( Palette . muted )
. padding (. horizontal , 10 )
. padding (. vertical , 4 )
. frame ( maxWidth : . infinity , alignment : . leading )
. overlay ( RoundedRectangle ( cornerRadius : 6 ). stroke ( hover || focused ? Palette . accent : Palette . border ))
. overlay {
if focused {
RoundedRectangle ( cornerRadius : 6 ). stroke ( Palette . accent , lineWidth : 2 ). padding ( - 2 )
}
}
. opacity ( model . busy ? 0.6 : 1 )
. contentShape ( Rectangle ())
. focusable ( ! model . busy )
. focusEffectDisabled ()
. focused ( focus , equals : otherStop )
. onTapGesture {
open ()
}
. onKeyPress ( keys : [. return , . space ]) { _ in
open ()
return . handled
}
. onHover { hover = $0 }
}
}
. onDisappear {
store . paneFocus . setDraft ( key , false )
}
}
private func open () {
if model . busy {
return
}
store . drafts ?. setTextAnswer ( model . item . id , "" )
DispatchQueue . main . async {
editing = true
}
}
}
private struct Thread : View {
let item : InboxItem
var body : some View {
if ! item . responses . isEmpty {
VStack ( alignment : . leading , spacing : 2 ) {
ForEach ( item . responses , id : \ . id ) { r in
2026-10-17 23:45:31 -07:00
let images = r . images . isEmpty ? "" : " ( \( r . images . count == 1 ? "1 image" : " \( r . images . count ) images" ) )"
let rest = " \( r . _type . rawValue . replacingOccurrences ( of : "_" , with : " " ))\( r . options . map { ": \( $0 . joined ( separator : "; " )) " } ?? "" )\( r . text . map { " — \( $0 ) " } ?? "" )\( images ) "
Text ( " \( Text ( r . user ) . font ( Fonts . font ( 11 , weight : 700 )))\( rest ) " )
. textSelection (. enabled )
. fixedSize ( horizontal : false , vertical : true )
2026-10-17 07:26:18 -07:00
}
}
. font ( Fonts . font ( 11 ))
. padding (. horizontal , 10 )
. padding (. vertical , 6 )
. frame ( maxWidth : . infinity , alignment : . leading )
. background ( Palette . surface , in : RoundedRectangle ( cornerRadius : 6 ))
. padding (. vertical , 8 )
}
}
}
private struct Attachments : View {
let list : [ Attachment ]
let taskId : String
@ Environment ( Store . self ) private var store
var body : some View {
if ! list . isEmpty {
VStack ( alignment : . leading , spacing : 4 ) {
ForEach ( Array ( list . enumerated ()), id : \ . offset ) { _ , a in
HStack ( alignment : . firstTextBaseline , spacing : 6 ) {
Text ( "•" )
VStack ( alignment : . leading , spacing : 2 ) {
content ( a )
if let caption = a . caption , ! caption . isEmpty {
Text ( caption ). font ( Fonts . font ( 11 )). foregroundStyle ( Palette . muted )
}
}
}
}
}
. padding (. leading , 6 )
. padding (. vertical , 6 )
}
}
private func code ( _ s : String ) -> Text {
2026-10-17 10:54:06 -07:00
Text ( verbatim : s )
2026-10-17 07:26:18 -07:00
}
@ ViewBuilder
private func content ( _ a : Attachment ) -> some View {
switch a . kind {
case . file :
let lines = a . startLine . map { ": \( $0 )\( a . endLine . map { "- \( $0 ) " } ?? "" ) " } ?? ""
code ( " \( a . path ?? "" )\( lines ) " ). textSelection (. enabled )
case . diff :
Text ( "diff \( code ( a . ref ?? "" )) " ). textSelection (. enabled )
case . image :
let src = ( a . image ?. isEmpty == false ? a . image : a . path ) ?? ""
2026-10-17 10:54:06 -07:00
let caption = a . caption ?? ""
2026-10-17 07:26:18 -07:00
if let source = worktreeImageSource ( src , taskId ) {
ByreImage ( source : source , src : src )
2026-10-17 10:54:06 -07:00
} else if ! src . isEmpty {
InlineView ( inlines : [. link ( href : src , [. text ( caption . isEmpty ? src : caption )])], ctx : MDContext ( taskId : taskId ))
2026-10-17 07:26:18 -07:00
}
case . diagram :
DiagramView ( source : a . mermaid ?? "" )
case . task :
let id = a . taskId ?? ""
Button ( "task \( id ) " ) {
2026-10-17 10:54:06 -07:00
store . route ( "#/task/ \( id ) " )
2026-10-17 07:26:18 -07:00
}
. buttonStyle (. plain )
. foregroundStyle ( store . taskColor ( id ) ?? Palette . accent )
case . snapshot :
Text ( "snapshot \( code ( a . snapshotId ?? "" )) " ). textSelection (. enabled )
case . url :
let url = a . url ?? ""
if httpURL . test ( url ), let u = URL ( string : url ) {
Link ( url , destination : u ). foregroundStyle ( Palette . accent )
} else {
code ( url ). textSelection (. enabled )
}
}
}
}