Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 46 additions & 46 deletions Sources/Bugbook/Services/BrowserAgentService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import WebKit

@MainActor
struct BrowserAgentService {
Expand All @@ -9,6 +8,20 @@ struct BrowserAgentService {
}

private let savedPageStore: SavedWebPageStore
private static let pageContentExtractionScript = """
(() => {
const title = document.title || '';
const text = (document.body && document.body.innerText ? document.body.innerText : '').trim();
return JSON.stringify({ title, text, url: location.href });
})()
"""
private static let pageSaveExtractionScript = """
(() => {
const title = document.title || '';
const text = (document.body && document.body.innerText ? document.body.innerText : '').trim().slice(0, 20000);
return JSON.stringify({ title, text, url: location.href });
})()
"""

init(savedPageStore: SavedWebPageStore = SavedWebPageStore()) {
self.savedPageStore = savedPageStore
Expand All @@ -29,16 +42,7 @@ struct BrowserAgentService {
}

func extractPageContent(from paneID: UUID, tabID: UUID, browserManager: BrowserManager) async -> String {
let webView = browserManager.ensureWebView(for: paneID, tabID: tabID)
let script = """
(() => {
const title = document.title || '';
const text = (document.body && document.body.innerText ? document.body.innerText : '').trim();
return JSON.stringify({ title, text, url: location.href });
})()
"""

guard let raw = try? await evaluateJavaScript(script, in: webView),
guard let raw = try? await browserManager.evaluateJavaScript(Self.pageContentExtractionScript, in: paneID, tabID: tabID),
let data = raw.data(using: .utf8),
let payload = try? JSONDecoder().decode(PageExtractionPayload.self, from: data) else {
return ""
Expand All @@ -56,8 +60,8 @@ struct BrowserAgentService {
settings: AppSettings,
aiService: AiService?
) async throws -> SaveResult {
let webView = browserManager.ensureWebView(for: paneID, tabID: tabID)
let payload = try await extractPayload(from: webView)
_ = browserManager.ensurePage(for: paneID, tabID: tabID)
let payload = try await extractPayload(from: paneID, tabID: tabID, browserManager: browserManager)

if let existing = savedPageStore.record(forURL: payload.url.absoluteString, in: workspacePath) {
browserManager.session(for: paneID).updateSavedRecordID(existing.id, for: tabID)
Expand Down Expand Up @@ -174,21 +178,21 @@ struct BrowserAgentService {
for proposal in proposals {
switch proposal.decision {
case .save:
if (try? await saveTab(
from: paneID,
tabID: proposal.tabID,
if await saveResult(
for: proposal.tabID,
paneID: paneID,
browserManager: browserManager,
fileSystem: fileSystem,
workspacePath: workspacePath,
settings: settings,
aiService: aiService
)) != nil {
) != nil {
saved += 1
}
case .readLater:
if let result = try? await saveTab(
from: paneID,
tabID: proposal.tabID,
if let result = await saveResult(
for: proposal.tabID,
paneID: paneID,
browserManager: browserManager,
fileSystem: fileSystem,
workspacePath: workspacePath,
Expand All @@ -209,16 +213,8 @@ struct BrowserAgentService {
return "Saved \(saved), queued \(queued), closed \(closed)"
}

private func extractPayload(from webView: WKWebView) async throws -> PageExtractionPayload {
let script = """
(() => {
const title = document.title || '';
const text = (document.body && document.body.innerText ? document.body.innerText : '').trim().slice(0, 20000);
return JSON.stringify({ title, text, url: location.href });
})()
"""

let raw = try await evaluateJavaScript(script, in: webView)
private func extractPayload(from paneID: UUID, tabID: UUID, browserManager: BrowserManager) async throws -> PageExtractionPayload {
let raw = try await browserManager.evaluateJavaScript(Self.pageSaveExtractionScript, in: paneID, tabID: tabID)
guard let data = raw.data(using: .utf8) else {
throw BrowserAgentError.invalidPagePayload
}
Expand Down Expand Up @@ -262,6 +258,26 @@ struct BrowserAgentService {
return fallbackSummary(from: trimmedText)
}

private func saveResult(
for tabID: UUID,
paneID: UUID,
browserManager: BrowserManager,
fileSystem: FileSystemService,
workspacePath: String,
settings: AppSettings,
aiService: AiService?
) async -> SaveResult? {
try? await saveTab(
from: paneID,
tabID: tabID,
browserManager: browserManager,
fileSystem: fileSystem,
workspacePath: workspacePath,
settings: settings,
aiService: aiService
)
}

private func fallbackSummary(from text: String) -> String {
let sentences = text
.components(separatedBy: CharacterSet(charactersIn: ".!?"))
Expand Down Expand Up @@ -301,22 +317,6 @@ struct BrowserAgentService {
.trimmingCharacters(in: .whitespacesAndNewlines)
return sanitized.isEmpty ? "Saved Page" : String(sanitized.prefix(80))
}

private func evaluateJavaScript(_ script: String, in webView: WKWebView) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
webView.evaluateJavaScript(script) { result, error in
if let error {
continuation.resume(throwing: error)
return
}
if let string = result as? String {
continuation.resume(returning: string)
} else {
continuation.resume(throwing: BrowserAgentError.invalidPagePayload)
}
}
}
}
}

private struct PageExtractionPayload: Codable {
Expand Down
59 changes: 59 additions & 0 deletions Sources/Bugbook/Services/BrowserEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import AppKit
import Foundation

struct BrowserPageState {
var title: String?
var url: URL?
var isLoading: Bool
var estimatedProgress: Double
var canGoBack: Bool
var canGoForward: Bool
var pageZoom: Double

static let empty = BrowserPageState(
title: nil,
url: nil,
isLoading: false,
estimatedProgress: 0,
canGoBack: false,
canGoForward: false,
pageZoom: 1.0
)
}

enum BrowserPageEvent {
case stateChanged(BrowserPageState)
case hoverURLChanged(String?)
case didFinishNavigation(title: String, url: URL)
case openInNewTab(URL)
case downloadStatusChanged(String)
}

typealias BrowserPageEventHandler = @MainActor (BrowserPageEvent) -> Void

@MainActor
protocol BrowserEngine: AnyObject {
func makePage(
for paneID: UUID,
tabID: UUID,
initialURL: URL?,
eventHandler: @escaping BrowserPageEventHandler
) -> any BrowserPage
}

@MainActor
protocol BrowserPage: AnyObject {
var hostView: NSView { get }
var state: BrowserPageState { get }

func load(_ request: URLRequest)
func goBack()
func goForward()
func reload()
func stopLoading()
func setPageZoom(_ zoom: Double)
func printPage()
func find(_ query: String, forward: Bool)
func evaluateJavaScript(_ script: String) async throws -> String
func dispose()
}
8 changes: 8 additions & 0 deletions Sources/Bugbook/Services/BrowserEngineFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

enum BrowserEngineFactory {
@MainActor
static func makeDefault() -> any BrowserEngine {
return WebKitBrowserEngine()
}
}
Loading