From 51aed3ca0a70329a01dccfbdbf968e6565198769 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 9 Dec 2025 04:42:32 +0100 Subject: [PATCH] chore(mac): apply swiftformat and lint fixes --- .../Sources/Clawdis/AgentEventStore.swift | 4 +- .../Sources/Clawdis/AgentEventsWindow.swift | 21 +++-- apps/macos/Sources/Clawdis/AgentRPC.swift | 2 +- apps/macos/Sources/Clawdis/AppState.swift | 3 +- .../Sources/Clawdis/ConfigSettings.swift | 6 +- .../Sources/Clawdis/CritterStatusLabel.swift | 3 +- .../macos/Sources/Clawdis/DebugSettings.swift | 10 ++- .../Sources/Clawdis/GeneralSettings.swift | 80 ++++++++++--------- apps/macos/Sources/Clawdis/HealthStore.swift | 2 +- .../Sources/Clawdis/HeartbeatStore.swift | 11 +-- apps/macos/Sources/Clawdis/IconState.swift | 52 ++++++------ .../Sources/Clawdis/InstancesSettings.swift | 24 +++--- .../Sources/Clawdis/InstancesStore.swift | 43 ++++++++-- apps/macos/Sources/Clawdis/Onboarding.swift | 26 ++++-- .../Sources/Clawdis/PresenceReporter.swift | 14 +++- .../Sources/Clawdis/RuntimeLocator.swift | 38 ++++++--- apps/macos/Sources/Clawdis/SoundEffects.swift | 4 +- apps/macos/Sources/Clawdis/Utilities.swift | 62 +++++++++----- .../Sources/Clawdis/VoicePushToTalk.swift | 26 +++--- .../Sources/Clawdis/VoiceWakeChime.swift | 15 ++-- .../Sources/Clawdis/VoiceWakeForwarder.swift | 13 ++- .../Sources/Clawdis/VoiceWakeOverlay.swift | 76 +++++++++++++----- .../Sources/Clawdis/VoiceWakeRuntime.swift | 20 +++-- .../Sources/Clawdis/VoiceWakeSettings.swift | 5 +- .../macos/Sources/Clawdis/WebChatWindow.swift | 32 +++++--- .../Sources/Clawdis/WorkActivityStore.swift | 30 +++---- apps/macos/Sources/ClawdisCLI/main.swift | 3 +- .../Tests/ClawdisIPCTests/AgentRPCTests.swift | 2 +- .../CommandResolverTests.swift | 15 ++-- .../ClawdisIPCTests/HealthDecodeTests.swift | 6 +- .../Tests/ClawdisIPCTests/Placeholder.swift | 1 - .../VoiceWakeRuntimeTests.swift | 3 +- 32 files changed, 416 insertions(+), 236 deletions(-) diff --git a/apps/macos/Sources/Clawdis/AgentEventStore.swift b/apps/macos/Sources/Clawdis/AgentEventStore.swift index afee39be2..369a5d687 100644 --- a/apps/macos/Sources/Clawdis/AgentEventStore.swift +++ b/apps/macos/Sources/Clawdis/AgentEventStore.swift @@ -9,8 +9,8 @@ final class AgentEventStore: ObservableObject { func append(_ event: ControlAgentEvent) { self.events.append(event) - if self.events.count > maxEvents { - self.events.removeFirst(self.events.count - maxEvents) + if self.events.count > self.maxEvents { + self.events.removeFirst(self.events.count - self.maxEvents) } } diff --git a/apps/macos/Sources/Clawdis/AgentEventsWindow.swift b/apps/macos/Sources/Clawdis/AgentEventsWindow.swift index b6fd4f99b..b769d151d 100644 --- a/apps/macos/Sources/Clawdis/AgentEventsWindow.swift +++ b/apps/macos/Sources/Clawdis/AgentEventsWindow.swift @@ -10,14 +10,14 @@ struct AgentEventsWindow: View { Text("Agent Events") .font(.title3.weight(.semibold)) Spacer() - Button("Clear") { store.clear() } + Button("Clear") { self.store.clear() } .buttonStyle(.bordered) } .padding(.bottom, 4) ScrollView { LazyVStack(alignment: .leading, spacing: 8) { - ForEach(store.events.reversed(), id: \.seq) { evt in + ForEach(self.store.events.reversed(), id: \.seq) { evt in EventRow(event: evt) } } @@ -34,14 +34,14 @@ private struct EventRow: View { var body: some View { VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { - Text(event.stream.uppercased()) + Text(self.event.stream.uppercased()) .font(.caption2.weight(.bold)) .padding(.horizontal, 6) .padding(.vertical, 2) .background(self.tint) .foregroundStyle(Color.white) .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) - Text("run " + event.runId) + Text("run " + self.event.runId) .font(.caption.monospaced()) .foregroundStyle(.secondary) Spacer() @@ -61,16 +61,15 @@ private struct EventRow: View { .padding(8) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(Color.primary.opacity(0.04)) - ) + .fill(Color.primary.opacity(0.04))) } private var tint: Color { - switch event.stream { - case "job": return .blue - case "tool": return .orange - case "assistant": return .green - default: return .gray + switch self.event.stream { + case "job": .blue + case "tool": .orange + case "assistant": .green + default: .gray } } diff --git a/apps/macos/Sources/Clawdis/AgentRPC.swift b/apps/macos/Sources/Clawdis/AgentRPC.swift index 7805b60e8..21db00875 100644 --- a/apps/macos/Sources/Clawdis/AgentRPC.swift +++ b/apps/macos/Sources/Clawdis/AgentRPC.swift @@ -1,5 +1,5 @@ -import Foundation import Darwin +import Foundation import OSLog struct ControlRequestParams: @unchecked Sendable { diff --git a/apps/macos/Sources/Clawdis/AppState.swift b/apps/macos/Sources/Clawdis/AppState.swift index fa00a67a9..2211a7d07 100644 --- a/apps/macos/Sources/Clawdis/AppState.swift +++ b/apps/macos/Sources/Clawdis/AppState.swift @@ -182,7 +182,8 @@ final class AppState: ObservableObject { UserDefaults.standard.set(true, forKey: heartbeatsEnabledKey) } if let storedOverride = UserDefaults.standard.string(forKey: iconOverrideKey), - let selection = IconOverrideSelection(rawValue: storedOverride) { + let selection = IconOverrideSelection(rawValue: storedOverride) + { self.iconOverride = selection } else { self.iconOverride = .system diff --git a/apps/macos/Sources/Clawdis/ConfigSettings.swift b/apps/macos/Sources/Clawdis/ConfigSettings.swift index 724a96f1a..ba4c1a9d1 100644 --- a/apps/macos/Sources/Clawdis/ConfigSettings.swift +++ b/apps/macos/Sources/Clawdis/ConfigSettings.swift @@ -108,7 +108,11 @@ struct ConfigSettings: View { .frame(width: 100) .disabled(!self.webChatEnabled) } - Text("Mac app connects to the relay’s loopback web chat on this port. Remote mode uses SSH -L to forward it.") + Text( + """ + Mac app connects to the relay’s loopback web chat on this port. + Remote mode uses SSH -L to forward it. + """) .font(.footnote) .foregroundStyle(.secondary) .frame(maxWidth: 480, alignment: .leading) diff --git a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift index 64de26f66..ccf0b86b2 100644 --- a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift +++ b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift @@ -113,8 +113,7 @@ struct CritterStatusLabel: View { .padding(3) .background( Circle() - .fill(self.iconState.tint.opacity(0.9)) - ) + .fill(self.iconState.tint.opacity(0.9))) .foregroundStyle(Color.white) .offset(x: -4, y: -2) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading) diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index 13da3f778..33436a760 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -194,7 +194,10 @@ struct DebugSettings: View { .foregroundStyle(.red) } else { Text( - "Uses the Voice Wake path: forwards over SSH when configured, otherwise runs locally via rpc.") + """ + Uses the Voice Wake path: forwards over SSH when configured, + otherwise runs locally via rpc. + """) .font(.caption) .foregroundStyle(.secondary) } @@ -289,7 +292,10 @@ struct DebugSettings: View { self.debugSendStatus = nil } - let message = "This is a debug test from the Mac app. Reply with \"Debug test works (and a funny pun)\" if you received that." + let message = """ + This is a debug test from the Mac app. Reply with "Debug test works (and a funny pun)" \ + if you received that. + """ let config = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } let shouldForward = config.enabled diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift index 2d59ea29b..d4de3b6e6 100644 --- a/apps/macos/Sources/Clawdis/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift @@ -137,57 +137,57 @@ struct GeneralSettings: View { .disabled(self.remoteStatus == .checking || self.state.remoteTarget .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - switch self.remoteStatus { - case .idle: - EmptyView() - case .checking: - Text("Checking…").font(.caption).foregroundStyle(.secondary) + switch self.remoteStatus { + case .idle: + EmptyView() + case .checking: + Text("Checking…").font(.caption).foregroundStyle(.secondary) case .ok: Label("Ready", systemImage: "checkmark.circle.fill") .font(.caption) .foregroundStyle(.green) case let .failed(message): - Text(message) + Text(message) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + + // Diagnostics + VStack(alignment: .leading, spacing: 4) { + Text("Control channel") + .font(.caption.weight(.semibold)) + Text(self.controlStatusLine) .font(.caption) .foregroundStyle(.secondary) - .lineLimit(2) + if let ping = ControlChannel.shared.lastPingMs { + Text("Last ping: \(Int(ping)) ms") + .font(.caption) + .foregroundStyle(.secondary) + } + if let hb = HeartbeatStore.shared.lastEvent { + let ageText = age(from: Date(timeIntervalSince1970: hb.ts / 1000)) + Text("Last heartbeat: \(hb.status) · \(ageText)") + .font(.caption) + .foregroundStyle(.secondary) + } } - } - // Diagnostics - VStack(alignment: .leading, spacing: 4) { - Text("Control channel") - .font(.caption.weight(.semibold)) - Text(self.controlStatusLine) - .font(.caption) + Text("Tip: enable Tailscale for stable remote access.") + .font(.footnote) .foregroundStyle(.secondary) - if let ping = ControlChannel.shared.lastPingMs { - Text("Last ping: \(Int(ping)) ms") - .font(.caption) - .foregroundStyle(.secondary) - } - if let hb = HeartbeatStore.shared.lastEvent { - let ageText = age(from: Date(timeIntervalSince1970: hb.ts / 1000)) - Text("Last heartbeat: \(hb.status) · \(ageText)") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - Text("Tip: enable Tailscale for stable remote access.") - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) + .lineLimit(1) } .transition(.opacity) } private var controlStatusLine: String { switch ControlChannel.shared.state { - case .connected: return "Connected" - case .connecting: return "Connecting…" - case .disconnected: return "Disconnected" - case let .degraded(msg): return "Degraded: \(msg)" + case .connected: "Connected" + case .connecting: "Connecting…" + case .disconnected: "Disconnected" + case let .degraded(msg): "Degraded: \(msg)" } } @@ -276,8 +276,10 @@ struct GeneralSettings: View { .font(.caption) .foregroundStyle(.secondary) if let recent = snap.sessions.recent.first { - Text( - "Last activity: \(recent.key) \(recent.updatedAt != nil ? relativeAge(from: Date(timeIntervalSince1970: (recent.updatedAt ?? 0) / 1000)) : "unknown")") + let lastActivity = recent.updatedAt != nil + ? relativeAge(from: Date(timeIntervalSince1970: (recent.updatedAt ?? 0) / 1000)) + : "unknown" + Text("Last activity: \(recent.key) \(lastActivity)") .font(.caption) .foregroundStyle(.secondary) } @@ -386,7 +388,9 @@ extension GeneralSettings { // Step 2: control channel health over tunnel let originalMode = AppStateStore.shared.connectionMode do { - try await ControlChannel.shared.configure(mode: .remote(target: settings.target, identity: settings.identity)) + try await ControlChannel.shared.configure(mode: .remote( + target: settings.target, + identity: settings.identity)) let data = try await ControlChannel.shared.health(timeout: 10) if decodeHealthSnapshot(from: data) != nil { self.remoteStatus = .ok diff --git a/apps/macos/Sources/Clawdis/HealthStore.swift b/apps/macos/Sources/Clawdis/HealthStore.swift index f4dc83df4..0db1a2b68 100644 --- a/apps/macos/Sources/Clawdis/HealthStore.swift +++ b/apps/macos/Sources/Clawdis/HealthStore.swift @@ -1,7 +1,7 @@ import Foundation +import Network import OSLog import SwiftUI -import Network struct HealthSnapshot: Codable, Sendable { struct Web: Codable, Sendable { diff --git a/apps/macos/Sources/Clawdis/HeartbeatStore.swift b/apps/macos/Sources/Clawdis/HeartbeatStore.swift index 382136e0d..bc3608ae3 100644 --- a/apps/macos/Sources/Clawdis/HeartbeatStore.swift +++ b/apps/macos/Sources/Clawdis/HeartbeatStore.swift @@ -13,12 +13,13 @@ final class HeartbeatStore: ObservableObject { self.observer = NotificationCenter.default.addObserver( forName: .controlHeartbeat, object: nil, - queue: .main) { [weak self] note in - guard let data = note.object as? Data else { return } - if let decoded = try? JSONDecoder().decode(ControlHeartbeatEvent.self, from: data) { - Task { @MainActor in self?.lastEvent = decoded } - } + queue: .main) + { [weak self] note in + guard let data = note.object as? Data else { return } + if let decoded = try? JSONDecoder().decode(ControlHeartbeatEvent.self, from: data) { + Task { @MainActor in self?.lastEvent = decoded } } + } Task { if self.lastEvent == nil { diff --git a/apps/macos/Sources/Clawdis/IconState.swift b/apps/macos/Sources/Clawdis/IconState.swift index b2b80ca1f..f91f79a5f 100644 --- a/apps/macos/Sources/Clawdis/IconState.swift +++ b/apps/macos/Sources/Clawdis/IconState.swift @@ -23,28 +23,28 @@ enum IconState: Equatable { var glyph: String { switch self.activity { - case .tool(.bash): return "💻" - case .tool(.read): return "📄" - case .tool(.write): return "✍️" - case .tool(.edit): return "📝" - case .tool(.attach): return "📎" - case .tool(.other), .job: return "🛠️" + case .tool(.bash): "💻" + case .tool(.read): "📄" + case .tool(.write): "✍️" + case .tool(.edit): "📝" + case .tool(.attach): "📎" + case .tool(.other), .job: "🛠️" } } var tint: Color { switch self { - case .workingMain: return .accentColor - case .workingOther: return .gray - case .overridden: return .orange - case .idle: return .clear + case .workingMain: .accentColor + case .workingOther: .gray + case .overridden: .orange + case .idle: .clear } } var isWorking: Bool { switch self { - case .idle: return false - default: return true + case .idle: false + default: true } } @@ -53,9 +53,9 @@ enum IconState: Equatable { case let .workingMain(kind), let .workingOther(kind), let .overridden(kind): - return kind + kind case .idle: - return .job + .job } } } @@ -70,18 +70,18 @@ enum IconOverrideSelection: String, CaseIterable, Identifiable { var label: String { switch self { - case .system: return "System (auto)" - case .idle: return "Idle" - case .mainBash: return "Working main – bash" - case .mainRead: return "Working main – read" - case .mainWrite: return "Working main – write" - case .mainEdit: return "Working main – edit" - case .mainOther: return "Working main – other" - case .otherBash: return "Working other – bash" - case .otherRead: return "Working other – read" - case .otherWrite: return "Working other – write" - case .otherEdit: return "Working other – edit" - case .otherOther: return "Working other – other" + case .system: "System (auto)" + case .idle: "Idle" + case .mainBash: "Working main – bash" + case .mainRead: "Working main – read" + case .mainWrite: "Working main – write" + case .mainEdit: "Working main – edit" + case .mainOther: "Working main – other" + case .otherBash: "Working other – bash" + case .otherRead: "Working other – read" + case .otherWrite: "Working other – write" + case .otherEdit: "Working other – edit" + case .otherOther: "Working other – other" } } diff --git a/apps/macos/Sources/Clawdis/InstancesSettings.swift b/apps/macos/Sources/Clawdis/InstancesSettings.swift index 569206fbb..3e8f6da54 100644 --- a/apps/macos/Sources/Clawdis/InstancesSettings.swift +++ b/apps/macos/Sources/Clawdis/InstancesSettings.swift @@ -5,24 +5,24 @@ struct InstancesSettings: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - header + self.header if let err = store.lastError { Text("Error: \(err)") .foregroundStyle(.red) } - if store.instances.isEmpty { + if self.store.instances.isEmpty { Text("No instances reported yet.") .foregroundStyle(.secondary) } else { - List(store.instances) { inst in - instanceRow(inst) + List(self.store.instances) { inst in + self.instanceRow(inst) } .listStyle(.inset) } Spacer() } - .onAppear { store.start() } - .onDisappear { store.stop() } + .onAppear { self.store.start() } + .onDisappear { self.store.stop() } } private var header: some View { @@ -35,10 +35,10 @@ struct InstancesSettings: View { .foregroundStyle(.secondary) } Spacer() - if store.isLoading { + if self.store.isLoading { ProgressView() } else { - Button("Refresh") { Task { await store.refresh() } } + Button("Refresh") { Task { await self.store.refresh() } } } } } @@ -52,12 +52,12 @@ struct InstancesSettings: View { } HStack(spacing: 8) { if let version = inst.version { - label(icon: "shippingbox", text: version) + self.label(icon: "shippingbox", text: version) } - label(icon: "clock", text: inst.lastInputDescription) - if let mode = inst.mode { label(icon: "network", text: mode) } + self.label(icon: "clock", text: inst.lastInputDescription) + if let mode = inst.mode { self.label(icon: "network", text: mode) } if let reason = inst.reason, !reason.isEmpty { - label(icon: "info.circle", text: reason) + self.label(icon: "info.circle", text: reason) } } Text(inst.text) diff --git a/apps/macos/Sources/Clawdis/InstancesStore.swift b/apps/macos/Sources/Clawdis/InstancesStore.swift index 438c88c17..18f484717 100644 --- a/apps/macos/Sources/Clawdis/InstancesStore.swift +++ b/apps/macos/Sources/Clawdis/InstancesStore.swift @@ -64,6 +64,7 @@ final class InstancesStore: ObservableObject { self.logger.error("instances fetch returned empty payload") self.instances = [self.localFallbackInstance()] self.lastError = "No presence data returned from relay yet." + await self.probeHealthIfNeeded() return } let decoded = try JSONDecoder().decode([InstanceInfo].self, from: data) @@ -83,6 +84,7 @@ final class InstancesStore: ObservableObject { if withIDs.isEmpty { self.instances = [self.localFallbackInstance()] self.lastError = nil + await self.probeHealthIfNeeded() } else { self.instances = withIDs self.lastError = nil @@ -93,10 +95,10 @@ final class InstancesStore: ObservableObject { instances fetch failed: \(error.localizedDescription, privacy: .public) \ len=\(self.lastPayload?.count ?? 0, privacy: .public) \ utf8=\(self.snippet(self.lastPayload), privacy: .public) - """ - ) + """) self.instances = [self.localFallbackInstance()] self.lastError = "Decode failed: \(error.localizedDescription)" + await self.probeHealthIfNeeded() } } @@ -126,7 +128,7 @@ final class InstancesStore: ObservableObject { } private static func primaryIPv4Address() -> String? { - var addrList: UnsafeMutablePointer? = nil + var addrList: UnsafeMutablePointer? guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } defer { freeifaddrs(addrList) } @@ -143,10 +145,18 @@ final class InstancesStore: ObservableObject { var addr = ptr.pointee.ifa_addr.pointee var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo(&addr, socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), &buffer, socklen_t(buffer.count), nil, 0, NI_NUMERICHOST) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) guard result == 0 else { continue } let len = buffer.prefix { $0 != 0 } - let ip = String(decoding: len.map { UInt8(bitPattern: $0) }, as: UTF8.self) + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } if name == "en0" { en0 = ip; break } if fallback == nil { fallback = ip } @@ -169,4 +179,27 @@ final class InstancesStore: ObservableObject { } return "<\(data.count) bytes non-utf8>" } + + private func probeHealthIfNeeded() async { + do { + let data = try await ControlChannel.shared.health(timeout: 8) + guard let snap = decodeHealthSnapshot(from: data) else { return } + let entry = InstanceInfo( + id: "health-\(snap.ts)", + host: "relay (health)", + ip: nil, + version: nil, + lastInputSeconds: nil, + mode: "health", + reason: "health probe", + text: "Health ok · linked=\(snap.web.linked) · ipc.exists=\(snap.ipc.exists)", + ts: snap.ts) + if !self.instances.contains(where: { $0.id == entry.id }) { + self.instances.insert(entry, at: 0) + } + self.lastError = nil + } catch { + self.logger.error("instances health probe failed: \(error.localizedDescription, privacy: .public)") + } + } } diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 9d293a137..45487c4a4 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -246,7 +246,10 @@ struct OnboardingView: View { .foregroundStyle(.secondary) } else if !self.cliInstalled, self.cliInstallLocation == nil { Text( - "We install into /usr/local/bin and /opt/homebrew/bin. Rerun anytime if you move the build output.") + """ + We install into /usr/local/bin and /opt/homebrew/bin. + Rerun anytime if you move the build output. + """) .font(.footnote) .foregroundStyle(.secondary) } @@ -259,7 +262,10 @@ struct OnboardingView: View { Text("Link WhatsApp") .font(.largeTitle.weight(.semibold)) Text( - "Run `clawdis login` where the relay runs (local if local mode, remote if remote). Scan the QR to pair your account.") + """ + Run `clawdis login` where the relay runs (local if local mode, remote if remote). + Scan the QR to pair your account. + """) .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -273,11 +279,17 @@ struct OnboardingView: View { systemImage: "terminal") self.featureRow( title: "Run `clawdis login --verbose`", - subtitle: "Scan the QR code with WhatsApp on your phone. We only use your personal session; no cloud relay involved.", + subtitle: """ + Scan the QR code with WhatsApp on your phone. + We only use your personal session; no cloud relay involved. + """, systemImage: "qrcode.viewfinder") self.featureRow( title: "Re-link after timeouts", - subtitle: "If Baileys auth expires, re-run login on that host. Settings → General shows remote/local mode so you know where to run it.", + subtitle: """ + If Baileys auth expires, re-run login on that host. + Settings → General shows remote/local mode so you know where to run it. + """, systemImage: "clock.arrow.circlepath") } } @@ -290,8 +302,10 @@ struct OnboardingView: View { self.onboardingCard { self.featureRow( title: "Run the dashboard", - subtitle: "Use the CLI helper from your scripts, and reopen onboarding from " - + "Settings if you add a new user.", + subtitle: """ + Use the CLI helper from your scripts, and reopen onboarding from Settings + if you add a new user. + """, systemImage: "checkmark.seal") self.featureRow( title: "Test a notification", diff --git a/apps/macos/Sources/Clawdis/PresenceReporter.swift b/apps/macos/Sources/Clawdis/PresenceReporter.swift index 452dbaf6b..29708e354 100644 --- a/apps/macos/Sources/Clawdis/PresenceReporter.swift +++ b/apps/macos/Sources/Clawdis/PresenceReporter.swift @@ -69,7 +69,7 @@ final class PresenceReporter { } private static func primaryIPv4Address() -> String? { - var addrList: UnsafeMutablePointer? = nil + var addrList: UnsafeMutablePointer? guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } defer { freeifaddrs(addrList) } @@ -86,10 +86,18 @@ final class PresenceReporter { var addr = ptr.pointee.ifa_addr.pointee var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) - let result = getnameinfo(&addr, socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), &buffer, socklen_t(buffer.count), nil, 0, NI_NUMERICHOST) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) guard result == 0 else { continue } let len = buffer.prefix { $0 != 0 } - let ip = String(decoding: len.map { UInt8(bitPattern: $0) }, as: UTF8.self) + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } if name == "en0" { en0 = ip; break } if fallback == nil { fallback = ip } diff --git a/apps/macos/Sources/Clawdis/RuntimeLocator.swift b/apps/macos/Sources/Clawdis/RuntimeLocator.swift index 64c236b85..50734cb61 100644 --- a/apps/macos/Sources/Clawdis/RuntimeLocator.swift +++ b/apps/macos/Sources/Clawdis/RuntimeLocator.swift @@ -10,7 +10,7 @@ struct RuntimeVersion: Comparable, CustomStringConvertible { let minor: Int let patch: Int - var description: String { "\(major).\(minor).\(patch)" } + var description: String { "\(self.major).\(self.minor).\(self.patch)" } static func < (lhs: RuntimeVersion, rhs: RuntimeVersion) -> Bool { if lhs.major != rhs.major { return lhs.major < rhs.major } @@ -41,7 +41,12 @@ struct RuntimeResolution { enum RuntimeResolutionError: Error { case notFound(searchPaths: [String], preferred: String?) - case unsupported(kind: RuntimeKind, found: RuntimeVersion, required: RuntimeVersion, path: String, searchPaths: [String]) + case unsupported( + kind: RuntimeKind, + found: RuntimeVersion, + required: RuntimeVersion, + path: String, + searchPaths: [String]) case versionParse(kind: RuntimeKind, raw: String, path: String, searchPaths: [String]) } @@ -51,22 +56,31 @@ enum RuntimeLocator { static func resolve( preferred: String? = ProcessInfo.processInfo.environment["CLAWDIS_RUNTIME"], - searchPaths: [String] = CommandResolver.preferredPaths() - ) -> Result { - let order = runtimeOrder(preferred: preferred) + searchPaths: [String] = CommandResolver.preferredPaths()) -> Result + { + let order = self.runtimeOrder(preferred: preferred) let pathEnv = searchPaths.joined(separator: ":") for runtime in order { guard let binary = findExecutable(named: runtime.binaryName, searchPaths: searchPaths) else { continue } guard let rawVersion = readVersion(of: binary, pathEnv: pathEnv) else { - return .failure(.versionParse(kind: runtime, raw: "(unreadable)", path: binary, searchPaths: searchPaths)) + return .failure(.versionParse( + kind: runtime, + raw: "(unreadable)", + path: binary, + searchPaths: searchPaths)) } guard let parsed = RuntimeVersion.from(string: rawVersion) else { return .failure(.versionParse(kind: runtime, raw: rawVersion, path: binary, searchPaths: searchPaths)) } - let minimum = runtime == .bun ? minBun : minNode + let minimum = runtime == .bun ? self.minBun : self.minNode guard parsed >= minimum else { - return .failure(.unsupported(kind: runtime, found: parsed, required: minimum, path: binary, searchPaths: searchPaths)) + return .failure(.unsupported( + kind: runtime, + found: parsed, + required: minimum, + path: binary, + searchPaths: searchPaths)) } return .success(RuntimeResolution(kind: runtime, path: binary, version: parsed)) } @@ -86,10 +100,11 @@ enum RuntimeLocator { "Install Bun: https://bun.sh/docs/installation", ].joined(separator: "\n") case let .unsupported(kind, found, required, path, searchPaths): + let fallbackRuntime = kind == .bun ? "node" : "bun" return [ "Found \(kind.rawValue) \(found) at \(path) but need >= \(required).", "PATH searched: \(searchPaths.joined(separator: ":"))", - "Upgrade \(kind.rawValue) or set CLAWDIS_RUNTIME=\(kind == .bun ? "node" : "bun") to try the other runtime.", + "Upgrade \(kind.rawValue) or set CLAWDIS_RUNTIME=\(fallbackRuntime) to try the other runtime.", ].joined(separator: "\n") case let .versionParse(kind, raw, path, searchPaths): return [ @@ -143,7 +158,6 @@ enum RuntimeLocator { } } -private extension RuntimeKind { - var binaryName: String { self == .bun ? "bun" : "node" } +extension RuntimeKind { + fileprivate var binaryName: String { self == .bun ? "bun" : "node" } } - diff --git a/apps/macos/Sources/Clawdis/SoundEffects.swift b/apps/macos/Sources/Clawdis/SoundEffects.swift index 3470e766c..c1f0cb9b4 100644 --- a/apps/macos/Sources/Clawdis/SoundEffects.swift +++ b/apps/macos/Sources/Clawdis/SoundEffects.swift @@ -1,7 +1,7 @@ import AppKit import Foundation -struct SoundEffectCatalog { +enum SoundEffectCatalog { /// All discoverable system sound names, with "Glass" pinned first. static var systemOptions: [String] { var names = Set(Self.discoveredSoundMap.keys).union(Self.fallbackNames) @@ -13,7 +13,7 @@ struct SoundEffectCatalog { static func displayName(for raw: String) -> String { raw } static func url(for name: String) -> URL? { - Self.discoveredSoundMap[name] + self.discoveredSoundMap[name] } // MARK: - Internals diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index 6a160d34a..bf95e7df7 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -225,8 +225,8 @@ enum CommandResolver { runtime: RuntimeResolution, entrypoint: String, subcommand: String, - extraArgs: [String] - ) -> [String] { + extraArgs: [String]) -> [String] + { [runtime.path, entrypoint, subcommand] + extraArgs } @@ -332,11 +332,19 @@ enum CommandResolver { if let relay = self.bundledRelayRoot(), let entry = self.relayEntrypoint(in: relay) { - return self.makeRuntimeCommand(runtime: runtime, entrypoint: entry, subcommand: subcommand, extraArgs: extraArgs) + return self.makeRuntimeCommand( + runtime: runtime, + entrypoint: entry, + subcommand: subcommand, + extraArgs: extraArgs) } if let entry = self.relayEntrypoint(in: self.projectRoot()) { - return self.makeRuntimeCommand(runtime: runtime, entrypoint: entry, subcommand: subcommand, extraArgs: extraArgs) + return self.makeRuntimeCommand( + runtime: runtime, + entrypoint: entry, + subcommand: subcommand, + extraArgs: extraArgs) } if let clawdisPath = self.clawdisExecutable() { @@ -347,7 +355,9 @@ enum CommandResolver { return [pnpm, "--silent", "clawdis", subcommand] + extraArgs } - let missingEntry = "clawdis entrypoint missing (looked for dist/index.js or bin/clawdis.js); run pnpm build." + let missingEntry = """ + clawdis entrypoint missing (looked for dist/index.js or bin/clawdis.js); run pnpm build. + """ return self.errorCommand(with: missingEntry) case let .failure(error): @@ -390,24 +400,32 @@ enum CommandResolver { args.append(userHost) // Run the real clawdis CLI on the remote host; do not fall back to clawdis-mac. - let exportedPath = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm:$PATH" + let exportedPath = [ + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/Users/steipete/Library/pnpm", + "$PATH", + ].joined(separator: ":") let quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ") let userPRJ = settings.projectRoot.trimmingCharacters(in: .whitespacesAndNewlines) - let projectSection: String - if userPRJ.isEmpty { - projectSection = """ - DEFAULT_PRJ="$HOME/Projects/clawdis" - if [ -d "$DEFAULT_PRJ" ]; then - PRJ="$DEFAULT_PRJ" - cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; } - fi - """ + let projectSection = if userPRJ.isEmpty { + """ + DEFAULT_PRJ="$HOME/Projects/clawdis" + if [ -d "$DEFAULT_PRJ" ]; then + PRJ="$DEFAULT_PRJ" + cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; } + fi + """ } else { - projectSection = """ - PRJ=\(self.shellQuote(userPRJ)) - cd \(self.shellQuote(userPRJ)) || { echo "Project root not found: \(userPRJ)"; exit 127; } - """ + """ + PRJ=\(self.shellQuote(userPRJ)) + cd \(self.shellQuote(userPRJ)) || { echo "Project root not found: \(userPRJ)"; exit 127; } + """ } let scriptBody = """ @@ -448,7 +466,11 @@ enum CommandResolver { return ["/usr/bin/ssh"] + args } - private static func sshMacHelperCommand(subcommand: String, extraArgs: [String], settings: RemoteSettings) -> [String]? { + private static func sshMacHelperCommand( + subcommand: String, + extraArgs: [String], + settings: RemoteSettings) -> [String]? + { guard !settings.target.isEmpty else { return nil } guard let parsed = self.parseSSHTarget(settings.target) else { return nil } diff --git a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift index 4b00785e6..ff3d98cf6 100644 --- a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift @@ -55,14 +55,14 @@ final class VoicePushToTalkHotkey { } let chordActive = self.optionDown - if chordActive && !self.active { + if chordActive, !self.active { self.active = true Task { Logger(subsystem: "com.steipete.clawdis", category: "voicewake.ptt") .info("ptt hotkey down") await VoicePushToTalk.shared.begin() } - } else if !chordActive && self.active { + } else if !chordActive, self.active { self.active = false Task { Logger(subsystem: "com.steipete.clawdis", category: "voicewake.ptt") @@ -126,7 +126,10 @@ actor VoicePushToTalk { // Pause the always-on wake word recognizer so both pipelines don't fight over the mic tap. await VoiceWakeRuntime.shared.pauseForPushToTalk() let adoptedPrefix = self.adoptedPrefix - let adoptedAttributed: NSAttributedString? = adoptedPrefix.isEmpty ? nil : Self.makeAttributed(committed: adoptedPrefix, volatile: "", isFinal: false) + let adoptedAttributed: NSAttributedString? = adoptedPrefix.isEmpty ? nil : Self.makeAttributed( + committed: adoptedPrefix, + volatile: "", + isFinal: false) self.overlayToken = await MainActor.run { VoiceWakeOverlayController.shared.startSession( source: .pushToTalk, @@ -166,7 +169,10 @@ actor VoicePushToTalk { let locale = localeID.flatMap { Locale(identifier: $0) } ?? Locale(identifier: Locale.current.identifier) self.recognizer = SFSpeechRecognizer(locale: locale) guard let recognizer, recognizer.isAvailable else { - throw NSError(domain: "VoicePushToTalk", code: 1, userInfo: [NSLocalizedDescriptionKey: "Recognizer unavailable"]) + throw NSError( + domain: "VoicePushToTalk", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Recognizer unavailable"]) } self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest() @@ -216,7 +222,10 @@ actor VoicePushToTalk { let attributed = Self.makeAttributed(committed: committedWithPrefix, volatile: self.volatile, isFinal: isFinal) if let token = self.overlayToken { await MainActor.run { - VoiceWakeOverlayController.shared.updatePartial(token: token, transcript: snapshot, attributed: attributed) + VoiceWakeOverlayController.shared.updatePartial( + token: token, + transcript: snapshot, + attributed: attributed) } } } @@ -238,11 +247,10 @@ actor VoicePushToTalk { committed: Self.join(self.adoptedPrefix, self.committed), volatile: self.volatile, isFinal: true) - let forward: VoiceWakeForwardConfig - if let cached = self.activeConfig?.forwardConfig { - forward = cached + let forward: VoiceWakeForwardConfig = if let cached = self.activeConfig?.forwardConfig { + cached } else { - forward = await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } + await MainActor.run { AppStateStore.shared.voiceWakeForwardConfig } } let chime = finalText.isEmpty ? .none : (self.activeConfig?.sendChime ?? .none) diff --git a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift index 3eface4eb..5f64e6b77 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeChime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeChime.swift @@ -17,16 +17,16 @@ enum VoiceWakeChime: Codable, Equatable, Sendable { var displayLabel: String { switch self { case .none: - return "No Sound" + "No Sound" case let .system(name): - return VoiceWakeChimeCatalog.displayName(for: name) + VoiceWakeChimeCatalog.displayName(for: name) case let .custom(displayName, _): - return displayName + displayName } } } -struct VoiceWakeChimeCatalog { +enum VoiceWakeChimeCatalog { /// Options shown in the picker. static var systemOptions: [String] { SoundEffectCatalog.systemOptions } @@ -57,12 +57,13 @@ enum VoiceWakeChimePlayer { private static func sound(for chime: VoiceWakeChime) -> NSSound? { switch chime { case .none: - return nil + nil + case let .system(name): - return SoundEffectPlayer.sound(named: name) + SoundEffectPlayer.sound(named: name) case let .custom(_, bookmark): - return SoundEffectPlayer.sound(from: bookmark) + SoundEffectPlayer.sound(from: bookmark) } } } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift index be8980d04..744a0c2f4 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeForwarder.swift @@ -20,7 +20,12 @@ enum VoiceWakeForwarder { ?? ProcessInfo.processInfo.hostName let safeMachine = resolvedMachine.isEmpty ? "this Mac" : resolvedMachine - return "User talked via voice recognition on \(safeMachine) - repeat prompt first + remember some words might be incorrectly transcribed.\n\n\(transcript)" + return """ + User talked via voice recognition on \(safeMachine) - repeat prompt first \ + + remember some words might be incorrectly transcribed. + + \(transcript) + """ } static func clearCliCache() { @@ -33,8 +38,8 @@ enum VoiceWakeForwarder { var errorDescription: String? { switch self { - case let .rpcFailed(message): return message - case .disabled: return "Voice wake forwarding disabled" + case let .rpcFailed(message): message + case .disabled: "Voice wake forwarding disabled" } } } @@ -149,7 +154,7 @@ enum VoiceWakeForwarder { } continue } - if ch.isWhitespace && quote == nil { + if ch.isWhitespace, quote == nil { flush() continue } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index abd0d0022..99e0392f8 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -20,7 +20,7 @@ final class VoiceWakeOverlayController: ObservableObject { var isVisible: Bool = false var forwardEnabled: Bool = false var isSending: Bool = false - var attributed: NSAttributedString = NSAttributedString(string: "") + var attributed: NSAttributedString = .init(string: "") var isOverflowing: Bool = false var isEditing: Bool = false var level: Double = 0 // normalized 0...1 speech level for UI @@ -52,7 +52,11 @@ final class VoiceWakeOverlayController: ObservableObject { isFinal: Bool = false) -> UUID { let token = UUID() - self.logger.log(level: .info, "overlay session_start source=\(source.rawValue, privacy: .public) len=\(transcript.count, privacy: .public)") + let message = """ + overlay session_start source=\(source.rawValue, privacy: .public) \ + len=\(transcript.count, privacy: .public) + """ + self.logger.log(level: .info, "\(message)") self.activeToken = token self.activeSource = source self.forwardConfig = nil @@ -76,7 +80,11 @@ final class VoiceWakeOverlayController: ObservableObject { func updatePartial(token: UUID, transcript: String, attributed: NSAttributedString? = nil) { guard self.guardToken(token, context: "partial") else { return } guard !self.model.isFinal else { return } - self.logger.log(level: .info, "overlay partial token=\(token.uuidString, privacy: .public) len=\(transcript.count, privacy: .public)") + let message = """ + overlay partial token=\(token.uuidString, privacy: .public) \ + len=\(transcript.count, privacy: .public) + """ + self.logger.log(level: .info, "\(message)") self.autoSendTask?.cancel(); self.autoSendTask = nil; self.autoSendToken = nil self.forwardConfig = nil self.model.text = transcript @@ -99,7 +107,13 @@ final class VoiceWakeOverlayController: ObservableObject { attributed: NSAttributedString? = nil) { guard self.guardToken(token, context: "final") else { return } - self.logger.log(level: .info, "overlay presentFinal token=\(token.uuidString, privacy: .public) len=\(transcript.count, privacy: .public) autoSendAfter=\(delay ?? -1, privacy: .public) forwardEnabled=\(forwardConfig.enabled, privacy: .public)") + let message = """ + overlay presentFinal token=\(token.uuidString, privacy: .public) \ + len=\(transcript.count, privacy: .public) \ + autoSendAfter=\(delay ?? -1, privacy: .public) \ + forwardEnabled=\(forwardConfig.enabled, privacy: .public) + """ + self.logger.log(level: .info, "\(message)") self.autoSendTask?.cancel() self.autoSendToken = token self.forwardConfig = forwardConfig @@ -142,7 +156,13 @@ final class VoiceWakeOverlayController: ObservableObject { func sendNow(token: UUID? = nil, sendChime: VoiceWakeChime = .none) { guard self.guardToken(token, context: "send") else { return } - self.logger.log(level: .info, "overlay sendNow called token=\(self.activeToken?.uuidString ?? "nil", privacy: .public) isSending=\(self.model.isSending, privacy: .public) forwardEnabled=\(self.model.forwardEnabled, privacy: .public) textLen=\(self.model.text.count, privacy: .public)") + let message = """ + overlay sendNow called token=\(self.activeToken?.uuidString ?? "nil", privacy: .public) \ + isSending=\(self.model.isSending, privacy: .public) \ + forwardEnabled=\(self.model.forwardEnabled, privacy: .public) \ + textLen=\(self.model.text.count, privacy: .public) + """ + self.logger.log(level: .info, "\(message)") self.autoSendTask?.cancel(); self.autoSendToken = nil if self.model.isSending { return } self.model.isEditing = false @@ -159,7 +179,8 @@ final class VoiceWakeOverlayController: ObservableObject { } if sendChime != .none { - self.logger.log(level: .info, "overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)") + let message = "overlay sendNow playing sendChime=\(String(describing: sendChime), privacy: .public)" + self.logger.log(level: .info, "\(message)") VoiceWakeChimePlayer.play(sendChime, reason: "overlay.send") } @@ -176,7 +197,14 @@ final class VoiceWakeOverlayController: ObservableObject { func dismiss(token: UUID? = nil, reason: DismissReason = .explicit, outcome: SendOutcome = .empty) { guard self.guardToken(token, context: "dismiss") else { return } - self.logger.log(level: .info, "overlay dismiss token=\(self.activeToken?.uuidString ?? "nil", privacy: .public) reason=\(String(describing: reason), privacy: .public) outcome=\(String(describing: outcome), privacy: .public) visible=\(self.model.isVisible, privacy: .public) sending=\(self.model.isSending, privacy: .public)") + let message = """ + overlay dismiss token=\(self.activeToken?.uuidString ?? "nil", privacy: .public) \ + reason=\(String(describing: reason), privacy: .public) \ + outcome=\(String(describing: outcome), privacy: .public) \ + visible=\(self.model.isVisible, privacy: .public) \ + sending=\(self.model.isSending, privacy: .public) + """ + self.logger.log(level: .info, "\(message)") self.autoSendTask?.cancel(); self.autoSendToken = nil self.model.isSending = false self.model.isEditing = false @@ -237,7 +265,9 @@ final class VoiceWakeOverlayController: ObservableObject { guard let window else { return } if !self.model.isVisible { self.model.isVisible = true - self.logger.log(level: .info, "overlay present windowShown textLen=\(self.model.text.count, privacy: .public)") + self.logger.log( + level: .info, + "overlay present windowShown textLen=\(self.model.text.count, privacy: .public)") // Keep the status item in “listening” mode until we explicitly dismiss the overlay. AppStateStore.shared.triggerVoiceEars(ttl: nil) let start = target.offsetBy(dx: 0, dy: -6) @@ -309,7 +339,8 @@ final class VoiceWakeOverlayController: ObservableObject { } private func measuredHeight() -> CGFloat { - let attributed = self.model.attributed.length > 0 ? self.model.attributed : self.makeAttributed(from: self.model.text) + let attributed = self.model.attributed.length > 0 ? self.model.attributed : self + .makeAttributed(from: self.model.text) let maxWidth = self.width - (self.padding * 2) - self.spacing - self.buttonWidth let textInset = NSSize(width: 2, height: 6) @@ -350,7 +381,13 @@ final class VoiceWakeOverlayController: ObservableObject { } private func scheduleAutoSend(token: UUID, after delay: TimeInterval, sendChime: VoiceWakeChime) { - self.logger.log(level: .info, "overlay scheduleAutoSend token=\(token.uuidString, privacy: .public) after=\(delay, privacy: .public) sendChime=\(String(describing: sendChime), privacy: .public)") + self.logger.log( + level: .info, + """ + overlay scheduleAutoSend token=\(token.uuidString, privacy: .public) \ + after=\(delay, privacy: .public) \ + sendChime=\(String(describing: sendChime), privacy: .public) + """) self.autoSendTask?.cancel() self.autoSendToken = token self.autoSendTask = Task { [weak self, sendChime, token] in @@ -360,7 +397,9 @@ final class VoiceWakeOverlayController: ObservableObject { await MainActor.run { guard let self else { return } guard self.guardToken(token, context: "autoSend") else { return } - self.logger.log(level: .info, "overlay autoSend firing token=\(token.uuidString, privacy: .public)") + self.logger.log( + level: .info, + "overlay autoSend firing token=\(token.uuidString, privacy: .public)") self.sendNow(token: token, sendChime: sendChime) self.autoSendTask = nil } @@ -376,6 +415,7 @@ final class VoiceWakeOverlayController: ObservableObject { ]) } } + private struct VoiceWakeOverlayView: View { @ObservedObject var controller: VoiceWakeOverlayController @FocusState private var textFocused: Bool @@ -469,9 +509,8 @@ private struct VoiceWakeOverlayView: View { // Close button rendered above and outside the clipped bubble CloseButtonOverlay( isVisible: self.controller.model.isEditing || self.isHovering || self.closeHovering, - onHover: { self.closeHovering = $0 }) { - self.controller.cancelEditingAndDismiss() - } + onHover: { self.closeHovering = $0 }, + onClose: { self.controller.cancelEditingAndDismiss() }) } .padding(.top, self.controller.closeOverflow) .padding(.leading, self.controller.closeOverflow) @@ -629,7 +668,6 @@ private struct VibrantLabelView: NSViewRepresentable { label.attributedStringValue = self.attributed.strippingForegroundColor() label.textColor = .labelColor } - } private final class ClickCatcher: NSView { @@ -675,8 +713,8 @@ private struct CloseButtonOverlay: View { var body: some View { Group { - if isVisible { - Button(action: onClose) { + if self.isVisible { + Button(action: self.onClose) { Image(systemName: "xmark") .font(.system(size: 12, weight: .bold)) .foregroundColor(Color.white.opacity(0.9)) @@ -695,7 +733,7 @@ private struct CloseButtonOverlay: View { .transition(.opacity) } } - .allowsHitTesting(isVisible) + .allowsHitTesting(self.isVisible) } } @@ -723,7 +761,7 @@ private final class TranscriptNSTextView: NSTextView { self.onEscape?() return } - if isReturn && event.modifierFlags.contains(.command) { + if isReturn, event.modifierFlags.contains(.command) { self.onSend?() return } diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index 7ef4c3544..6e97a57d5 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -136,7 +136,12 @@ actor VoiceWakeRuntime { guard let self else { return } let transcript = result?.bestTranscription.formattedString let isFinal = result?.isFinal ?? false - Task { await self.handleRecognition(transcript: transcript, isFinal: isFinal, error: error, config: config, generation: generation) } + Task { await self.handleRecognition( + transcript: transcript, + isFinal: isFinal, + error: error, + config: config, + generation: generation) } } self.logger.info("voicewake runtime started") @@ -213,7 +218,10 @@ actor VoiceWakeRuntime { let snapshot = self.committedTranscript + self.volatileTranscript if let token = self.overlayToken { await MainActor.run { - VoiceWakeOverlayController.shared.updatePartial(token: token, transcript: snapshot, attributed: attributed) + VoiceWakeOverlayController.shared.updatePartial( + token: token, + transcript: snapshot, + attributed: attributed) } } } @@ -335,10 +343,10 @@ actor VoiceWakeRuntime { VoiceWakeOverlayController.shared.presentFinal( token: token, transcript: finalTranscript, - forwardConfig: forwardConfig, - autoSendAfter: delay, - sendChime: sendChime, - attributed: finalAttributed) + forwardConfig: forwardConfig, + autoSendAfter: delay, + sendChime: sendChime, + attributed: finalAttributed) } } else if forwardConfig.enabled, !finalTranscript.isEmpty { if sendChime != .none { diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index 5c0f86a6e..e8100a92f 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -49,7 +49,10 @@ struct VoiceWakeSettings: View { SettingsToggleRow( title: "Hold Right Option to talk", - subtitle: "Push-to-talk mode that starts listening while you hold the key and shows the preview overlay.", + subtitle: """ + Push-to-talk mode that starts listening while you hold the key + and shows the preview overlay. + """, binding: self.$state.voicePushToTalkEnabled) .disabled(!voiceWakeSupported) diff --git a/apps/macos/Sources/Clawdis/WebChatWindow.swift b/apps/macos/Sources/Clawdis/WebChatWindow.swift index 6e4327562..82f626e17 100644 --- a/apps/macos/Sources/Clawdis/WebChatWindow.swift +++ b/apps/macos/Sources/Clawdis/WebChatWindow.swift @@ -67,7 +67,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private func bootstrap() async { do { guard AppStateStore.webChatEnabled else { - throw NSError(domain: "WebChat", code: 5, userInfo: [NSLocalizedDescriptionKey: "Web chat disabled in settings"]) + throw NSError( + domain: "WebChat", + code: 5, + userInfo: [NSLocalizedDescriptionKey: "Web chat disabled in settings"]) } let endpoint = try await self.prepareEndpoint(remotePort: self.remotePort) self.baseEndpoint = endpoint @@ -90,9 +93,9 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private func prepareEndpoint(remotePort: Int) async throws -> URL { if CommandResolver.connectionModeIsRemote() { - return try await self.startOrRestartTunnel() + try await self.startOrRestartTunnel() } else { - return URL(string: "http://127.0.0.1:\(remotePort)/")! + URL(string: "http://127.0.0.1:\(remotePort)/")! } } @@ -116,11 +119,17 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { let (_, response) = try await session.data(for: request) if let http = response as? HTTPURLResponse { guard (200..<500).contains(http.statusCode) else { - throw NSError(domain: "WebChat", code: http.statusCode, userInfo: [NSLocalizedDescriptionKey: "webchat returned HTTP \(http.statusCode)"]) + throw NSError( + domain: "WebChat", + code: http.statusCode, + userInfo: [NSLocalizedDescriptionKey: "webchat returned HTTP \(http.statusCode)"]) } } } catch { - throw NSError(domain: "WebChat", code: 7, userInfo: [NSLocalizedDescriptionKey: "webchat unreachable: \(error.localizedDescription)"]) + throw NSError( + domain: "WebChat", + code: 7, + userInfo: [NSLocalizedDescriptionKey: "webchat unreachable: \(error.localizedDescription)"]) } } @@ -128,7 +137,7 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { // Kill existing tunnel if any self.stopTunnel(allowRestart: false) - let tunnel = try await WebChatTunnel.create(remotePort: self.remotePort, preferredLocalPort: 18_788) + let tunnel = try await WebChatTunnel.create(remotePort: self.remotePort, preferredLocalPort: 18788) self.tunnel = tunnel self.tunnelRestartEnabled = true @@ -162,7 +171,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate { private func showError(_ text: String) { let html = """ - Web chat failed to connect.

\(text) + Web chat failed to connect.

\( + text) """ self.webView.loadHTMLString(html, baseURL: nil) } @@ -247,7 +257,7 @@ final class WebChatTunnel { "-o", "ServerAliveCountMax=3", "-o", "TCPKeepAlive=yes", "-N", - "-L", "\(localPort):127.0.0.1:\(remotePort)" + "-L", "\(localPort):127.0.0.1:\(remotePort)", ] if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) @@ -263,7 +273,9 @@ final class WebChatTunnel { // Consume stderr so ssh cannot block if it logs pipe.fileHandleForReading.readabilityHandler = { handle in let data = handle.availableData - guard !data.isEmpty, let line = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !line.isEmpty else { return } + guard !data.isEmpty, + let line = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), + !line.isEmpty else { return } webChatLogger.error("webchat tunnel stderr: \(line, privacy: .public)") } try process.run() @@ -272,7 +284,7 @@ final class WebChatTunnel { } private static func findPort(preferred: UInt16?) async throws -> UInt16 { - if let preferred, Self.portIsFree(preferred) { return preferred } + if let preferred, portIsFree(preferred) { return preferred } return try await withCheckedThrowingContinuation { cont in let queue = DispatchQueue(label: "com.steipete.clawdis.webchat.port", qos: .utility) diff --git a/apps/macos/Sources/Clawdis/WorkActivityStore.swift b/apps/macos/Sources/Clawdis/WorkActivityStore.swift index 8782b8f2e..d72443e7f 100644 --- a/apps/macos/Sources/Clawdis/WorkActivityStore.swift +++ b/apps/macos/Sources/Clawdis/WorkActivityStore.swift @@ -44,8 +44,8 @@ final class WorkActivityStore: ObservableObject { phase: String, name: String?, meta: String?, - args: [String: AnyCodable]? - ) { + args: [String: AnyCodable]?) + { let toolKind = Self.mapToolKind(name) let label = Self.buildLabel(kind: toolKind, meta: meta, args: args) if phase.lowercased() == "start" { @@ -124,7 +124,7 @@ final class WorkActivityStore: ObservableObject { return } // Otherwise, pick most recent by lastUpdate. - if let next = self.active.values.sorted(by: { $0.lastUpdate > $1.lastUpdate }).first { + if let next = self.active.values.max(by: { $0.lastUpdate < $1.lastUpdate }) { self.currentSessionKey = next.sessionKey } else { self.currentSessionKey = nil @@ -145,20 +145,20 @@ final class WorkActivityStore: ObservableObject { private static func mapToolKind(_ name: String?) -> ToolKind { switch name?.lowercased() { - case "bash", "shell": return .bash - case "read": return .read - case "write": return .write - case "edit": return .edit - case "attach": return .attach - default: return .other + case "bash", "shell": .bash + case "read": .read + case "write": .write + case "edit": .edit + case "attach": .attach + default: .other } } private static func buildLabel( kind: ToolKind, meta: String?, - args: [String: AnyCodable]? - ) -> String { + args: [String: AnyCodable]?) -> String + { switch kind { case .bash: if let cmd = args?["command"]?.value as? String { @@ -166,7 +166,7 @@ final class WorkActivityStore: ObservableObject { } return "bash" case .read, .write, .edit, .attach: - if let path = Self.extractPath(args: args, meta: meta) { + if let path = extractPath(args: args, meta: meta) { return "\(kind.rawValue): \(path)" } return kind.rawValue @@ -179,9 +179,9 @@ final class WorkActivityStore: ObservableObject { } private static func extractPath(args: [String: AnyCodable]?, meta: String?) -> String? { - if let p = args?["path"]?.value as? String { return shortenHome(path: p) } - if let p = args?["file_path"]?.value as? String { return shortenHome(path: p) } - if let meta { return shortenHome(path: meta) } + if let p = args?["path"]?.value as? String { return self.shortenHome(path: p) } + if let p = args?["file_path"]?.value as? String { return self.shortenHome(path: p) } + if let meta { return self.shortenHome(path: meta) } return nil } diff --git a/apps/macos/Sources/ClawdisCLI/main.swift b/apps/macos/Sources/ClawdisCLI/main.swift index 267a8c3d0..db1865b3e 100644 --- a/apps/macos/Sources/ClawdisCLI/main.swift +++ b/apps/macos/Sources/ClawdisCLI/main.swift @@ -183,7 +183,8 @@ struct ClawdisCLI { clawdis-mac run [--cwd ] [--env KEY=VAL] [--timeout ] [--needs-screen-recording] clawdis-mac status clawdis-mac rpc-status - clawdis-mac agent --message [--thinking ] [--session ] [--deliver] [--to ] + clawdis-mac agent --message [--thinking ] + [--session ] [--deliver] [--to ] clawdis-mac --help Returns JSON to stdout: diff --git a/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift b/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift index 7e55e76f6..f62bf6af0 100644 --- a/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/AgentRPCTests.swift @@ -1,5 +1,5 @@ -@testable import Clawdis import Testing +@testable import Clawdis @testable import ClawdisIPC @Suite(.serialized) struct AgentRPCTests { diff --git a/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift b/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift index e4b5f80c3..5612b9154 100644 --- a/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/CommandResolverTests.swift @@ -12,8 +12,9 @@ import Testing } private func makeExec(at path: URL) throws { - try FileManager.default.createDirectory(at: path.deletingLastPathComponent(), - withIntermediateDirectories: true) + try FileManager.default.createDirectory( + at: path.deletingLastPathComponent(), + withIntermediateDirectories: true) FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) } @@ -23,7 +24,7 @@ import Testing CommandResolver.setProjectRoot(tmp.path) let clawdisPath = tmp.appendingPathComponent("node_modules/.bin/clawdis") - try makeExec(at: clawdisPath) + try self.makeExec(at: clawdisPath) let cmd = CommandResolver.clawdisCommand(subcommand: "relay") #expect(cmd.prefix(2).elementsEqual([clawdisPath.path, "relay"])) @@ -35,10 +36,10 @@ import Testing let nodePath = tmp.appendingPathComponent("node_modules/.bin/node") let scriptPath = tmp.appendingPathComponent("bin/clawdis.js") - try makeExec(at: nodePath) + try self.makeExec(at: nodePath) try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) - try makeExec(at: scriptPath) + try self.makeExec(at: scriptPath) let previous = getenv("CLAWDIS_RUNTIME").flatMap { String(validatingCString: $0) } setenv("CLAWDIS_RUNTIME", "node", 1) @@ -63,7 +64,7 @@ import Testing CommandResolver.setProjectRoot(tmp.path) let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm") - try makeExec(at: pnpmPath) + try self.makeExec(at: pnpmPath) let cmd = CommandResolver.clawdisCommand(subcommand: "rpc") @@ -75,7 +76,7 @@ import Testing CommandResolver.setProjectRoot(tmp.path) let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm") - try makeExec(at: pnpmPath) + try self.makeExec(at: pnpmPath) let cmd = CommandResolver.clawdisCommand(subcommand: "health", extraArgs: ["--json", "--timeout", "5"]) diff --git a/apps/macos/Tests/ClawdisIPCTests/HealthDecodeTests.swift b/apps/macos/Tests/ClawdisIPCTests/HealthDecodeTests.swift index 596de4731..4972a796a 100644 --- a/apps/macos/Tests/ClawdisIPCTests/HealthDecodeTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/HealthDecodeTests.swift @@ -3,12 +3,10 @@ import Testing @testable import Clawdis @Suite struct HealthDecodeTests { - private let sampleJSON: String = { - // minimal but complete payload + private let sampleJSON: String = // minimal but complete payload """ {"ts":1733622000,"durationMs":420,"web":{"linked":true,"authAgeMs":120000,"connect":{"ok":true,"status":200,"error":null,"elapsedMs":800}},"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]},"ipc":{"path":"/tmp/ipc.sock","exists":true}} """ - }() @Test func decodesCleanJSON() async throws { let data = Data(sampleJSON.utf8) @@ -20,7 +18,7 @@ import Testing } @Test func decodesWithLeadingNoise() async throws { - let noisy = "debug: something logged\n" + sampleJSON + "\ntrailer" + let noisy = "debug: something logged\n" + self.sampleJSON + "\ntrailer" let snap = decodeHealthSnapshot(from: Data(noisy.utf8)) #expect(snap?.web.connect?.status == 200) diff --git a/apps/macos/Tests/ClawdisIPCTests/Placeholder.swift b/apps/macos/Tests/ClawdisIPCTests/Placeholder.swift index 425250293..14e5c056b 100644 --- a/apps/macos/Tests/ClawdisIPCTests/Placeholder.swift +++ b/apps/macos/Tests/ClawdisIPCTests/Placeholder.swift @@ -5,4 +5,3 @@ import Testing #expect(true) } } - diff --git a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift index 58beb1740..c942251bc 100644 --- a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeRuntimeTests.swift @@ -33,7 +33,8 @@ import Testing @Test func trimsAfterFirstMatchingTrigger() { let triggers = ["buddy", "claude"] let text = "hello buddy this is after trigger claude also here" - #expect(VoiceWakeRuntime._testTrimmedAfterTrigger(text, triggers: triggers) == "this is after trigger claude also here") + #expect(VoiceWakeRuntime + ._testTrimmedAfterTrigger(text, triggers: triggers) == "this is after trigger claude also here") } @Test func hasContentAfterTriggerFalseWhenOnlyTrigger() {