Add tabs to the page view for comparing pages

This commit is contained in:
Ian Gulliver
2026-10-21 19:41:07 -07:00
parent b7c762757e
commit f57741f33d
4 changed files with 100 additions and 17 deletions
+96 -13
View File
@@ -5,7 +5,7 @@ import Observation
import SwiftUI import SwiftUI
import WebKit import WebKit
enum Page: Equatable { enum Page: Hashable {
case file(taskId: String, path: String) case file(taskId: String, path: String)
case preview(taskId: String, port: Int) case preview(taskId: String, port: Int)
@@ -15,6 +15,13 @@ enum Page: Equatable {
} }
} }
var isFile: Bool {
if case .file = self {
return true
}
return false
}
@MainActor @MainActor
func title(_ store: Store) -> String { func title(_ store: Store) -> String {
switch self { switch self {
@@ -38,11 +45,43 @@ func isPage(_ path: String) -> Bool {
pageExts.contains((path as NSString).pathExtension.lowercased()) pageExts.contains((path as NSString).pathExtension.lowercased())
} }
struct ShownPages {
var pages: [Page]
var current: Page
}
extension Store { extension Store {
func openPage(_ page: Page) { func openPage(_ page: Page) {
shownPages[page.taskId] = page var shown = shownPages[page.taskId] ?? ShownPages(pages: [], current: page)
if !shown.pages.contains(page) {
shown.pages.append(page)
}
shown.current = page
shownPages[page.taskId] = shown
sheet = Sheet(owner: .task(page.taskId), view: .tab(.page)) sheet = Sheet(owner: .task(page.taskId), view: .tab(.page))
} }
func closePage(_ page: Page) {
guard var shown = shownPages[page.taskId], let i = shown.pages.firstIndex(of: page) else {
return
}
shown.pages.remove(at: i)
if shown.pages.isEmpty {
shownPages[page.taskId] = nil
if sheet?.tab == .page {
sheet = nil
}
return
}
if shown.current == page {
shown.current = shown.pages[min(i, shown.pages.count - 1)]
}
shownPages[page.taskId] = shown
}
func currentPage() -> Page? {
selectedTaskId.flatMap { shownPages[$0]?.current }
}
} }
@MainActor @MainActor
@@ -239,23 +278,37 @@ private final class PageWatch {
} }
} }
struct PageView: View { struct PageTabs: View {
let page: Page let shown: ShownPages
@Environment(Store.self) private var store @Environment(Store.self) private var store
@ViewState private var watch = PageWatch() @ViewState private var watch = PageWatch()
var body: some View { var body: some View {
Group { VStack(spacing: 0) {
if let url = page.url(store) { ScrollView(.horizontal, showsIndicators: false) {
WebPage(url: url, reloads: watch.reloads) HStack(spacing: 4) {
} else { ForEach(shown.pages, id: \.self) { page in
EmptyText(text: "Loading…") PageTabButton(page: page, active: page == shown.current)
}
}
.padding(.horizontal, 12)
}
.fixedSize(horizontal: false, vertical: true)
.overlay(alignment: .bottom) {
Rectangle().fill(Palette.border).frame(height: 1)
}
ZStack {
ForEach(shown.pages, id: \.self) { page in
if let url = page.url(store) {
WebPage(url: url, reloads: page.isFile ? watch.reloads : 0, shown: page == shown.current)
} else if page == shown.current {
EmptyText(text: "Loading…")
}
}
} }
} }
.task(id: page) { .task(id: shown.current.taskId) {
guard case .file(let taskId, _) = page else { let taskId = shown.current.taskId
return
}
let watch = watch let watch = watch
let api = store.api let api = store.api
let pages = store.pages let pages = store.pages
@@ -271,9 +324,38 @@ struct PageView: View {
} }
} }
private struct PageTabButton: View {
let page: Page
let active: Bool
@Environment(Store.self) private var store
@ViewState private var hover = false
var body: some View {
HStack(spacing: 0) {
PaneTabButton(label: page.title(store), active: active) {
store.openPage(page)
}
Button {
store.closePage(page)
} label: {
Text("✕")
.font(Fonts.font(11))
.foregroundStyle(hover ? Palette.text : Palette.muted)
.padding(.trailing, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.help("Close this page")
.onHover { hover = $0 }
}
.help(page.url(store)?.absoluteString ?? "")
}
}
private struct WebPage: NSViewRepresentable { private struct WebPage: NSViewRepresentable {
let url: URL let url: URL
let reloads: Int let reloads: Int
let shown: Bool
final class Coordinator: NSObject, WKUIDelegate { final class Coordinator: NSObject, WKUIDelegate {
var url: URL? var url: URL?
@@ -300,6 +382,7 @@ private struct WebPage: NSViewRepresentable {
} }
func updateNSView(_ view: WKWebView, context: Context) { func updateNSView(_ view: WKWebView, context: Context) {
view.isHidden = !shown
let c = context.coordinator let c = context.coordinator
if c.url != url { if c.url != url {
c.url = url c.url = url
+2 -2
View File
@@ -232,8 +232,8 @@ struct TaskTabBody: View {
case .files: case .files:
FilesView() FilesView()
case .page: case .page:
if let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }) { if let shown = store.selectedTaskId.flatMap({ store.shownPages[$0] }) {
PageView(page: page) PageTabs(shown: shown)
} }
case .terminal: case .terminal:
TerminalTab() TerminalTab()
+1 -1
View File
@@ -81,7 +81,7 @@ private struct SheetPanel: View {
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
Spacer(minLength: 0) Spacer(minLength: 0)
if sheet.tab == .page, let page = store.selectedTaskId.flatMap({ store.shownPages[$0] }), let url = page.url(store) { if sheet.tab == .page, let url = store.currentPage()?.url(store) {
Button { Button {
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} label: { } label: {
+1 -1
View File
@@ -71,7 +71,7 @@ final class Store {
var connected = false var connected = false
private(set) var focus = Focus() private(set) var focus = Focus()
var sheet: Sheet? var sheet: Sheet?
var shownPages: [String: Page] = [:] var shownPages: [String: ShownPages] = [:]
var leadSessions: [AgentSession] = [] var leadSessions: [AgentSession] = []
let leadFeed: Feed let leadFeed: Feed
var jump: Jump? var jump: Jump?