fix(canvas): load A2UI resources across platforms

main
Peter Steinberger 2025-12-19 01:53:35 +00:00
parent 95ea67de28
commit b8012a2281
6 changed files with 45 additions and 22 deletions

View File

@ -285,6 +285,7 @@ class NodeRuntime(context: Context) {
add(ClawdisCanvasCommand.Eval.rawValue) add(ClawdisCanvasCommand.Eval.rawValue)
add(ClawdisCanvasCommand.Snapshot.rawValue) add(ClawdisCanvasCommand.Snapshot.rawValue)
add(ClawdisCanvasA2UICommand.Push.rawValue) add(ClawdisCanvasA2UICommand.Push.rawValue)
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdisCanvasA2UICommand.Reset.rawValue) add(ClawdisCanvasA2UICommand.Reset.rawValue)
if (cameraEnabled.value) { if (cameraEnabled.value) {
add(ClawdisCameraCommand.Snap.rawValue) add(ClawdisCameraCommand.Snap.rawValue)

View File

@ -179,6 +179,7 @@ final class BridgeConnectionController {
ClawdisCanvasCommand.evalJS.rawValue, ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue, ClawdisCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue, ClawdisCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue, ClawdisCanvasA2UICommand.reset.rawValue,
] ]

View File

@ -184,13 +184,25 @@ final class ScreenController {
return data.base64EncodedString() return data.base64EncodedString()
} }
// SwiftPM flattens resource directories; ensure resource filenames are unique. private static func bundledResourceURL(
private static let canvasScaffoldURL: URL? = ClawdisKitResources.bundle.url( name: String,
forResource: "scaffold", ext: String,
withExtension: "html") subdirectory: String)
private static let a2uiIndexURL: URL? = ClawdisKitResources.bundle.url( -> URL?
forResource: "index", {
withExtension: "html") let bundle = ClawdisKitResources.bundle
return bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory)
?? bundle.url(forResource: name, withExtension: ext)
}
private static let canvasScaffoldURL: URL? = Self.bundledResourceURL(
name: "scaffold",
ext: "html",
subdirectory: "CanvasScaffold")
private static let a2uiIndexURL: URL? = Self.bundledResourceURL(
name: "index",
ext: "html",
subdirectory: "CanvasA2UI")
func isTrustedCanvasUIURL(_ url: URL) -> Bool { func isTrustedCanvasUIURL(_ url: URL) -> Bool {
guard url.isFileURL else { return false } guard url.isFileURL else { return false }

View File

@ -305,6 +305,7 @@ struct SettingsTab: View {
ClawdisCanvasCommand.evalJS.rawValue, ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue, ClawdisCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue, ClawdisCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue, ClawdisCanvasA2UICommand.reset.rawValue,
] ]

View File

@ -226,12 +226,10 @@ final class CanvasManager {
} }
private static func hasBundledA2UIShell() -> Bool { private static func hasBundledA2UIShell() -> Bool {
guard let base = ClawdisKitResources.bundle.resourceURL? let bundle = ClawdisKitResources.bundle
.appendingPathComponent("CanvasA2UI", isDirectory: true) if bundle.url(forResource: "index", withExtension: "html", subdirectory: "CanvasA2UI") != nil {
else { return true
return false
} }
let index = base.appendingPathComponent("index.html", isDirectory: false) return bundle.url(forResource: "index", withExtension: "html") != nil
return FileManager.default.fileExists(atPath: index.path)
} }
} }

View File

@ -206,7 +206,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
private func a2uiShellPage(sessionRoot: URL) -> CanvasResponse { private func a2uiShellPage(sessionRoot: URL) -> CanvasResponse {
// Default Canvas UX: when no index exists, show the built-in scaffold page. // Default Canvas UX: when no index exists, show the built-in scaffold page.
if let data = self.loadBundledResourceData(relativePath: "scaffold.html") { if let data = self.loadBundledResourceData(relativePath: "CanvasScaffold/scaffold.html") {
return CanvasResponse(mime: "text/html", data: data) return CanvasResponse(mime: "text/html", data: data)
} }
@ -234,7 +234,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
return self.html("Forbidden", title: "Canvas: 403") return self.html("Forbidden", title: "Canvas: 403")
} }
guard let data = self.loadBundledResourceData(relativePath: relative) else { guard let data = self.loadBundledResourceData(relativePath: "CanvasA2UI/\(relative)") else {
return self.html("Not Found", title: "Canvas: 404") return self.html("Not Found", title: "Canvas: 404")
} }
@ -244,14 +244,24 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
} }
private func loadBundledResourceData(relativePath: String) -> Data? { private func loadBundledResourceData(relativePath: String) -> Data? {
// SwiftPM flattens resource directories; treat bundled canvas resources as uniquely-named files. let trimmed = relativePath.trimmingCharacters(in: .whitespacesAndNewlines)
if relativePath.contains("/") { return nil } guard !trimmed.isEmpty else { return nil }
let url = URL(fileURLWithPath: relativePath) if trimmed.contains("..") || trimmed.contains("\\") { return nil }
let ext = url.pathExtension
let name = url.deletingPathExtension().lastPathComponent let parts = trimmed.split(separator: "/")
guard let filename = parts.last else { return nil }
let subdirectory =
parts.count > 1 ? parts.dropLast().joined(separator: "/") : nil
let fileURL = URL(fileURLWithPath: String(filename))
let ext = fileURL.pathExtension
let name = fileURL.deletingPathExtension().lastPathComponent
guard !name.isEmpty, !ext.isEmpty else { return nil } guard !name.isEmpty, !ext.isEmpty else { return nil }
guard let resourceURL = ClawdisKitResources.bundle.url(forResource: name, withExtension: ext)
else { return nil } let bundle = ClawdisKitResources.bundle
let resourceURL =
bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory)
?? bundle.url(forResource: name, withExtension: ext)
guard let resourceURL else { return nil }
return try? Data(contentsOf: resourceURL) return try? Data(contentsOf: resourceURL)
} }