124 lines
4.6 KiB
Swift
124 lines
4.6 KiB
Swift
import AppKit
|
|
import ClawdisKit
|
|
import Foundation
|
|
import OSLog
|
|
import Security
|
|
|
|
private let deepLinkLogger = Logger(subsystem: "com.steipete.clawdis", category: "DeepLink")
|
|
|
|
@MainActor
|
|
final class DeepLinkHandler {
|
|
static let shared = DeepLinkHandler()
|
|
|
|
private var lastPromptAt: Date = .distantPast
|
|
|
|
func handle(url: URL) async {
|
|
guard let route = DeepLinkParser.parse(url) else {
|
|
deepLinkLogger.debug("ignored url \(url.absoluteString, privacy: .public)")
|
|
return
|
|
}
|
|
guard UserDefaults.standard.bool(forKey: deepLinkAgentEnabledKey) else {
|
|
self.presentAlert(
|
|
title: "Deep links are disabled",
|
|
message: "Enable “Allow URL scheme (agent)” in Clawdis Debug Settings to accept clawdis:// links.")
|
|
return
|
|
}
|
|
guard !AppStateStore.shared.isPaused else {
|
|
self.presentAlert(title: "Clawdis is paused", message: "Unpause Clawdis to run agent actions.")
|
|
return
|
|
}
|
|
|
|
switch route {
|
|
case let .agent(link):
|
|
await self.handleAgent(link: link, originalURL: url)
|
|
}
|
|
}
|
|
|
|
private func handleAgent(link: AgentDeepLink, originalURL: URL) async {
|
|
let messagePreview = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if messagePreview.count > 20000 {
|
|
self.presentAlert(title: "Deep link too large", message: "Message exceeds 20,000 characters.")
|
|
return
|
|
}
|
|
|
|
let allowUnattended = link.key == Self.expectedKey()
|
|
if !allowUnattended {
|
|
if Date().timeIntervalSince(self.lastPromptAt) < 1.0 {
|
|
deepLinkLogger.debug("throttling deep link prompt")
|
|
return
|
|
}
|
|
self.lastPromptAt = Date()
|
|
|
|
let trimmed = messagePreview.count > 240 ? "\(messagePreview.prefix(240))…" : messagePreview
|
|
let body =
|
|
"Run the agent with this message?\n\n\(trimmed)\n\nURL:\n\(originalURL.absoluteString)"
|
|
guard self.confirm(title: "Run Clawdis agent?", message: body) else { return }
|
|
}
|
|
|
|
if AppStateStore.shared.connectionMode == .local {
|
|
GatewayProcessManager.shared.setActive(true)
|
|
}
|
|
|
|
do {
|
|
var params: [String: AnyCodable] = [
|
|
"message": AnyCodable(messagePreview),
|
|
"idempotencyKey": AnyCodable(UUID().uuidString),
|
|
]
|
|
if let sessionKey = link.sessionKey, !sessionKey.isEmpty { params["sessionKey"] = AnyCodable(sessionKey) }
|
|
if let thinking = link.thinking, !thinking.isEmpty { params["thinking"] = AnyCodable(thinking) }
|
|
if let to = link.to, !to.isEmpty { params["to"] = AnyCodable(to) }
|
|
if let channel = link.channel, !channel.isEmpty { params["channel"] = AnyCodable(channel) }
|
|
if let timeout = link.timeoutSeconds { params["timeout"] = AnyCodable(timeout) }
|
|
params["deliver"] = AnyCodable(link.deliver)
|
|
|
|
_ = try await GatewayConnection.shared.request(method: "agent", params: params)
|
|
} catch {
|
|
self.presentAlert(title: "Agent request failed", message: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// MARK: - Auth
|
|
|
|
static func currentKey() -> String {
|
|
self.expectedKey()
|
|
}
|
|
|
|
private static func expectedKey() -> String {
|
|
let defaults = UserDefaults.standard
|
|
if let key = defaults.string(forKey: deepLinkKeyKey), !key.isEmpty {
|
|
return key
|
|
}
|
|
var bytes = [UInt8](repeating: 0, count: 32)
|
|
_ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
|
let data = Data(bytes)
|
|
let key = data
|
|
.base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
.replacingOccurrences(of: "=", with: "")
|
|
defaults.set(key, forKey: deepLinkKeyKey)
|
|
return key
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
private func confirm(title: String, message: String) -> Bool {
|
|
let alert = NSAlert()
|
|
alert.messageText = title
|
|
alert.informativeText = message
|
|
alert.addButton(withTitle: "Run")
|
|
alert.addButton(withTitle: "Cancel")
|
|
alert.alertStyle = .warning
|
|
return alert.runModal() == .alertFirstButtonReturn
|
|
}
|
|
|
|
private func presentAlert(title: String, message: String) {
|
|
let alert = NSAlert()
|
|
alert.messageText = title
|
|
alert.informativeText = message
|
|
alert.addButton(withTitle: "OK")
|
|
alert.alertStyle = .informational
|
|
alert.runModal()
|
|
}
|
|
}
|