diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt index 4e431bf8e..c1cd428b5 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt @@ -285,6 +285,7 @@ class NodeRuntime(context: Context) { add(ClawdisCanvasCommand.Eval.rawValue) add(ClawdisCanvasCommand.Snapshot.rawValue) add(ClawdisCanvasA2UICommand.Push.rawValue) + add(ClawdisCanvasA2UICommand.PushJSONL.rawValue) add(ClawdisCanvasA2UICommand.Reset.rawValue) if (cameraEnabled.value) { add(ClawdisCameraCommand.Snap.rawValue) diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index b12609d2d..e8421a2c1 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -179,6 +179,7 @@ final class BridgeConnectionController { ClawdisCanvasCommand.evalJS.rawValue, ClawdisCanvasCommand.snapshot.rawValue, ClawdisCanvasA2UICommand.push.rawValue, + ClawdisCanvasA2UICommand.pushJSONL.rawValue, ClawdisCanvasA2UICommand.reset.rawValue, ] diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index 92c31cde8..8c64e9fec 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -184,13 +184,25 @@ final class ScreenController { return data.base64EncodedString() } - // SwiftPM flattens resource directories; ensure resource filenames are unique. - private static let canvasScaffoldURL: URL? = ClawdisKitResources.bundle.url( - forResource: "scaffold", - withExtension: "html") - private static let a2uiIndexURL: URL? = ClawdisKitResources.bundle.url( - forResource: "index", - withExtension: "html") + private static func bundledResourceURL( + name: String, + ext: String, + subdirectory: String) + -> URL? + { + 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 { guard url.isFileURL else { return false } diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index 6395f0322..09251fffa 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -305,6 +305,7 @@ struct SettingsTab: View { ClawdisCanvasCommand.evalJS.rawValue, ClawdisCanvasCommand.snapshot.rawValue, ClawdisCanvasA2UICommand.push.rawValue, + ClawdisCanvasA2UICommand.pushJSONL.rawValue, ClawdisCanvasA2UICommand.reset.rawValue, ] diff --git a/apps/macos/Sources/Clawdis/CanvasManager.swift b/apps/macos/Sources/Clawdis/CanvasManager.swift index 668fb4721..f88593bde 100644 --- a/apps/macos/Sources/Clawdis/CanvasManager.swift +++ b/apps/macos/Sources/Clawdis/CanvasManager.swift @@ -226,12 +226,10 @@ final class CanvasManager { } private static func hasBundledA2UIShell() -> Bool { - guard let base = ClawdisKitResources.bundle.resourceURL? - .appendingPathComponent("CanvasA2UI", isDirectory: true) - else { - return false + let bundle = ClawdisKitResources.bundle + if bundle.url(forResource: "index", withExtension: "html", subdirectory: "CanvasA2UI") != nil { + return true } - let index = base.appendingPathComponent("index.html", isDirectory: false) - return FileManager.default.fileExists(atPath: index.path) + return bundle.url(forResource: "index", withExtension: "html") != nil } } diff --git a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift index fda061e05..52b9d467d 100644 --- a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift +++ b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift @@ -206,7 +206,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { private func a2uiShellPage(sessionRoot: URL) -> CanvasResponse { // 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) } @@ -234,7 +234,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { 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") } @@ -244,14 +244,24 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { } private func loadBundledResourceData(relativePath: String) -> Data? { - // SwiftPM flattens resource directories; treat bundled canvas resources as uniquely-named files. - if relativePath.contains("/") { return nil } - let url = URL(fileURLWithPath: relativePath) - let ext = url.pathExtension - let name = url.deletingPathExtension().lastPathComponent + let trimmed = relativePath.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if trimmed.contains("..") || trimmed.contains("\\") { return nil } + + 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 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) }