Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
import Observation
import OpenClawKit
import UIKit
import WebKit
@MainActor
@Observable
final class ScreenController {
private weak var activeWebView: WKWebView?
var urlString: String = ""
var errorText: String?
var isCanvasPresented: Bool = false
/// Callback invoked when an openclaw:// deep link is tapped in the canvas
var onDeepLink: ((URL) -> Void)?
/// Callback invoked when the user clicks an A2UI action (e.g. button) inside the canvas web UI.
var onA2UIAction: (([String: Any]) -> Void)?
private var debugStatusEnabled: Bool = false
private var debugStatusTitle: String?
private var debugStatusSubtitle: String?
private var homeCanvasStateJSON: String?
init() {
self.reload()
}
func navigate(to urlString: String, trustA2UIActions _: Bool = false) {
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
self.urlString = ""
self.reload()
return
}
if let url = URL(string: trimmed),
!url.isFileURL,
let host = url.host,
LoopbackHost.isLoopback(host)
{
// Never try to load loopback URLs from a remote gateway.
self.showDefaultCanvas()
return
}
self.urlString = (trimmed == "/" ? "" : trimmed)
self.reload()
}
func reload() {
self.applyScrollBehavior()
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
guard let url = Self.canvasScaffoldURL else { return }
self.errorText = nil
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
return
}
guard let url = URL(string: trimmed) else {
self.errorText = "Invalid URL: \(trimmed)"
return
}
self.errorText = nil
if url.isFileURL {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
} else {
webView.load(URLRequest(url: url))
}
}
func showDefaultCanvas() {
self.urlString = ""
self.reload()
}
func presentDefaultCanvas() {
self.isCanvasPresented = true
self.showDefaultCanvas()
}
func present(urlString: String) {
self.isCanvasPresented = true
self.navigate(to: urlString)
}
func hideCanvas() {
self.isCanvasPresented = false
self.showDefaultCanvas()
}
func showLocalA2UI() {
self.isCanvasPresented = true
guard let url = Self.localA2UIURL else {
self.showDefaultCanvas()
return
}
self.urlString = url.absoluteString
self.reload()
}
func isShowingLocalA2UI() -> Bool {
guard let url = URL(string: self.urlString),
url.isFileURL,
let expected = Self.localA2UIURL
else { return false }
return url.standardizedFileURL == expected.standardizedFileURL
}
func setDebugStatusEnabled(_ enabled: Bool) {
self.debugStatusEnabled = enabled
self.applyDebugStatusIfNeeded()
}
func updateDebugStatus(title: String?, subtitle: String?) {
self.debugStatusTitle = title
self.debugStatusSubtitle = subtitle
self.applyDebugStatusIfNeeded()
}
func applyDebugStatusIfNeeded() {
guard let webView = self.activeWebView else { return }
WebViewJavaScriptSupport.applyDebugStatus(
webView: webView,
enabled: self.debugStatusEnabled,
title: self.debugStatusTitle,
subtitle: self.debugStatusSubtitle)
}
func updateHomeCanvasState(json: String?) {
self.homeCanvasStateJSON = json
self.applyHomeCanvasStateIfNeeded()
}
func applyHomeCanvasStateIfNeeded() {
guard let webView = self.activeWebView else { return }
let payload = self.homeCanvasStateJSON ?? "null"
let js = """
(() => {
try {
const api = globalThis.__openclaw;
if (!api || typeof api.renderHome !== 'function') return;
api.renderHome(\(payload));
} catch (_) {}
})()
"""
webView.evaluateJavaScript(js) { _, _ in }
}
func waitForA2UIReady(timeoutMs: Int) async -> Bool {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: .milliseconds(timeoutMs))
while clock.now < deadline {
do {
let res = try await self.eval(javaScript: """
(() => {
try {
const host = globalThis.openclawA2UI;
return !!host && typeof host.applyMessages === 'function';
} catch (_) { return false; }
})()
""")
let trimmed = res.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if trimmed == "true" || trimmed == "1" { return true }
} catch {
// ignore; page likely still loading
}
try? await Task.sleep(nanoseconds: 120_000_000)
}
return false
}
func eval(javaScript: String) async throws -> String {
guard let webView = self.activeWebView else {
throw NSError(domain: "Screen", code: 3, userInfo: [
NSLocalizedDescriptionKey: "web view unavailable",
])
}
return try await WebViewJavaScriptSupport.evaluateToString(webView: webView, javaScript: javaScript)
}
func snapshotBase64(
maxWidth: CGFloat? = nil,
format: OpenClawCanvasSnapshotFormat,
quality: Double? = nil) async throws -> String
{
let image = try await self.snapshotImage(maxWidth: maxWidth)
let data: Data?
switch format {
case .png:
data = image.pngData()
case .jpeg:
let q = (quality ?? 0.82).clamped(to: 0.1...1.0)
data = image.jpegData(compressionQuality: q)
}
guard let data else {
throw NSError(domain: "Screen", code: 1, userInfo: [
NSLocalizedDescriptionKey: "snapshot encode failed",
])
}
return data.base64EncodedString()
}
private func snapshotImage(maxWidth: CGFloat?) async throws -> UIImage {
let config = WKSnapshotConfiguration()
if let maxWidth {
config.snapshotWidth = NSNumber(value: Double(maxWidth))
}
guard let webView = self.activeWebView else {
throw NSError(domain: "Screen", code: 3, userInfo: [
NSLocalizedDescriptionKey: "web view unavailable",
])
}
return try await withCheckedThrowingContinuation { cont in
webView.takeSnapshot(with: config) { image, error in
if let error {
cont.resume(throwing: error)
return
}
guard let image else {
cont.resume(throwing: NSError(domain: "Screen", code: 2, userInfo: [
NSLocalizedDescriptionKey: "snapshot failed",
]))
return
}
cont.resume(returning: image)
}
}
}
func attachWebView(_ webView: WKWebView) {
self.activeWebView = webView
self.reload()
self.applyDebugStatusIfNeeded()
self.applyHomeCanvasStateIfNeeded()
}
func detachWebView(_ webView: WKWebView) {
guard self.activeWebView === webView else { return }
self.activeWebView = nil
}
private static func bundledResourceURL(
name: String,
ext: String,
subdirectory: String)
-> URL?
{
let bundle = OpenClawKitResources.bundle
return bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory)
?? bundle.url(forResource: name, withExtension: ext)
}
private static let canvasScaffoldURL: URL? = ScreenController.bundledResourceURL(
name: "scaffold",
ext: "html",
subdirectory: "CanvasScaffold")
private static let localA2UIURL: URL? = ScreenController.bundledResourceURL(
name: "index",
ext: "html",
subdirectory: "CanvasA2UI")
func isTrustedCanvasUIURL(_ url: URL) -> Bool {
if url.isFileURL {
let std = url.standardizedFileURL
if let expected = Self.canvasScaffoldURL,
std == expected.standardizedFileURL
{
return true
}
if let expected = Self.localA2UIURL,
std == expected.standardizedFileURL
{
return true
}
return false
}
return false
}
nonisolated static func parseA2UIActionBody(_ body: Any) -> [String: Any]? {
if let dict = body as? [String: Any] { return dict.isEmpty ? nil : dict }
if let str = body as? String,
let data = str.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
{
return json.isEmpty ? nil : json
}
if let dict = body as? [AnyHashable: Any] {
let mapped = dict.reduce(into: [String: Any]()) { acc, pair in
guard let key = pair.key as? String else { return }
acc[key] = pair.value
}
return mapped.isEmpty ? nil : mapped
}
return nil
}
private func applyScrollBehavior() {
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
let allowScroll = !trimmed.isEmpty
let scrollView = webView.scrollView
// Default canvas needs raw touch events; external pages should scroll.
scrollView.isScrollEnabled = allowScroll
scrollView.bounces = allowScroll
}
}
extension Double {
fileprivate func clamped(to range: ClosedRange<Double>) -> Double {
if self < range.lowerBound { return range.lowerBound }
if self > range.upperBound { return range.upperBound }
return self
}
}

View File

@@ -0,0 +1,386 @@
import AVFoundation
import OpenClawKit
import ReplayKit
final class ScreenRecordService: @unchecked Sendable {
typealias CaptureHandler = @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void
typealias CaptureCompletion = @Sendable (Error?) -> Void
private struct UncheckedSendableBox<T>: @unchecked Sendable {
let value: T
}
private final class CaptureState: @unchecked Sendable {
private let lock = NSLock()
var writer: AVAssetWriter?
var videoInput: AVAssetWriterInput?
var audioInput: AVAssetWriterInput?
var started = false
var sawVideo = false
var lastVideoTime: CMTime?
var handlerError: Error?
func withLock<T>(_ body: (CaptureState) -> T) -> T {
self.lock.lock()
defer { lock.unlock() }
return body(self)
}
}
private let startReplayKitCaptureAction: @Sendable (
Bool,
@escaping CaptureHandler,
@escaping CaptureCompletion)
-> Void
private let stopReplayKitCaptureAction: @Sendable (@escaping CaptureCompletion) -> Void
init(
startReplayKitCaptureAction: @escaping @Sendable (
Bool,
@escaping CaptureHandler,
@escaping CaptureCompletion)
-> Void = { includeAudio, handler, completion in
Task { @MainActor in
startReplayKitCapture(
includeAudio: includeAudio,
handler: handler,
completion: completion)
}
},
stopReplayKitCaptureAction: @escaping @Sendable (@escaping CaptureCompletion) -> Void = { completion in
Task { @MainActor in
stopReplayKitCapture(completion)
}
})
{
self.startReplayKitCaptureAction = startReplayKitCaptureAction
self.stopReplayKitCaptureAction = stopReplayKitCaptureAction
}
enum ScreenRecordError: LocalizedError {
case invalidScreenIndex(Int)
case captureFailed(String)
case writeFailed(String)
var errorDescription: String? {
switch self {
case let .invalidScreenIndex(idx):
"Invalid screen index \(idx)"
case let .captureFailed(msg):
msg
case let .writeFailed(msg):
msg
}
}
}
func record(
screenIndex: Int?,
durationMs: Int?,
fps: Double?,
includeAudio: Bool?,
outPath: String?) async throws -> String
{
let config = try self.makeRecordConfig(
screenIndex: screenIndex,
durationMs: durationMs,
fps: fps,
includeAudio: includeAudio,
outPath: outPath)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "ai.openclawfoundation.app.screenrecord")
try await self.startCapture(state: state, config: config, recordQueue: recordQueue)
do {
try await Task.sleep(nanoseconds: UInt64(config.durationMs) * 1_000_000)
} catch {
try? await self.stopCapture()
throw error
}
try await self.stopCapture()
try self.finalizeCapture(state: state)
try await self.finishWriting(state: state)
return config.outURL.path
}
private struct RecordConfig {
let durationMs: Int
let fpsValue: Double
let includeAudio: Bool
let outURL: URL
}
private func makeRecordConfig(
screenIndex: Int?,
durationMs: Int?,
fps: Double?,
includeAudio: Bool?,
outPath: String?) throws -> RecordConfig
{
if let idx = screenIndex, idx != 0 {
throw ScreenRecordError.invalidScreenIndex(idx)
}
let durationMs = CaptureRateLimits.clampDurationMs(durationMs)
let fps = CaptureRateLimits.clampFps(fps, maxFps: 30)
let fpsInt = Int32(fps.rounded())
let fpsValue = Double(fpsInt)
let includeAudio = includeAudio ?? true
let outURL = self.makeOutputURL(outPath: outPath)
try? FileManager().removeItem(at: outURL)
return RecordConfig(
durationMs: durationMs,
fpsValue: fpsValue,
includeAudio: includeAudio,
outURL: outURL)
}
private func makeOutputURL(outPath: String?) -> URL {
if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return URL(fileURLWithPath: outPath)
}
return FileManager().temporaryDirectory
.appendingPathComponent("openclaw-screen-record-\(UUID().uuidString).mp4")
}
private func startCapture(
state: CaptureState,
config: RecordConfig,
recordQueue: DispatchQueue) async throws
{
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let handler = self.makeCaptureHandler(
state: state,
config: config,
recordQueue: recordQueue)
let completion: @Sendable (Error?) -> Void = { error in
if let error { cont.resume(throwing: error) } else { cont.resume() }
}
self.startReplayKitCaptureAction(
config.includeAudio,
handler,
completion)
}
}
private func makeCaptureHandler(
state: CaptureState,
config: RecordConfig,
recordQueue: DispatchQueue) -> @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void
{
{ sample, type, error in
let sampleBox = UncheckedSendableBox(value: sample)
// ReplayKit can call the capture handler on a background queue.
// Serialize writes to avoid queue asserts.
recordQueue.async {
let sample = sampleBox.value
if let error {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
return
}
guard CMSampleBufferDataIsReady(sample) else { return }
switch type {
case .video:
self.handleVideoSample(sample, state: state, config: config)
case .audioApp, .audioMic:
self.handleAudioSample(sample, state: state, includeAudio: config.includeAudio)
@unknown default:
break
}
}
}
}
private func handleVideoSample(
_ sample: CMSampleBuffer,
state: CaptureState,
config: RecordConfig)
{
let pts = CMSampleBufferGetPresentationTimeStamp(sample)
let shouldSkip = state.withLock { state in
if let lastVideoTime = state.lastVideoTime {
let delta = CMTimeSubtract(pts, lastVideoTime)
return delta.seconds < (1.0 / config.fpsValue)
}
return false
}
if shouldSkip { return }
if state.withLock({ $0.writer == nil }) {
self.prepareWriter(sample: sample, state: state, config: config, pts: pts)
}
let vInput = state.withLock { $0.videoInput }
let isStarted = state.withLock { $0.started }
guard let vInput, isStarted else { return }
if vInput.isReadyForMoreMediaData {
if vInput.append(sample) {
state.withLock { state in
state.sawVideo = true
state.lastVideoTime = pts
}
} else {
let err = state.withLock { $0.writer?.error }
if let err {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.writeFailed(err.localizedDescription)
}
}
}
}
}
}
private func prepareWriter(
sample: CMSampleBuffer,
state: CaptureState,
config: RecordConfig,
pts: CMTime)
{
guard let imageBuffer = CMSampleBufferGetImageBuffer(sample) else {
state.withLock { state in
if state.handlerError == nil {
state.handlerError = ScreenRecordError.captureFailed("Missing image buffer")
}
}
return
}
let width = CVPixelBufferGetWidth(imageBuffer)
let height = CVPixelBufferGetHeight(imageBuffer)
do {
let writer = try AVAssetWriter(outputURL: config.outURL, fileType: .mp4)
let settings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: width,
AVVideoHeightKey: height,
]
let vInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
vInput.expectsMediaDataInRealTime = true
guard writer.canAdd(vInput) else {
throw ScreenRecordError.writeFailed("Cannot add video input")
}
writer.add(vInput)
if config.includeAudio {
let aInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
aInput.expectsMediaDataInRealTime = true
if writer.canAdd(aInput) {
writer.add(aInput)
state.withLock { state in
state.audioInput = aInput
}
}
}
guard writer.startWriting() else {
throw ScreenRecordError.writeFailed(
writer.error?.localizedDescription ?? "Failed to start writer")
}
writer.startSession(atSourceTime: pts)
state.withLock { state in
state.writer = writer
state.videoInput = vInput
state.started = true
}
} catch {
state.withLock { state in
if state.handlerError == nil { state.handlerError = error }
}
}
}
private func handleAudioSample(
_ sample: CMSampleBuffer,
state: CaptureState,
includeAudio: Bool)
{
let aInput = state.withLock { $0.audioInput }
let isStarted = state.withLock { $0.started }
guard includeAudio, let aInput, isStarted else { return }
if aInput.isReadyForMoreMediaData {
_ = aInput.append(sample)
}
}
private func stopCapture() async throws {
let stopError = await withCheckedContinuation { cont in
self.stopReplayKitCaptureAction { error in
cont.resume(returning: error)
}
}
if let stopError { throw stopError }
}
private func finalizeCapture(state: CaptureState) throws {
if let handlerErrorSnapshot = state.withLock({ $0.handlerError }) {
throw handlerErrorSnapshot
}
let writerSnapshot = state.withLock { $0.writer }
let videoInputSnapshot = state.withLock { $0.videoInput }
let audioInputSnapshot = state.withLock { $0.audioInput }
let sawVideoSnapshot = state.withLock { $0.sawVideo }
guard let writerSnapshot, let videoInputSnapshot, sawVideoSnapshot else {
throw ScreenRecordError.captureFailed("No frames captured")
}
videoInputSnapshot.markAsFinished()
audioInputSnapshot?.markAsFinished()
_ = writerSnapshot
}
private func finishWriting(state: CaptureState) async throws {
guard let writerSnapshot = state.withLock({ $0.writer }) else {
throw ScreenRecordError.captureFailed("Missing writer")
}
let writerBox = UncheckedSendableBox(value: writerSnapshot)
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
writerBox.value.finishWriting {
let writer = writerBox.value
if let err = writer.error {
cont.resume(throwing: ScreenRecordError.writeFailed(err.localizedDescription))
} else if writer.status != .completed {
cont.resume(throwing: ScreenRecordError.writeFailed("Failed to finalize video"))
} else {
cont.resume()
}
}
}
}
}
@MainActor
private func startReplayKitCapture(
includeAudio: Bool,
handler: @escaping @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void,
completion: @escaping @Sendable (Error?) -> Void)
{
let recorder = RPScreenRecorder.shared()
recorder.isMicrophoneEnabled = includeAudio
recorder.startCapture(handler: handler, completionHandler: completion)
}
@MainActor
private func stopReplayKitCapture(_ completion: @escaping @Sendable (Error?) -> Void) {
RPScreenRecorder.shared().stopCapture { error in completion(error) }
}
#if DEBUG
extension ScreenRecordService {
nonisolated static func _test_clampDurationMs(_ ms: Int?) -> Int {
CaptureRateLimits.clampDurationMs(ms)
}
nonisolated static func _test_clampFps(_ fps: Double?) -> Double {
CaptureRateLimits.clampFps(fps, maxFps: 30)
}
}
#endif

View File

@@ -0,0 +1,189 @@
import OpenClawKit
import SwiftUI
import WebKit
struct ScreenWebView: UIViewRepresentable {
var controller: ScreenController
func makeCoordinator() -> ScreenWebViewCoordinator {
ScreenWebViewCoordinator(controller: self.controller)
}
func makeUIView(context: Context) -> UIView {
context.coordinator.makeContainerView()
}
func updateUIView(_: UIView, context: Context) {
context.coordinator.updateController(self.controller)
}
static func dismantleUIView(_: UIView, coordinator: ScreenWebViewCoordinator) {
coordinator.teardown()
}
}
@MainActor
final class ScreenWebViewCoordinator: NSObject {
private weak var controller: ScreenController?
private let navigationDelegate = ScreenNavigationDelegate()
private let a2uiActionHandler = CanvasA2UIActionMessageHandler()
private let userContentController = WKUserContentController()
private(set) var managedWebView: WKWebView?
private weak var containerView: UIView?
init(controller: ScreenController) {
self.controller = controller
super.init()
self.navigationDelegate.controller = controller
self.a2uiActionHandler.controller = controller
}
func makeContainerView() -> UIView {
if let containerView {
return containerView
}
let container = UIView(frame: .zero)
container.backgroundColor = .black
let webView = Self.makeWebView(userContentController: self.userContentController)
webView.navigationDelegate = self.navigationDelegate
self.installA2UIHandlers()
webView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
self.managedWebView = webView
self.containerView = container
self.controller?.attachWebView(webView)
return container
}
func updateController(_ controller: ScreenController) {
let previousController = self.controller
let controllerChanged = self.controller !== controller
self.controller = controller
self.navigationDelegate.controller = controller
self.a2uiActionHandler.controller = controller
if controllerChanged, let managedWebView {
previousController?.detachWebView(managedWebView)
controller.attachWebView(managedWebView)
}
}
func teardown() {
if let managedWebView {
self.controller?.detachWebView(managedWebView)
managedWebView.navigationDelegate = nil
}
self.removeA2UIHandlers()
self.navigationDelegate.controller = nil
self.a2uiActionHandler.controller = nil
self.managedWebView = nil
self.containerView = nil
}
private static func makeWebView(userContentController: WKUserContentController) -> WKWebView {
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
config.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: config)
// Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays.
webView.isOpaque = true
webView.backgroundColor = .black
let scrollView = webView.scrollView
scrollView.backgroundColor = .black
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.contentInset = .zero
scrollView.scrollIndicatorInsets = .zero
scrollView.automaticallyAdjustsScrollIndicatorInsets = false
return webView
}
private func installA2UIHandlers() {
for name in CanvasA2UIActionMessageHandler.handlerNames {
self.userContentController.add(self.a2uiActionHandler, name: name)
}
}
private func removeA2UIHandlers() {
for name in CanvasA2UIActionMessageHandler.handlerNames {
self.userContentController.removeScriptMessageHandler(forName: name)
}
}
}
// MARK: - Navigation Delegate
/// Handles navigation policy to intercept OpenClaw deep links from canvas.
@MainActor
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController?
func webView(
_: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void)
{
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let scheme = url.scheme?.lowercased()
if scheme == "openclaw" || scheme == "openclaw-debug" {
decisionHandler(.cancel)
self.controller?.onDeepLink?(url)
return
}
decisionHandler(.allow)
}
func webView(
_: WKWebView,
didFailProvisionalNavigation _: WKNavigation?,
withError error: any Error)
{
self.controller?.errorText = error.localizedDescription
}
func webView(_: WKWebView, didFinish _: WKNavigation?) {
self.controller?.errorText = nil
self.controller?.applyDebugStatusIfNeeded()
self.controller?.applyHomeCanvasStateIfNeeded()
}
func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) {
self.controller?.errorText = error.localizedDescription
}
}
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "openclawCanvasA2UIAction"
static let handlerNames = [messageName]
weak var controller: ScreenController?
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
guard Self.handlerNames.contains(message.name) else { return }
guard let controller else { return }
guard let url = message.webView?.url else { return }
guard controller.isTrustedCanvasUIURL(url) else { return }
guard let body = ScreenController.parseA2UIActionBody(message.body) else { return }
controller.onA2UIAction?(body)
}
}