Nodes: advertise canvas invoke commands

This commit is contained in:
Peter Steinberger
2025-12-18 02:05:06 +00:00
parent 54830e8401
commit efed2ae30f
15 changed files with 212 additions and 153 deletions

View File

@@ -46,16 +46,17 @@ actor BridgeClient {
}
onStatus?("Requesting approval…")
try await self.send(
BridgePairRequest(
nodeId: hello.nodeId,
displayName: hello.displayName,
platform: hello.platform,
version: hello.version,
deviceFamily: hello.deviceFamily,
modelIdentifier: hello.modelIdentifier,
caps: hello.caps),
over: connection)
try await self.send(
BridgePairRequest(
nodeId: hello.nodeId,
displayName: hello.displayName,
platform: hello.platform,
version: hello.version,
deviceFamily: hello.deviceFamily,
modelIdentifier: hello.modelIdentifier,
caps: hello.caps,
commands: hello.commands),
over: connection)
onStatus?("Waiting for approval…")
let ok = try await self.withTimeout(seconds: 60, purpose: "pairing approval") {

View File

@@ -136,7 +136,8 @@ final class BridgeConnectionController {
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier(),
caps: self.currentCaps())
caps: self.currentCaps(),
commands: self.currentCommands())
}
private func resolvedDisplayName(defaults: UserDefaults) -> String {
@@ -170,6 +171,25 @@ final class BridgeConnectionController {
return caps
}
private func currentCommands() -> [String] {
var commands: [String] = [
ClawdisCanvasCommand.show.rawValue,
ClawdisCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.setMode.rawValue,
ClawdisCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue,
]
let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue)
}
return commands
}
private func platformString() -> String {
let v = ProcessInfo.processInfo.operatingSystemVersion
let name = switch UIDevice.current.userInterfaceIdiom {

View File

@@ -287,30 +287,30 @@ final class NodeAppModel {
do {
switch command {
case ClawdisScreenCommand.show.rawValue:
case ClawdisCanvasCommand.show.rawValue:
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisScreenCommand.hide.rawValue:
case ClawdisCanvasCommand.hide.rawValue:
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisScreenCommand.setMode.rawValue:
let params = try Self.decodeParams(ClawdisScreenSetModeParams.self, from: req.paramsJSON)
case ClawdisCanvasCommand.setMode.rawValue:
let params = try Self.decodeParams(ClawdisCanvasSetModeParams.self, from: req.paramsJSON)
self.screen.setMode(params.mode)
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisScreenCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdisScreenNavigateParams.self, from: req.paramsJSON)
case ClawdisCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdisCanvasNavigateParams.self, from: req.paramsJSON)
self.screen.navigate(to: params.url)
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisScreenCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdisScreenEvalParams.self, from: req.paramsJSON)
case ClawdisCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdisCanvasEvalParams.self, from: req.paramsJSON)
let result = try await self.screen.eval(javaScript: params.javaScript)
let payload = try Self.encodePayload(["result": result])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisScreenCommand.snapshot.rawValue:
let params = try? Self.decodeParams(ClawdisScreenSnapshotParams.self, from: req.paramsJSON)
case ClawdisCanvasCommand.snapshot.rawValue:
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON)
let maxWidth = params?.maxWidth.map { CGFloat($0) }
let base64 = try await self.screen.snapshotPNGBase64(maxWidth: maxWidth)
let payload = try Self.encodePayload(["format": "png", "base64": base64])

View File

@@ -9,7 +9,7 @@ final class ScreenController {
let webView: WKWebView
private let navigationDelegate: ScreenNavigationDelegate
var mode: ClawdisScreenMode = .canvas
var mode: ClawdisCanvasMode = .canvas
var urlString: String = ""
var errorText: String?
@@ -36,7 +36,7 @@ final class ScreenController {
self.reload()
}
func setMode(_ mode: ClawdisScreenMode) {
func setMode(_ mode: ClawdisCanvasMode) {
self.mode = mode
self.reload()
}

View File

@@ -262,6 +262,60 @@ struct SettingsTab: View {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
}
private func deviceFamily() -> String {
switch UIDevice.current.userInterfaceIdiom {
case .pad:
"iPad"
case .phone:
"iPhone"
default:
"iOS"
}
}
private func modelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in
String(decoding: ptr.prefix { $0 != 0 }, as: UTF8.self)
}
return machine.isEmpty ? "unknown" : machine
}
private func currentCaps() -> [String] {
var caps = [ClawdisCapability.canvas.rawValue]
let cameraEnabled =
UserDefaults.standard.object(forKey: "camera.enabled") == nil
? true
: UserDefaults.standard.bool(forKey: "camera.enabled")
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) }
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) }
return caps
}
private func currentCommands() -> [String] {
var commands: [String] = [
ClawdisCanvasCommand.show.rawValue,
ClawdisCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.setMode.rawValue,
ClawdisCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue,
]
let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue)
}
return commands
}
private func connect(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) async {
self.connectingBridgeID = bridge.id
self.manualBridgeEnabled = false
@@ -285,7 +339,11 @@ struct SettingsTab: View {
displayName: self.displayName,
token: existingToken,
platform: self.platformString(),
version: self.appVersion())
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier(),
caps: self.currentCaps(),
commands: self.currentCommands())
let token = try await BridgeClient().pairAndHello(
endpoint: bridge.endpoint,
hello: hello,
@@ -309,7 +367,11 @@ struct SettingsTab: View {
displayName: self.displayName,
token: token,
platform: self.platformString(),
version: self.appVersion()))
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier(),
caps: self.currentCaps(),
commands: self.currentCommands()))
} catch {
self.connectStatus.text = "Failed: \(error.localizedDescription)"
@@ -351,7 +413,11 @@ struct SettingsTab: View {
displayName: self.displayName,
token: existingToken,
platform: self.platformString(),
version: self.appVersion())
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier(),
caps: self.currentCaps(),
commands: self.currentCommands())
let token = try await BridgeClient().pairAndHello(
endpoint: endpoint,
hello: hello,
@@ -375,7 +441,11 @@ struct SettingsTab: View {
displayName: self.displayName,
token: token,
platform: self.platformString(),
version: self.appVersion()))
version: self.appVersion(),
deviceFamily: self.deviceFamily(),
modelIdentifier: self.modelIdentifier(),
caps: self.currentCaps(),
commands: self.currentCommands()))
} catch {
self.connectStatus.text = "Failed: \(error.localizedDescription)"

View File

@@ -1,38 +0,0 @@
import ClawdisKit
import Testing
@Suite struct CanvasCommandAliasTests {
@Test func mapsKnownCanvasCommandsToScreen() {
let mappings: [(ClawdisCanvasCommand, ClawdisScreenCommand)] = [
(.show, .show),
(.hide, .hide),
(.setMode, .setMode),
(.navigate, .navigate),
(.evalJS, .evalJS),
(.snapshot, .snapshot),
]
for (canvas, screen) in mappings {
#expect(
ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen(canvas.rawValue) ==
screen.rawValue)
}
}
@Test func mapsUnknownCanvasNamespaceToScreen() {
#expect(ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen("canvas.foo") == "screen.foo")
}
@Test func leavesNonCanvasCommandsUnchanged() {
#expect(
ClawdisInvokeCommandAliases.canonicalizeCanvasToScreen(ClawdisCameraCommand.snap.rawValue) ==
ClawdisCameraCommand.snap.rawValue)
}
@Test func capabilitiesUseStableStrings() {
#expect(ClawdisCapability.canvas.rawValue == "canvas")
#expect(ClawdisCapability.camera.rawValue == "camera")
#expect(ClawdisCapability.voiceWake.rawValue == "voiceWake")
}
}