From 9c7d51429e55c229eef56f9168bc1162030a1a06 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 17 Dec 2025 17:36:40 +0100 Subject: [PATCH] macOS: auto-start gateway for Canvas actions --- apps/macos/Sources/Clawdis/CanvasWindow.swift | 16 ++++++------- .../Sources/Clawdis/GatewayConnection.swift | 24 ++++++++++++++++++- docs/refactor/canvas-a2ui.md | 19 +++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 docs/refactor/canvas-a2ui.md diff --git a/apps/macos/Sources/Clawdis/CanvasWindow.swift b/apps/macos/Sources/Clawdis/CanvasWindow.swift index ebeec96a2..de265d9f2 100644 --- a/apps/macos/Sources/Clawdis/CanvasWindow.swift +++ b/apps/macos/Sources/Clawdis/CanvasWindow.swift @@ -682,7 +682,7 @@ private final class HoverChromeContainerView: NSView { v.state = .active v.appearance = NSAppearance(named: .vibrantDark) v.wantsLayer = true - v.layer?.cornerRadius = 11 + v.layer?.cornerRadius = 10 v.layer?.masksToBounds = true v.layer?.borderWidth = 1 v.layer?.borderColor = NSColor.white.withAlphaComponent(0.22).cgColor @@ -695,7 +695,7 @@ private final class HoverChromeContainerView: NSView { }() private let closeButton: NSButton = { - let cfg = NSImage.SymbolConfiguration(pointSize: 9, weight: .semibold) + let cfg = NSImage.SymbolConfiguration(pointSize: 8, weight: .semibold) let img = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close")? .withSymbolConfiguration(cfg) ?? NSImage(size: NSSize(width: 18, height: 18)) @@ -744,13 +744,13 @@ private final class HoverChromeContainerView: NSView { self.closeBackground.centerXAnchor.constraint(equalTo: self.closeButton.centerXAnchor), self.closeBackground.centerYAnchor.constraint(equalTo: self.closeButton.centerYAnchor), - self.closeBackground.widthAnchor.constraint(equalToConstant: 22), - self.closeBackground.heightAnchor.constraint(equalToConstant: 22), + self.closeBackground.widthAnchor.constraint(equalToConstant: 20), + self.closeBackground.heightAnchor.constraint(equalToConstant: 20), - self.closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -9), - self.closeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 9), - self.closeButton.widthAnchor.constraint(equalToConstant: 18), - self.closeButton.heightAnchor.constraint(equalToConstant: 18), + self.closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -8), + self.closeButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 8), + self.closeButton.widthAnchor.constraint(equalToConstant: 16), + self.closeButton.heightAnchor.constraint(equalToConstant: 16), self.resizeHandle.trailingAnchor.constraint(equalTo: self.trailingAnchor), self.resizeHandle.bottomAnchor.constraint(equalTo: self.bottomAnchor), diff --git a/apps/macos/Sources/Clawdis/GatewayConnection.swift b/apps/macos/Sources/Clawdis/GatewayConnection.swift index 2b7abb4cc..6fc2b0f65 100644 --- a/apps/macos/Sources/Clawdis/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdis/GatewayConnection.swift @@ -94,7 +94,29 @@ actor GatewayConnection { guard let client else { throw NSError(domain: "Gateway", code: 0, userInfo: [NSLocalizedDescriptionKey: "gateway not configured"]) } - return try await client.request(method: method, params: params, timeoutMs: timeoutMs) + + do { + return try await client.request(method: method, params: params, timeoutMs: timeoutMs) + } catch { + // Auto-recover in local mode by spawning/attaching a gateway and retrying a few times. + // Canvas interactions should "just work" even if the local gateway isn't running yet. + let isLocal = await MainActor.run { AppStateStore.shared.connectionMode == .local } + guard isLocal else { throw error } + + await MainActor.run { GatewayProcessManager.shared.setActive(true) } + + var lastError: Error = error + for delayMs in [150, 400, 900] { + try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) + do { + return try await client.request(method: method, params: params, timeoutMs: timeoutMs) + } catch { + lastError = error + } + } + + throw lastError + } } func requestRaw( diff --git a/docs/refactor/canvas-a2ui.md b/docs/refactor/canvas-a2ui.md new file mode 100644 index 000000000..92149e687 --- /dev/null +++ b/docs/refactor/canvas-a2ui.md @@ -0,0 +1,19 @@ +# Canvas / A2UI + +## Goal +- A2UI rendering works out-of-the-box (no per-user toggles). +- A2UI button clicks always reach the agent automatically. +- Canvas chrome (close button) stays readable on any content. + +## Current behavior +- Canvas can show a bundled A2UI shell at `/__clawdis__/a2ui/` when no session `index.html` exists. +- The A2UI shell forwards `a2ui.action` button clicks to native via `WKScriptMessageHandler` (`clawdisCanvasA2UIAction`). +- Native forwards the click to the gateway as an agent invocation. + +## Fixes (2025-12-17) +- Close button: render a small vibrancy/material pill behind the “x” and reduce the button size for less visual weight. +- Click reliability: `GatewayConnection` auto-starts the local gateway (and retries briefly) when a request fails in `.local` mode, so Canvas actions don’t silently fail if the gateway isn’t running yet. + +## Follow-ups +- Add a small “action sent / failed” debug overlay in the A2UI shell (dev-only) to make failures obvious. +- Decide whether non-local Canvas content should ever be allowed to emit A2UI actions (current stance: no, for safety).