Simplify page view file loading to plain relative paths

This commit is contained in:
Ian Gulliver
2026-10-23 06:55:15 -07:00
parent c6da60f728
commit 5b69d2c639
6 changed files with 466 additions and 461 deletions
+35 -47
View File
@@ -34,8 +34,7 @@ enum Page: Hashable {
@MainActor
func url(_ store: Store) -> URL? {
switch self {
case .file(let taskId, let path):
path.hasPrefix("/") ? store.pages.url(path) : store.findTask(taskId).flatMap { store.pages.url("\($0.worktreePath)/\(path)") }
case .file(let taskId, let path): store.pages.url(taskId, path)
case .preview(let taskId, let port): store.forwards.url(taskId, port)
case .web: store.pages.existing(self)?.url
}
@@ -140,7 +139,7 @@ extension Store {
final class Pages {
private(set) var port: Int?
private(set) var titles: [Page: String] = [:]
@ObservationIgnored var served: [String: Data] = [:]
@ObservationIgnored var served: [String: [String: Data]] = [:]
@ObservationIgnored var popup: ((Page) -> Void)?
@ObservationIgnored private var listener: PageListener?
@ObservationIgnored private var views: [Page: WKWebView] = [:]
@@ -158,14 +157,14 @@ final class Pages {
Task { @MainActor in
self?.port = port
}
}, served: { [weak self] path, body in
}, served: { [weak self] taskId, path, body in
Task { @MainActor in
self?.served[path] = body
self?.served[taskId, default: [:]][path] = body
}
})
}
func url(_ path: String) -> URL? {
func url(_ taskId: String, _ path: String) -> URL? {
guard let port else {
return nil
}
@@ -173,7 +172,7 @@ final class Pages {
c.scheme = "http"
c.host = "127.0.0.1"
c.port = port
c.path = path
c.path = "/\(taskId)/\(path)"
return c.url
}
@@ -254,16 +253,6 @@ final class Pages {
return "Loaded \(view.url?.absoluteString ?? url.absoluteString): \(view.title ?? "")"
}
func loaded(_ view: WKWebView) async -> Set<String> {
let urls = (try? await view.callAsyncJavaScript("return [location.href, ...performance.getEntriesByType('resource').map(e => e.name)]", contentWorld: .defaultClient)) as? [String] ?? []
return Set(urls.compactMap { s -> String? in
guard let c = URLComponents(string: s), c.host == "127.0.0.1", c.port == port else {
return nil
}
return c.path.hasSuffix("/") ? "\(c.path)index.html" : c.path
})
}
func js(_ view: WKWebView, _ code: String) async throws -> String {
let value = try await view.callAsyncJavaScript(code, contentWorld: .page)
guard let value, !(value is NSNull) else {
@@ -308,9 +297,9 @@ final class Pages {
port = nil
}
static func fetch(_ api: API, _ path: String) async -> (status: Int, type: String, body: Data) {
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/raw", [URLQueryItem(name: "path", value: path)]))
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 {
@@ -326,12 +315,12 @@ private final class PageListener: @unchecked Sendable {
private let api: API
private let queue = DispatchQueue(label: "run.fc.byre.pages")
private let ready: @Sendable (Int) -> Void
private let served: @Sendable (String, Data) -> Void
private let served: @Sendable (String, String, Data) -> Void
private var listener: NWListener?
private var port: UInt16 = 0
private var closed = false
init(api: API, ready: @escaping @Sendable (Int) -> Void, served: @escaping @Sendable (String, Data) -> Void) {
init(api: API, ready: @escaping @Sendable (Int) -> Void, served: @escaping @Sendable (String, String, Data) -> Void) {
self.api = api
self.ready = ready
self.served = served
@@ -451,14 +440,18 @@ private final class PageListener: @unchecked Sendable {
respond(conn, 400, "text/plain; charset=utf-8", Data("bad request".utf8))
return
}
let file = path.hasSuffix("/") ? "\(path)index.html" : path
let route = path.drop(while: { $0 == "/" }).split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false)
guard route.count == 2, !route[1].isEmpty else {
respond(conn, 404, "text/plain; charset=utf-8", Data("not found".utf8))
return
}
let taskId = String(route[0])
let file = route[1].hasSuffix("/") ? "\(route[1])index.html" : String(route[1])
let api = api
Task {
let (status, type, body) = await Pages.fetch(api, file)
self.served(file, body)
// Agents write file:///abs/path references, which a page served over http can't load; /abs/path serves the same file here.
let shown = type.hasPrefix("text/html") || type.hasPrefix("text/css") ? body.replacing(Data("file://".utf8), with: Data()) : body
self.respond(conn, status, type, shown)
let (status, type, body) = await Pages.fetch(api, taskId, file)
self.served(taskId, file, body)
self.respond(conn, status, type, body)
}
}
@@ -473,30 +466,26 @@ private final class PageListener: @unchecked Sendable {
@MainActor
@Observable
private final class PageWatch {
private(set) var reloads: [Page: Int] = [:]
private(set) var reloads = 0
@ObservationIgnored private var timer: Task<Void, Never>?
func check(_ api: API, _ pages: Pages, _ views: [Page: WKWebView]) {
func check(_ api: API, _ pages: Pages, _ taskId: String) {
timer?.cancel()
timer = Task {
try? await Task.sleep(for: .milliseconds(1500))
var loaded: [Page: Set<String>] = [:]
for (page, view) in views {
loaded[page] = await pages.loaded(view)
}
var fresh: [String: Data] = [:]
for path in loaded.values.reduce(Set(), { $0.union($1) }) {
fresh[path] = await Pages.fetch(api, path).body
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
}
}
let changed = Set(fresh.filter { pages.served[$0.key] != $0.value }.keys)
for (path, body) in fresh {
pages.served[path] = body
}
for (page, paths) in loaded where !paths.isDisjoint(with: changed) {
reloads[page, default: 0] += 1
if changed {
reloads += 1
}
}
}
@@ -529,7 +518,7 @@ struct PageTabs: View {
ZStack {
ForEach(shown.pages, id: \.self) { page in
if let view = store.pageView(page) {
WebPage(view: view, reloads: watch.reloads[page] ?? 0, shown: page == shown.current, refresh: page.isFile)
WebPage(view: view, reloads: page.isFile ? watch.reloads : 0, shown: page == shown.current, refresh: page.isFile)
} else if page == shown.current {
EmptyText(text: "Loading…")
}
@@ -539,12 +528,11 @@ struct PageTabs: View {
.task(id: shown.current.taskId) {
let taskId = shown.current.taskId
let watch = watch
let store = store
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" }) {
let files = (store.shownPages[taskId]?.pages ?? []).filter(\.isFile)
let views = Dictionary(uniqueKeysWithValues: files.compactMap { page in store.pages.existing(page).map { (page, $0) } })
watch.check(store.api, store.pages, views)
watch.check(api, pages, taskId)
}
}
await untilCancelled()