import AppKit import Foundation import Network import Observation import SwiftUI import WebKit enum Page: Equatable { case file(taskId: String, path: String) case preview(taskId: String, port: Int) var taskId: String { switch self { case .file(let taskId, _), .preview(let taskId, _): taskId } } @MainActor func title(_ store: Store) -> String { switch self { case .file(_, let path): (path as NSString).lastPathComponent case .preview(let taskId, let port): store.forwards.name(taskId, port) ?? "port \(port)" } } @MainActor func url(_ store: Store) -> URL? { switch self { case .file(let taskId, let path): store.pages.url(taskId, path) case .preview(let taskId, let port): store.forwards.url(taskId, port) } } } private let pageExts: Set = ["html", "htm"] func isPage(_ path: String) -> Bool { pageExts.contains((path as NSString).pathExtension.lowercased()) } extension Store { func openPage(_ page: Page) { sheet = Sheet(owner: .task(page.taskId), view: .page(page)) } } @MainActor @Observable final class Pages { private(set) var port: Int? @ObservationIgnored var served: [String: [String: Data]] = [:] @ObservationIgnored private var listener: PageListener? init(api: API) { listener = PageListener(api: api, ready: { [weak self] port in Task { @MainActor in self?.port = port } }, served: { [weak self] taskId, path, body in Task { @MainActor in self?.served[taskId, default: [:]][path] = body } }) } func url(_ taskId: String, _ path: String) -> URL? { guard let port else { return nil } var c = URLComponents() c.scheme = "http" c.host = "127.0.0.1" c.port = port c.path = "/\(taskId)/\(path)" return c.url } func close() { listener?.close() listener = nil port = nil } static func fetch(_ api: API, _ taskId: String, _ path: String) async -> (status: Int, type: String, body: Data) { do { let (body, response) = try await api.session.data(from: api.url("/api/tasks/\(taskId)/raw", [URLQueryItem(name: "path", value: path)])) let http = response as! HTTPURLResponse return (http.statusCode, http.value(forHTTPHeaderField: "Content-Type") ?? "application/octet-stream", body) } catch { return (502, "text/plain; charset=utf-8", Data(errorText(error).utf8)) } } } private let headEnd = Data("\r\n\r\n".utf8) private let maxHead = 64 << 10 private final class PageListener: @unchecked Sendable { private let api: API private let queue = DispatchQueue(label: "run.fc.byre.pages") private let served: @Sendable (String, String, Data) -> Void private var listener: NWListener? init(api: API, ready: @escaping @Sendable (Int) -> Void, served: @escaping @Sendable (String, String, Data) -> Void) { self.api = api self.served = served let params = NWParameters.tcp params.requiredLocalEndpoint = .hostPort(host: .ipv4(.loopback), port: .any) let listener: NWListener do { listener = try NWListener(using: params) } catch { log.error("page listener: \(String(describing: error), privacy: .public)") return } self.listener = listener listener.stateUpdateHandler = { [weak listener] state in switch state { case .ready: if let port = listener?.port { ready(Int(port.rawValue)) } case .failed(let error): log.error("page listener: \(String(describing: error), privacy: .public)") default: break } } listener.newConnectionHandler = { [weak self] conn in guard let self else { conn.cancel() return } conn.start(queue: self.queue) self.read(conn, Data()) } listener.start(queue: queue) } func close() { queue.async { self.listener?.newConnectionHandler = nil self.listener?.cancel() self.listener = nil } } private func read(_ conn: NWConnection, _ buf: Data) { conn.receive(minimumIncompleteLength: 1, maximumLength: maxHead) { data, _, complete, error in var buf = buf if let data { buf.append(data) } if let end = buf.range(of: headEnd) { self.serve(conn, String(decoding: buf[.. maxHead { conn.cancel() return } self.read(conn, buf) } } private func serve(_ conn: NWConnection, _ head: String) { let lines = head.components(separatedBy: "\r\n") let hosts = lines.dropFirst().compactMap { line -> String? in guard let colon = line.firstIndex(of: ":"), line[..? func check(_ api: API, _ pages: Pages, _ taskId: String) { timer?.cancel() timer = Task { try? await Task.sleep(for: .milliseconds(1500)) var changed = false for (path, shown) in pages.served[taskId] ?? [:] { let fresh = await Pages.fetch(api, taskId, path).body if Task.isCancelled { return } if fresh != shown { pages.served[taskId]?[path] = fresh changed = true } } if changed { reloads += 1 } } } func stop() { timer?.cancel() timer = nil } } struct PageView: View { let page: Page @Environment(Store.self) private var store @ViewState private var watch = PageWatch() var body: some View { Group { if let url = page.url(store) { WebPage(url: url, reloads: watch.reloads) } else { EmptyText(text: "Loading…") } } .task(id: page) { guard case .file(let taskId, _) = page else { return } let watch = watch let api = store.api let pages = store.pages let off = store.selectedFeed?.appended.add { events in if events.contains(where: { $0.kind == "agent.tool_result" || $0.kind == "turn.ended" }) { watch.check(api, pages, taskId) } } await untilCancelled() off?() watch.stop() } } } private struct WebPage: NSViewRepresentable { let url: URL let reloads: Int final class Coordinator: NSObject, WKUIDelegate { var url: URL? var reloads = 0 func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for action: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if let url = action.request.url { NSWorkspace.shared.open(url) } return nil } } func makeCoordinator() -> Coordinator { Coordinator() } func makeNSView(context: Context) -> WKWebView { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() let view = WKWebView(frame: .zero, configuration: config) view.uiDelegate = context.coordinator return view } func updateNSView(_ view: WKWebView, context: Context) { let c = context.coordinator if c.url != url { c.url = url c.reloads = reloads view.load(URLRequest(url: url)) return } if c.reloads != reloads { c.reloads = reloads view.reload() } } }