import SwiftUI enum MDSegment { case text(AttributedString) case image(ImageSource, src: String) } enum AppLink { static func task(_ id: String) -> URL { var c = URLComponents() c.scheme = "byre-app" c.host = "task" c.queryItems = [URLQueryItem(name: "id", value: id)] return c.url! } static func todo(_ projectId: String, _ title: String, discuss: Bool) -> URL { var c = URLComponents() c.scheme = "byre-app" c.host = "todo" c.queryItems = [URLQueryItem(name: "project", value: projectId), URLQueryItem(name: "title", value: title), URLQueryItem(name: "discuss", value: discuss ? "1" : "0")] return c.url! } } @MainActor func openAppLink(_ url: URL, _ store: Store, _ overlays: Overlays) -> OpenURLAction.Result { guard url.scheme == "byre-app", let c = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return .systemAction } let q = Dictionary((c.queryItems ?? []).map { ($0.name, $0.value ?? "") }, uniquingKeysWith: { a, _ in a }) switch c.host { case "task": store.focusTask(q["id"] ?? "") case "todo": store.startTodo(q["project"] ?? "", q["title"] ?? "", discuss: q["discuss"] == "1", overlays) default: break } return .handled } private let httpLink = Re(#"^https?://"#, caseless: true) private let taskHref = Re(#"^#/task/(\w+)(/\w+)?$"#) private let todoHref = Re(#"^#/todo/(\w+)/(.+)$"#) @MainActor struct InlineRenderer { let store: Store let size: CGFloat let taskId: String? private struct Style { var bold = false var italic = false var link: URL? var color: Color? var small = false } func segments(_ items: [MDInline]) -> [MDSegment] { var out: [MDSegment] = [] render(items, Style(), &out) return out } func text(_ items: [MDInline]) -> AttributedString { var out = AttributedString() for seg in segments(items) { if case .text(let a) = seg { out += a } } return out } private func run(_ s: String, _ style: Style) -> AttributedString { var a = AttributedString(s) a.font = Fonts.font(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic) if let link = style.link { a.link = link } if let color = style.color { a.foregroundColor = color } return a } private func append(_ a: AttributedString, _ out: inout [MDSegment]) { if case .text(let prev) = out.last { out[out.count - 1] = .text(prev + a) return } out.append(.text(a)) } private func render(_ items: [MDInline], _ style: Style, _ out: inout [MDSegment]) { for item in items { switch item { case .text(let t): append(run(t, style), &out) case .lineBreak: append(run("\n", style), &out) case .code(let c): var a = run(c, style) a.backgroundColor = Palette.surface append(a, &out) case .strong(let inner): var s = style s.bold = true render(inner, s, &out) case .em(let inner): var s = style s.italic = true render(inner, s, &out) case .image(let src, let alt): if let source = worktreeImageSource(src, taskId) { out.append(.image(source, src: src)) } else { link(src, [.text(alt.isEmpty ? src : alt)], style, &out) } case .link(let href, let inner): link(href, inner, style, &out) } } } private func link(_ href: String, _ inner: [MDInline], _ style: Style, _ out: inout [MDSegment]) { var s = style if httpLink.test(href), let url = URL(string: href) { s.link = url s.color = Palette.accent render(inner, s, &out) return } if let m = taskHref.exec(href) { s.link = AppLink.task(m[1]!) s.color = store.taskColor(m[1]!) ?? Palette.accent render(inner, s, &out) return } if let m = todoHref.exec(href), let title = m[2]!.removingPercentEncoding { todo(m[1]!, title, inner, style, &out) return } render(inner, style, &out) } private func todo(_ projectId: String, _ title: String, _ inner: [MDInline], _ style: Style, _ out: inout [MDSegment]) { let task = (store.tasks[projectId] ?? []).first { $0.status == .active && $0.todoItem == title } var s = style if let task { s.color = taskPalette[task.color] render(inner, s, &out) append(run(" ", style), &out) let state = store.currentSession(task.id)?.state var dot = Style(link: AppLink.task(task.id), color: state == .running ? Palette.ok : Palette.muted, small: true) append(run(state == .running ? "●" : "○", dot), &out) dot.color = Palette.muted append(run(" \(state?.rawValue ?? "no agent")", dot), &out) return } render(inner, style, &out) append(run(" ", style), &out) if store.todoStarting.contains("\(projectId)\n\(title)") { append(run("starting…", Style(color: Palette.muted, small: true)), &out) return } append(run("start", Style(link: AppLink.todo(projectId, title, discuss: false), color: Palette.accent, small: true)), &out) append(run(" ", style), &out) append(run("discuss", Style(link: AppLink.todo(projectId, title, discuss: true), color: Palette.accent, small: true)), &out) } } struct MDContext { var size: CGFloat = 13 var color: Color = Palette.text var taskId: String? var choice: ((MDItem) -> AnyView)? } struct MarkdownView: View { let blocks: [MDBlock] var ctx = MDContext() var body: some View { VStack(alignment: .leading, spacing: 8) { ForEach(Array(blocks.enumerated()), id: \.offset) { _, block in BlockView(block: block, ctx: ctx) } } .frame(maxWidth: .infinity, alignment: .leading) } } struct InlineView: View { let inlines: [MDInline] let ctx: MDContext @Environment(Store.self) private var store var body: some View { let segs = InlineRenderer(store: store, size: ctx.size, taskId: ctx.taskId).segments(inlines) VStack(alignment: .leading, spacing: 4) { ForEach(Array(segs.enumerated()), id: \.offset) { _, seg in switch seg { case .text(let a): Text(a) .font(Fonts.font(ctx.size)) .foregroundStyle(ctx.color) .lineSpacing(3) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) case .image(let source, let src): ByreImage(source: source, src: src) } } } } } private struct BlockView: View { let block: MDBlock let ctx: MDContext var body: some View { switch block { case .paragraph(let inlines): InlineView(inlines: inlines, ctx: ctx) case .heading(let level, let inlines): InlineView(inlines: [.strong(inlines)], ctx: MDContext(size: level == 3 ? 14 : 13, color: ctx.color, taskId: ctx.taskId)) .padding(.top, 4) case .rule: Rectangle() .fill(Palette.border) .frame(height: 1) .padding(.vertical, 2) case .quote(let blocks): MarkdownView(blocks: blocks, ctx: MDContext(size: ctx.size, color: Palette.muted, taskId: ctx.taskId)) .padding(.leading, 13) .overlay(alignment: .leading) { Rectangle().fill(Palette.border).frame(width: 3) } case .list(let ordered, let start, let items, let grid): ListBlock(ordered: ordered, start: start, items: items, grid: grid, ctx: ctx) case .code(let code): Copyable(text: code) { ScrollView(.horizontal) { Text(verbatim: code) .font(Fonts.font(ctx.size)) .foregroundStyle(Palette.text) .textSelection(.enabled) .fixedSize() .padding(.horizontal, 8) .padding(.vertical, 6) } .scrollBounceBehavior(.basedOnSize) .frame(maxWidth: .infinity, alignment: .leading) .background(Palette.surface, in: RoundedRectangle(cornerRadius: 6)) .overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border)) } case .diff(let lines): DiffBlock(lines: lines, size: ctx.size) case .details(let summary, let body): DetailsBlock(summary: summary, blocks: body, ctx: ctx) case .diagram(let source): DiagramView(source: source) case .table(let head, let rows): TableBlock(head: head, rows: rows, ctx: ctx) } } } private struct ListBlock: View { let ordered: Bool let start: Int let items: [MDItem] let grid: Bool let ctx: MDContext var body: some View { if grid, let choice = ctx.choice { LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8, alignment: .top), count: 4), spacing: 8) { ForEach(Array(items.enumerated()), id: \.offset) { _, item in choice(item) } } } else { VStack(alignment: .leading, spacing: 2) { ForEach(Array(items.enumerated()), id: \.offset) { n, item in if item.choice != nil, let choice = ctx.choice { choice(item) } else { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(verbatim: ordered ? "\(start + n)." : "•") .font(Fonts.font(ctx.size)) .foregroundStyle(ctx.color) ItemBody(item: item, ctx: ctx) } } } } } } } struct ItemBody: View { let item: MDItem let ctx: MDContext var body: some View { VStack(alignment: .leading, spacing: 6) { if !item.inline.isEmpty { InlineView(inlines: item.inline, ctx: ctx) } if !item.blocks.isEmpty { MarkdownView(blocks: item.blocks, ctx: ctx) } } .frame(maxWidth: .infinity, alignment: .leading) } } private struct DiffBlock: View { let lines: [String] let size: CGFloat var body: some View { Copyable(text: lines.map { $0 + "\n" }.joined()) { VStack(alignment: .leading, spacing: 0) { ForEach(Array(lines.enumerated()), id: \.offset) { _, line in Text(verbatim: line.isEmpty ? " " : line) .font(Fonts.font(size)) .foregroundStyle(line.hasPrefix("@@") ? Palette.muted : Palette.text) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 8) .background(background(line)) } } .padding(.vertical, 6) .background(Palette.surface, in: RoundedRectangle(cornerRadius: 6)) .overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border)) } } private func background(_ l: String) -> Color { if l.hasPrefix("@@") { return Palette.surface } if l.hasPrefix("+++") || l.hasPrefix("---") { return .clear } if l.hasPrefix("+") { return Palette.ok.opacity(0.14) } if l.hasPrefix("-") { return Palette.danger.opacity(0.14) } return .clear } } private struct DetailsBlock: View { let summary: [MDInline] let blocks: [MDBlock] let ctx: MDContext @ViewState private var open = false var body: some View { VStack(alignment: .leading, spacing: 4) { Button { open.toggle() } label: { HStack(alignment: .firstTextBaseline, spacing: 4) { Text(open ? "▾" : "▸") .foregroundStyle(Palette.accent) InlineView(inlines: summary, ctx: MDContext(size: ctx.size, color: Palette.accent, taskId: ctx.taskId)) } .contentShape(Rectangle()) } .buttonStyle(.plain) if open { MarkdownView(blocks: blocks, ctx: ctx) .padding(.leading, 11) .overlay(alignment: .leading) { Rectangle().fill(Palette.border).frame(width: 1) } } } .padding(.vertical, 2) } } private struct TableBlock: View { let head: [[MDInline]] let rows: [[[MDInline]]] let ctx: MDContext var body: some View { Grid(alignment: .topLeading, horizontalSpacing: 0, verticalSpacing: 0) { GridRow { ForEach(Array(head.enumerated()), id: \.offset) { _, cell in cellView([.strong(cell)]) .background(Palette.surface) } } ForEach(Array(rows.enumerated()), id: \.offset) { _, row in GridRow { ForEach(Array(row.enumerated()), id: \.offset) { _, cell in cellView(cell) } } } } .overlay(Rectangle().stroke(Palette.border)) } private func cellView(_ cell: [MDInline]) -> some View { InlineView(inlines: cell, ctx: ctx) .padding(.horizontal, 8) .padding(.vertical, 3) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .overlay(Rectangle().stroke(Palette.border, lineWidth: 0.5)) } }