Canvas: click progress + context-rich actions
This commit is contained in:
@@ -84,6 +84,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
||||
|
||||
const deepLinkKey = \(Self.jsStringLiteral(deepLinkKey));
|
||||
const sessionKey = \(Self.jsStringLiteral(injectedSessionKey));
|
||||
const machineName = \(Self.jsStringLiteral(InstanceIdentity.displayName));
|
||||
const instanceId = \(Self.jsStringLiteral(InstanceIdentity.instanceId));
|
||||
|
||||
globalThis.addEventListener('a2uiaction', (evt) => {
|
||||
try {
|
||||
@@ -96,6 +98,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
||||
|
||||
const context = Array.isArray(action?.context) ? action.context : [];
|
||||
const userAction = {
|
||||
id: (globalThis.crypto?.randomUUID?.() ?? String(Date.now())),
|
||||
name,
|
||||
surfaceId: payload.surfaceId ?? 'main',
|
||||
sourceComponentId: payload.sourceComponentId ?? '',
|
||||
@@ -117,7 +120,16 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
||||
return;
|
||||
}
|
||||
|
||||
const message = 'A2UI action: ' + name + '\\n\\n```json\\n' + JSON.stringify(userAction, null, 2) + '\\n```';
|
||||
const ctx = userAction.context ? (' ctx=' + JSON.stringify(userAction.context)) : '';
|
||||
const message =
|
||||
'CANVAS_A2UI action=' + userAction.name +
|
||||
' session=' + sessionKey +
|
||||
' surface=' + userAction.surfaceId +
|
||||
' component=' + (userAction.sourceComponentId || '-') +
|
||||
' host=' + machineName.replace(/\\s+/g, '_') +
|
||||
' instance=' + instanceId +
|
||||
ctx +
|
||||
' default=update_canvas';
|
||||
const params = new URLSearchParams();
|
||||
params.set('message', message);
|
||||
params.set('sessionKey', sessionKey);
|
||||
@@ -621,23 +633,24 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
|
||||
guard !userAction.isEmpty else { return }
|
||||
|
||||
guard let name = userAction["name"] as? String, !name.isEmpty else { return }
|
||||
|
||||
let json: String = {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: userAction, options: [.prettyPrinted, .sortedKeys]),
|
||||
let str = String(data: data, encoding: .utf8)
|
||||
{
|
||||
return str
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
let actionId =
|
||||
(userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||
?? UUID().uuidString
|
||||
|
||||
canvasWindowLogger.info("A2UI action \(name, privacy: .public) session=\(self.sessionKey, privacy: .public)")
|
||||
|
||||
let text = json.isEmpty
|
||||
? "A2UI action: \(name)"
|
||||
: "A2UI action: \(name)\n\n```json\n\(json)\n```"
|
||||
let surfaceId = (userAction["surfaceId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main"
|
||||
let sourceComponentId = (userAction["sourceComponentId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-"
|
||||
let host = Self.sanitizeTagValue(InstanceIdentity.displayName)
|
||||
let instanceId = InstanceIdentity.instanceId.lowercased()
|
||||
let contextJSON = Self.compactJSON(userAction["context"])
|
||||
let contextSuffix = contextJSON.flatMap { $0.isEmpty ? nil : " ctx=\($0)" } ?? ""
|
||||
|
||||
Task {
|
||||
// Token-efficient and unambiguous. The agent should treat this as a UI event and (by default) update Canvas.
|
||||
let text =
|
||||
"CANVAS_A2UI action=\(Self.sanitizeTagValue(name)) session=\(Self.sanitizeTagValue(self.sessionKey)) surface=\(Self.sanitizeTagValue(surfaceId)) component=\(Self.sanitizeTagValue(sourceComponentId)) host=\(host) instance=\(instanceId)\(contextSuffix) default=update_canvas"
|
||||
|
||||
Task { [weak webView] in
|
||||
if AppStateStore.shared.connectionMode == .local {
|
||||
GatewayProcessManager.shared.setActive(true)
|
||||
}
|
||||
@@ -648,13 +661,54 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
|
||||
thinking: "low",
|
||||
deliver: false,
|
||||
to: nil,
|
||||
channel: .last))
|
||||
channel: .last,
|
||||
idempotencyKey: actionId))
|
||||
|
||||
await MainActor.run {
|
||||
guard let webView else { return }
|
||||
let js = Self.jsDispatchA2UIActionStatus(actionId: actionId, ok: result.ok, error: result.error)
|
||||
webView.evaluateJavaScript(js) { _, _ in }
|
||||
}
|
||||
if !result.ok {
|
||||
canvasWindowLogger.error(
|
||||
"A2UI action send failed name=\(name, privacy: .public) error=\(result.error ?? "unknown", privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func sanitizeTagValue(_ value: String) -> String {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "-"
|
||||
let normalized = trimmed.replacingOccurrences(of: " ", with: "_")
|
||||
let allowed = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.:")
|
||||
let scalars = normalized.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
||||
return String(scalars)
|
||||
}
|
||||
|
||||
private static func compactJSON(_ obj: Any?) -> String? {
|
||||
guard let obj else { return nil }
|
||||
guard JSONSerialization.isValidJSONObject(obj) else { return nil }
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: obj, options: []),
|
||||
let str = String(data: data, encoding: .utf8)
|
||||
else { return nil }
|
||||
return str
|
||||
}
|
||||
|
||||
private static func jsDispatchA2UIActionStatus(actionId: String, ok: Bool, error: String?) -> String {
|
||||
let payload: [String: Any] = [
|
||||
"id": actionId,
|
||||
"ok": ok,
|
||||
"error": error ?? "",
|
||||
]
|
||||
let json: String = {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
|
||||
let str = String(data: data, encoding: .utf8)
|
||||
{
|
||||
return str
|
||||
}
|
||||
return "{\"id\":\"\(actionId)\",\"ok\":\(ok ? "true" : "false"),\"error\":\"\"}"
|
||||
}()
|
||||
return "window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: \(json) }));"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hover chrome container
|
||||
|
||||
Reference in New Issue
Block a user