import ByreAPI import SwiftUI struct BoardView: View { @Environment(Store.self) private var store var body: some View { let taskId = store.selectedTaskId let posts = store.transcript.filter { $0.kind == "board.posted" } VStack(spacing: 0) { HStack(spacing: 10) { Text("Board").font(Fonts.font(13, weight: 700)) Text(taskId.map { store.team($0).map(\.member).joined(separator: ", ") } ?? "") .foregroundStyle(Palette.muted) .lineLimit(1) .truncationMode(.tail) } .modifier(HeaderBar()) if posts.isEmpty { EmptyText(text: taskId == nil ? "No task selected." : "No posts yet. Post from the Transcript tab's composer.") } else { ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 12) { ForEach(posts, id: \.seq) { ev in PostView(ev: ev, taskId: taskId) .id(ev.seq) } } .padding(.horizontal, 16) .padding(.vertical, 12) } .onAppear { proxy.scrollTo(posts.last?.seq, anchor: .bottom) } .onChange(of: posts.count) { proxy.scrollTo(posts.last?.seq, anchor: .bottom) } } } } .frame(maxHeight: .infinity, alignment: .top) } } private struct PostView: View { let ev: ByreEvent let taskId: String? var body: some View { let p = ev.payload.value let member = str(p, "member") ?? "" VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text(member.isEmpty ? ev.actor : member).font(Fonts.font(12, weight: 700)) if !member.isEmpty { Text(str(p, "model") ?? "").foregroundStyle(Palette.muted) } WhenText(when: ev.createdAt).foregroundStyle(Palette.muted) } .font(Fonts.font(11)) MarkdownView(blocks: parseMarkdown(str(p, "text") ?? ""), ctx: MDContext(taskId: taskId)) } .padding(.horizontal, 12) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) .background(member.isEmpty ? Palette.accentTint : Palette.raised, in: RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border)) } }