diff --git a/apps/macos/Sources/Clawdis/AppMain.swift b/apps/macos/Sources/Clawdis/AppMain.swift index 9b46a85ad..7abb680a0 100644 --- a/apps/macos/Sources/Clawdis/AppMain.swift +++ b/apps/macos/Sources/Clawdis/AppMain.swift @@ -1466,6 +1466,7 @@ actor MicLevelMonitor { } } +@MainActor final class VoiceWakeTester { private let recognizer: SFSpeechRecognizer? private let audioEngine = AVAudioEngine() @@ -2146,14 +2147,14 @@ struct VoiceWakeSettings: View { let cleanedID = self.normalizedLocaleIdentifier(locale.identifier) let cleanLocale = Locale(identifier: cleanedID) - if let langCode = cleanLocale.languageCode, + if let langCode = cleanLocale.language.languageCode?.identifier, let lang = cleanLocale.localizedString(forLanguageCode: langCode), - let regionCode = cleanLocale.regionCode, + let regionCode = cleanLocale.region?.identifier, let region = cleanLocale.localizedString(forRegionCode: regionCode) { return "\(lang) (\(region))" } - if let langCode = cleanLocale.languageCode, + if let langCode = cleanLocale.language.languageCode?.identifier, let lang = cleanLocale.localizedString(forLanguageCode: langCode) { return lang diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift new file mode 100644 index 000000000..75f90083e --- /dev/null +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -0,0 +1,243 @@ +import AppKit +import Foundation +import WebKit + +final class WebChatWindowController: NSWindowController, WKScriptMessageHandler { + private let webView: WKWebView + private let sessionKey: String + + init(sessionKey: String) { + self.sessionKey = sessionKey + + let config = WKWebViewConfiguration() + let contentController = WKUserContentController() + config.userContentController = contentController + config.preferences.isElementFullscreenEnabled = true + + // Inject callback receiver stub + let callbackScript = """ + window.__clawdisCallbacks = new Map(); + window.__clawdisReceive = function(resp) { + const entry = window.__clawdisCallbacks.get(resp.id); + if (!entry) return; + window.__clawdisCallbacks.delete(resp.id); + if (resp.ok) { + entry.resolve(resp.result); + } else { + entry.reject(resp.error || 'unknown error'); + } + }; + window.__clawdisSend = function(payload) { + const id = crypto.randomUUID(); + return new Promise((resolve, reject) => { + window.__clawdisCallbacks.set(id, { resolve, reject }); + window.webkit?.messageHandlers?.clawdis?.postMessage({ id, ...payload }); + }); + }; + """ + let userScript = WKUserScript(source: callbackScript, injectionTime: .atDocumentStart, forMainFrameOnly: true) + contentController.addUserScript(userScript) + + self.webView = WKWebView(frame: .zero, configuration: config) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 960, height: 720), + styleMask: [.titled, .closable, .resizable, .miniaturizable], + backing: .buffered, + defer: false) + window.title = "Clawd Web Chat" + window.contentView = self.webView + super.init(window: window) + contentController.add(self, name: "clawdis") + self.loadPage() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + private func loadPage() { + let monoRoot = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Projects/pi-mono") + let webUiDist = monoRoot.appendingPathComponent("packages/web-ui/dist") + let piAiDist = monoRoot.appendingPathComponent("packages/ai/dist/index.js") + let miniLitDist = monoRoot.appendingPathComponent("packages/web-ui/node_modules/@mariozechner/mini-lit/dist/index.js") + let litDist = monoRoot.appendingPathComponent("packages/web-ui/node_modules/lit/index.js") + let lucideDist = monoRoot.appendingPathComponent("node_modules/lucide/dist/esm/lucide.js") + + let distPath = webUiDist.path(percentEncoded: false) + let cssPath = webUiDist.appendingPathComponent("app.css").path(percentEncoded: false) + + let importMap = [ + "imports": [ + "@mariozechner/pi-web-ui": "file://\(distPath)/index.js", + "@mariozechner/pi-ai": "file://\(piAiDist.path(percentEncoded: false))", + "@mariozechner/mini-lit": "file://\(miniLitDist.path(percentEncoded: false))", + "lit": "file://\(litDist.path(percentEncoded: false))", + "lucide": "file://\(lucideDist.path(percentEncoded: false))" + ] + ] + + let importMapJSON: String + if let data = try? JSONSerialization.data(withJSONObject: importMap, options: [.prettyPrinted]), + let json = String(data: data, encoding: .utf8) { + importMapJSON = json + } else { + importMapJSON = "{}" + } + + let html = """ + + +
+ +