refactor: apply stashed bridge + CLI changes

This commit is contained in:
Peter Steinberger
2025-12-13 19:30:46 +00:00
parent 0b990443de
commit e2a93e17f9
23 changed files with 1337 additions and 1097 deletions

View File

@@ -9,11 +9,7 @@ actor BridgeClient {
func pairAndHello(
endpoint: NWEndpoint,
nodeId: String,
displayName: String?,
platform: String,
version: String,
existingToken: String?,
hello: BridgeHello,
onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String
{
self.lineBuffer = Data()
@@ -25,14 +21,7 @@ actor BridgeClient {
}
onStatus?("Authenticating…")
try await self.send(
BridgeHello(
nodeId: nodeId,
displayName: displayName,
token: existingToken,
platform: platform,
version: version),
over: connection)
try await self.send(hello, over: connection)
let first = try await self.withTimeout(seconds: 10, purpose: "hello") { () -> ReceivedFrame in
guard let frame = try await self.receiveFrame(over: connection) else {
@@ -46,7 +35,7 @@ actor BridgeClient {
switch first.base.type {
case "hello-ok":
// We only return a token if we have one; callers should treat empty as "no token yet".
return existingToken ?? ""
return hello.token ?? ""
case "error":
let err = try self.decoder.decode(BridgeErrorFrame.self, from: first.data)
@@ -59,10 +48,11 @@ actor BridgeClient {
onStatus?("Requesting approval…")
try await self.send(
BridgePairRequest(
nodeId: nodeId,
displayName: displayName,
platform: platform,
version: version),
nodeId: hello.nodeId,
displayName: hello.displayName,
platform: hello.platform,
version: hello.version
),
over: connection)
onStatus?("Waiting for approval…")
@@ -155,7 +145,9 @@ actor BridgeClient {
var errorDescription: String? {
if self.purpose == "pairing approval" {
return "Timed out waiting for approval (\(self.seconds)s). Approve the node on your gateway and try again."
return
"Timed out waiting for approval (\(self.seconds)s). " +
"Approve the node on your gateway and try again."
}
return "Timed out during \(self.purpose) (\(self.seconds)s)."
}

View File

@@ -42,45 +42,36 @@ final class NodeAppModel: ObservableObject {
}
}
func setVoiceWakeEnabled(_ enabled: Bool) {
self.voiceWake.setEnabled(enabled)
}
func setVoiceWakeEnabled(_ enabled: Bool) {
self.voiceWake.setEnabled(enabled)
}
func connectToBridge(
endpoint: NWEndpoint,
token: String,
nodeId: String,
displayName: String?,
platform: String,
version: String)
{
self.bridgeTask?.cancel()
self.bridgeStatusText = "Connecting…"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
func connectToBridge(
endpoint: NWEndpoint,
hello: BridgeHello)
{
self.bridgeTask?.cancel()
self.bridgeStatusText = "Connecting…"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
self.bridgeTask = Task {
do {
try await self.bridge.connect(
endpoint: endpoint,
hello: BridgeHello(
nodeId: nodeId,
displayName: displayName,
token: token,
platform: platform,
version: version),
onConnected: { [weak self] serverName in
guard let self else { return }
await MainActor.run {
self.bridgeStatusText = "Connected"
do {
try await self.bridge.connect(
endpoint: endpoint,
hello: hello,
onConnected: { [weak self] serverName in
guard let self else { return }
await MainActor.run {
self.bridgeStatusText = "Connected"
self.bridgeServerName = serverName
}
if let addr = await self.bridge.currentRemoteAddress() {
await MainActor.run {
self.bridgeRemoteAddress = addr
}
}
}
}
},
onInvoke: { [weak self] req in
guard let self else {
@@ -119,16 +110,20 @@ final class NodeAppModel: ObservableObject {
self.connectedBridgeID = nil
}
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
struct Payload: Codable {
var text: String
var sessionKey: String?
}
let payload = Payload(text: text, sessionKey: sessionKey)
let data = try JSONEncoder().encode(payload)
let json = String(decoding: data, as: UTF8.self)
try await self.bridge.sendEvent(event: "voice.transcript", payloadJSON: json)
}
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
struct Payload: Codable {
var text: String
var sessionKey: String?
}
let payload = Payload(text: text, sessionKey: sessionKey)
let data = try JSONEncoder().encode(payload)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8",
])
}
try await self.bridge.sendEvent(event: "voice.transcript", payloadJSON: json)
}
func handleDeepLink(url: URL) async {
guard let route = DeepLinkParser.parse(url) else { return }
@@ -168,12 +163,16 @@ final class NodeAppModel: ObservableObject {
])
}
// iOS bridge forwards to the gateway; no local auth prompts here.
// (Key-based unattended auth is handled on macOS for clawdis:// links.)
let data = try JSONEncoder().encode(link)
let json = String(decoding: data, as: UTF8.self)
try await self.bridge.sendEvent(event: "agent.request", payloadJSON: json)
}
// iOS bridge forwards to the gateway; no local auth prompts here.
// (Key-based unattended auth is handled on macOS for clawdis:// links.)
let data = try JSONEncoder().encode(link)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8",
])
}
try await self.bridge.sendEvent(event: "agent.request", payloadJSON: json)
}
private func isBridgeConnected() async -> Bool {
if case .connected = await self.bridge.state { return true }
@@ -244,8 +243,13 @@ final class NodeAppModel: ObservableObject {
return try JSONDecoder().decode(type, from: data)
}
private static func encodePayload(_ obj: some Encodable) throws -> String {
let data = try JSONEncoder().encode(obj)
return String(decoding: data, as: UTF8.self)
}
private static func encodePayload(_ obj: some Encodable) throws -> String {
let data = try JSONEncoder().encode(obj)
guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 21, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode payload as UTF-8",
])
}
return json
}
}

View File

@@ -105,17 +105,19 @@ final class ScreenController: ObservableObject {
#000;
overflow: hidden;
}
body::before {
content:"";
position: fixed;
inset: -20%;
background:
repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 48px),
repeating-linear-gradient(90deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 48px);
transform: rotate(-7deg);
opacity: 0.55;
pointer-events: none;
}
body::before {
content:"";
position: fixed;
inset: -20%;
background:
repeating-linear-gradient(0deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
transparent 1px, transparent 48px),
repeating-linear-gradient(90deg, rgba(255,255,255,0.02) 0, rgba(255,255,255,0.02) 1px,
transparent 1px, transparent 48px);
transform: rotate(-7deg);
opacity: 0.55;
pointer-events: none;
}
canvas {
display:block;
width:100vw;

View File

@@ -77,17 +77,20 @@ struct SettingsTab: View {
guard let existing, !existing.isEmpty else { return }
guard let target = self.pickAutoConnectBridge(from: newValue) else { return }
self.didAutoConnect = true
self.preferredBridgeStableID = target.stableID
self.appModel.connectToBridge(
endpoint: target.endpoint,
token: existing,
nodeId: self.instanceId,
displayName: self.displayName,
platform: self.platformString(),
version: self.appVersion())
self.connectStatus = nil
}
self.didAutoConnect = true
self.preferredBridgeStableID = target.stableID
self.appModel.connectToBridge(
endpoint: target.endpoint,
hello: BridgeHello(
nodeId: self.instanceId,
displayName: self.displayName,
token: existing,
platform: self.platformString(),
version: self.appVersion()
)
)
self.connectStatus = nil
}
.onChange(of: self.appModel.bridgeServerName) { _, _ in
self.connectStatus = nil
}
@@ -170,18 +173,22 @@ struct SettingsTab: View {
existing :
nil
let token = try await BridgeClient().pairAndHello(
endpoint: bridge.endpoint,
nodeId: self.instanceId,
displayName: self.displayName,
platform: self.platformString(),
version: self.appVersion(),
existingToken: existingToken,
onStatus: { status in
Task { @MainActor in
self.connectStatus = status
}
})
let hello = BridgeHello(
nodeId: self.instanceId,
displayName: self.displayName,
token: existingToken,
platform: self.platformString(),
version: self.appVersion()
)
let token = try await BridgeClient().pairAndHello(
endpoint: bridge.endpoint,
hello: hello,
onStatus: { status in
Task { @MainActor in
self.connectStatus = status
}
}
)
if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString(
@@ -190,13 +197,16 @@ struct SettingsTab: View {
account: self.keychainAccount())
}
self.appModel.connectToBridge(
endpoint: bridge.endpoint,
token: token,
nodeId: self.instanceId,
displayName: self.displayName,
platform: self.platformString(),
version: self.appVersion())
self.appModel.connectToBridge(
endpoint: bridge.endpoint,
hello: BridgeHello(
nodeId: self.instanceId,
displayName: self.displayName,
token: token,
platform: self.platformString(),
version: self.appVersion()
)
)
} catch {
self.connectStatus = "Failed: \(error.localizedDescription)"