diff --git a/apps/macos/Sources/Clawdis/Constants.swift b/apps/macos/Sources/Clawdis/Constants.swift
index e29a98ee3..37ee7480a 100644
--- a/apps/macos/Sources/Clawdis/Constants.swift
+++ b/apps/macos/Sources/Clawdis/Constants.swift
@@ -1,8 +1,9 @@
import Foundation
let launchdLabel = "com.steipete.clawdis"
+let gatewayLaunchdLabel = "com.steipete.clawdis.gateway"
let onboardingVersionKey = "clawdis.onboardingVersion"
-let currentOnboardingVersion = 6
+let currentOnboardingVersion = 7
let pauseDefaultsKey = "clawdis.pauseEnabled"
let iconAnimationsEnabledKey = "clawdis.iconAnimationsEnabled"
let swabbleEnabledKey = "clawdis.swabbleEnabled"
diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift
index c886c6cee..efa209aad 100644
--- a/apps/macos/Sources/Clawdis/DebugSettings.swift
+++ b/apps/macos/Sources/Clawdis/DebugSettings.swift
@@ -133,9 +133,6 @@ struct DebugSettings: View {
self.gridLabel("Status")
HStack(spacing: 8) {
Text(self.gatewayManager.status.label)
- Text("Restarts: \(self.gatewayManager.restartCount)")
- .font(.caption2)
- .foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
diff --git a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift
index 1306a625d..ef6cdf0cd 100644
--- a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift
+++ b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift
@@ -64,6 +64,12 @@ struct GatewayCommandResolution {
enum GatewayEnvironment {
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway.env")
+ static func bundledGatewayExecutable() -> String? {
+ guard let res = Bundle.main.resourceURL else { return nil }
+ let path = res.appendingPathComponent("Relay/clawdis-gateway").path
+ return FileManager.default.isExecutableFile(atPath: path) ? path : nil
+ }
+
static func gatewayPort() -> Int {
let stored = UserDefaults.standard.integer(forKey: "gatewayPort")
return stored > 0 ? stored : 18789
@@ -90,6 +96,26 @@ enum GatewayEnvironment {
}
}
let expected = self.expectedGatewayVersion()
+
+ if let bundled = self.bundledGatewayExecutable() {
+ let installed = self.readGatewayVersion(binary: bundled)
+ if let expected, let installed, !installed.compatible(with: expected) {
+ return GatewayEnvironmentStatus(
+ kind: .incompatible(found: installed.description, required: expected.description),
+ nodeVersion: nil,
+ gatewayVersion: installed.description,
+ requiredGateway: expected.description,
+ message: "Bundled gateway \(installed.description) is incompatible with app \(expected.description); rebuild the app bundle.")
+ }
+ let gatewayVersionText = installed?.description ?? "unknown"
+ return GatewayEnvironmentStatus(
+ kind: .ok,
+ nodeVersion: nil,
+ gatewayVersion: gatewayVersionText,
+ requiredGateway: expected?.description,
+ message: "Bundled gateway \(gatewayVersionText) (bun)")
+ }
+
let projectRoot = CommandResolver.projectRoot()
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
@@ -160,6 +186,7 @@ enum GatewayEnvironment {
let projectEntrypoint = CommandResolver.gatewayEntrypoint(in: projectRoot)
let status = self.check()
let gatewayBin = CommandResolver.clawdisExecutable()
+ let bundled = self.bundledGatewayExecutable()
let runtime = RuntimeLocator.resolve(searchPaths: CommandResolver.preferredPaths())
guard case .ok = status.kind else {
@@ -167,6 +194,10 @@ enum GatewayEnvironment {
}
let port = self.gatewayPort()
+ if let bundled {
+ let cmd = [bundled, "--port", "\(port)", "--bind", "loopback"]
+ return GatewayCommandResolution(status: status, command: cmd)
+ }
if let gatewayBin {
let cmd = [gatewayBin, "gateway", "--port", "\(port)"]
return GatewayCommandResolution(status: status, command: cmd)
diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift
new file mode 100644
index 000000000..160404b8f
--- /dev/null
+++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift
@@ -0,0 +1,114 @@
+import Foundation
+
+enum GatewayLaunchAgentManager {
+ private static var plistURL: URL {
+ FileManager.default.homeDirectoryForCurrentUser
+ .appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
+ }
+
+ private static func gatewayExecutablePath(bundlePath: String) -> String {
+ "\(bundlePath)/Contents/Resources/Relay/clawdis-gateway"
+ }
+
+ static func status() async -> Bool {
+ guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
+ let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(gatewayLaunchdLabel)"])
+ return result.status == 0
+ }
+
+ static func set(enabled: Bool, bundlePath: String, port: Int) async -> String? {
+ if enabled {
+ let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath)
+ guard FileManager.default.isExecutableFile(atPath: gatewayBin) else {
+ return "Embedded gateway missing in bundle; rebuild via scripts/package-mac-app.sh"
+ }
+ self.writePlist(bundlePath: bundlePath, port: port)
+ _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"])
+ let bootstrap = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
+ if bootstrap.status != 0 {
+ return bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ ? "Failed to bootstrap gateway launchd job"
+ : bootstrap.output.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+ _ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(gatewayLaunchdLabel)"])
+ return nil
+ }
+
+ _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(gatewayLaunchdLabel)"])
+ try? FileManager.default.removeItem(at: self.plistURL)
+ return nil
+ }
+
+ static func kickstart() async {
+ _ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(gatewayLaunchdLabel)"])
+ }
+
+ private static func writePlist(bundlePath: String, port: Int) {
+ let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath)
+ let plist = """
+
+
+
+
+ Label
+ \(gatewayLaunchdLabel)
+ ProgramArguments
+
+ \(gatewayBin)
+ --port
+ \(port)
+ --bind
+ loopback
+
+ WorkingDirectory
+ \(FileManager.default.homeDirectoryForCurrentUser.path)
+ RunAtLoad
+
+ KeepAlive
+
+ EnvironmentVariables
+
+ PATH
+ \(CommandResolver.preferredPaths().joined(separator: ":"))
+ CLAWDIS_SKIP_BROWSER_CONTROL_SERVER
+ 1
+ CLAWDIS_IMAGE_BACKEND
+ sips
+
+ StandardOutPath
+ \(LogLocator.launchdGatewayLogPath)
+ StandardErrorPath
+ \(LogLocator.launchdGatewayLogPath)
+
+
+ """
+ try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
+ }
+
+ private struct LaunchctlResult {
+ let status: Int32
+ let output: String
+ }
+
+ @discardableResult
+ private static func runLaunchctl(_ args: [String]) async -> LaunchctlResult {
+ await Task.detached(priority: .utility) { () -> LaunchctlResult in
+ let process = Process()
+ process.launchPath = "/bin/launchctl"
+ process.arguments = args
+ let pipe = Pipe()
+ process.standardOutput = pipe
+ process.standardError = pipe
+ do {
+ try process.run()
+ process.waitUntilExit()
+ let data = pipe.fileHandleForReading.readToEndSafely()
+ let output = String(data: data, encoding: .utf8) ?? ""
+ return LaunchctlResult(status: process.terminationStatus, output: output)
+ } catch {
+ return LaunchctlResult(status: -1, output: error.localizedDescription)
+ }
+ }.value
+ }
+}
+
diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift
index e5856664d..41b6bc96e 100644
--- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift
+++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift
@@ -1,16 +1,5 @@
import Foundation
-import Network
import Observation
-import OSLog
-import Subprocess
-#if canImport(Darwin)
-import Darwin
-#endif
-#if canImport(System)
-import System
-#else
-import SystemPackage
-#endif
@MainActor
@Observable
@@ -20,8 +9,7 @@ final class GatewayProcessManager {
enum Status: Equatable {
case stopped
case starting
- case running(pid: Int32)
- case restarting
+ case running(details: String?)
case attachedExisting(details: String?)
case failed(String)
@@ -29,8 +17,9 @@ final class GatewayProcessManager {
switch self {
case .stopped: return "Stopped"
case .starting: return "Starting…"
- case let .running(pid): return "Running (pid \(pid))"
- case .restarting: return "Restarting…"
+ case let .running(details):
+ if let details, !details.isEmpty { return "Running (\(details))" }
+ return "Running"
case let .attachedExisting(details):
if let details, !details.isEmpty {
return "Using existing gateway (\(details))"
@@ -43,41 +32,15 @@ final class GatewayProcessManager {
private(set) var status: Status = .stopped
private(set) var log: String = ""
- private(set) var restartCount: Int = 0
private(set) var environmentStatus: GatewayEnvironmentStatus = .checking
private(set) var existingGatewayDetails: String?
private(set) var lastFailureReason: String?
- private(set) var lastExitCode: Int32?
- private(set) var lastSubprocessError: String?
-
- private var execution: Execution?
- private var lastPid: Int32?
- private var lastCommand: [String]?
private var desiredActive = false
- private var stopping = false
- private var recentCrashes: [Date] = []
private var environmentRefreshTask: Task?
private var lastEnvironmentRefresh: Date?
+ private var logRefreshTask: Task?
- private final class GatewayLockHandle {
- private let fd: FileDescriptor
- private let path: String
-
- init(fd: FileDescriptor, path: String) {
- self.fd = fd
- self.path = path
- }
-
- func cancel() {
- try? self.fd.close()
- try? FileManager.default.removeItem(atPath: self.path)
- }
- }
-
- private let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway")
private let logLimit = 20000 // characters to keep in-memory
- private let maxCrashes = 3
- private let crashWindow: TimeInterval = 120 // seconds
private let environmentRefreshMinInterval: TimeInterval = 30
func setActive(_ active: Bool) {
@@ -99,20 +62,13 @@ final class GatewayProcessManager {
}
func startIfNeeded() {
- guard self.execution == nil, self.desiredActive else { return }
+ guard self.desiredActive else { return }
// Do not spawn in remote mode (the gateway should run on the remote host).
guard !CommandResolver.connectionModeIsRemote() else {
self.status = .stopped
return
}
- if self.shouldGiveUpAfterCrashes() {
- self.status = .failed("Too many crashes; giving up")
- return
- }
-
- if self.status != .restarting {
- self.status = .starting
- }
+ self.status = .starting
// First try to latch onto an already-running gateway to avoid spawning a duplicate.
Task { [weak self] in
@@ -128,26 +84,17 @@ final class GatewayProcessManager {
}
return
}
- await self.spawnGateway()
+ await self.enableLaunchdGateway()
}
}
func stop() {
self.desiredActive = false
- self.stopping = true
self.existingGatewayDetails = nil
self.lastFailureReason = nil
- self.lastExitCode = nil
- self.lastSubprocessError = nil
- guard let execution else {
- self.status = .stopped
- return
- }
self.status = .stopped
- Task {
- await execution.teardown(using: [.gracefulShutDown(allowedDurationToNextStep: .seconds(1))])
- }
- self.execution = nil
+ let bundlePath = Bundle.main.bundleURL.path
+ Task { _ = await GatewayLaunchAgentManager.set(enabled: false, bundlePath: bundlePath, port: GatewayEnvironment.gatewayPort()) }
}
func refreshEnvironmentStatus(force: Bool = false) {
@@ -173,6 +120,24 @@ final class GatewayProcessManager {
}
}
+ func refreshLog() {
+ guard self.logRefreshTask == nil else { return }
+ let path = LogLocator.launchdGatewayLogPath
+ let limit = self.logLimit
+ self.logRefreshTask = Task { [weak self] in
+ let log = await Task.detached(priority: .utility) {
+ Self.readGatewayLog(path: path, limit: limit)
+ }.value
+ await MainActor.run {
+ guard let self else { return }
+ if !log.isEmpty {
+ self.log = log
+ }
+ self.logRefreshTask = nil
+ }
+ }
+ }
+
// MARK: - Internals
/// Attempt to connect to an already-running gateway on the configured port.
@@ -204,6 +169,7 @@ final class GatewayProcessManager {
self.existingGatewayDetails = details
self.status = .attachedExisting(details: details)
self.appendLog("[gateway] using existing instance: \(details)\n")
+ self.refreshLog()
return true
} catch {
// No reachable gateway (or token mismatch) — fall through to spawn.
@@ -212,162 +178,47 @@ final class GatewayProcessManager {
}
}
- private func spawnGateway() async {
- if self.status != .restarting {
- self.status = .starting
- }
+ private func enableLaunchdGateway() async {
self.existingGatewayDetails = nil
let resolution = await Task.detached(priority: .utility) {
GatewayEnvironment.resolveGatewayCommand()
}.value
await MainActor.run { self.environmentStatus = resolution.status }
- guard let command = resolution.command else {
+ guard resolution.command != nil else {
await MainActor.run {
self.status = .failed(resolution.status.message)
}
return
}
- let cwd = self.defaultProjectRoot().path
- self.appendLog("[gateway] starting: \(command.joined(separator: " ")) (cwd: \(cwd))\n")
- self.lastCommand = command
-
- do {
- // Acquire the same UDS lock the CLI uses to guarantee a single instance.
- let lockPath = FileManager.default.temporaryDirectory.appendingPathComponent("clawdis-gateway.lock").path
- let listener = try self.acquireGatewayLock(path: lockPath)
-
- let result = try await run(
- .name(command.first ?? "clawdis"),
- arguments: Arguments(Array(command.dropFirst())),
- environment: self.makeEnvironment(),
- workingDirectory: FilePath(cwd))
- { execution, stdin, stdout, stderr in
- self.didStart(execution)
- // Consume stdout/stderr eagerly so the gateway can't block on full pipes.
- async let out: Void = self.stream(output: stdout, label: "stdout")
- async let err: Void = self.stream(output: stderr, label: "stderr")
- try await stdin.finish()
- await out
- await err
- }
-
- // Release the lock after the process exits.
- listener.cancel()
-
- await self.handleTermination(status: result.terminationStatus)
- } catch {
- await self.handleError(error)
- }
- }
-
- /// Minimal clone of the Node gateway lock: take an exclusive file lock.
- private func acquireGatewayLock(path: String) throws -> GatewayLockHandle {
- // Remove stale lock if needed (mirrors CLI behavior).
- try? FileManager.default.removeItem(atPath: path)
- let fd = try FileDescriptor.open(
- FilePath(path),
- .readWrite,
- options: [.create, .exclusiveCreate],
- permissions: [.ownerReadWrite])
- return GatewayLockHandle(fd: fd, path: path)
- }
-
- private func didStart(_ execution: Execution) {
- self.execution = execution
- self.stopping = false
- self.lastFailureReason = nil
- self.lastExitCode = nil
- self.lastSubprocessError = nil
- self.status = .running(pid: execution.processIdentifier.value)
- self.lastPid = execution.processIdentifier.value
- self.logger.info("gateway started pid \(execution.processIdentifier.value)")
- Task {
- await PortGuardian.shared.record(
- port: GatewayEnvironment.gatewayPort(),
- pid: execution.processIdentifier.value,
- command: (self.lastCommand ?? []).joined(separator: " "),
- mode: AppStateStore.shared.connectionMode)
- }
- }
-
- private func handleTermination(status: TerminationStatus) async {
- let code: Int32 = switch status {
- case let .exited(exitCode): exitCode
- case let .unhandledException(sig): -Int32(sig)
- }
-
- self.execution = nil
- if let pid = self.lastPid {
- Task { await PortGuardian.shared.removeRecord(pid: pid) }
- }
- self.lastPid = nil
- self.lastCommand = nil
- if self.stopping || !self.desiredActive {
- self.status = .stopped
- self.stopping = false
- if let pid = self.lastPid {
- Task { await PortGuardian.shared.removeRecord(pid: pid) }
- }
+ let bundlePath = Bundle.main.bundleURL.path
+ let port = GatewayEnvironment.gatewayPort()
+ self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n")
+ let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
+ if let err {
+ self.status = .failed(err)
+ self.lastFailureReason = err
return
}
- self.lastExitCode = code
- self.lastFailureReason = "Gateway exited (code \(code))."
- self.recentCrashes.append(Date())
- self.recentCrashes = self.recentCrashes.filter { Date().timeIntervalSince($0) < self.crashWindow }
- self.restartCount += 1
- self.appendLog("[gateway] exited (\(code)).\n")
-
- if self.shouldGiveUpAfterCrashes() {
- self.status = .failed("Too many crashes; last exit code \(code).")
- self.logger.error("gateway crash loop detected; giving up")
- return
- }
-
- self.status = .restarting
- self.logger.warning("gateway crashed (code \(code)); restarting")
- // Slight backoff to avoid hammering the system in case of immediate crash-on-start.
- try? await Task.sleep(nanoseconds: 750_000_000)
- self.startIfNeeded()
- }
-
- private func handleError(_ error: any Error) async {
- self.execution = nil
- var message = error.localizedDescription
- if let sp = error as? SubprocessError {
- message = "SubprocessError \(sp.code.value): \(sp)"
- self.lastSubprocessError = message
- }
- self.lastFailureReason = message
- self.appendLog("[gateway] failed: \(message)\n")
- self.logger.error("gateway failed: \(message, privacy: .public)")
- if self.desiredActive, !self.shouldGiveUpAfterCrashes() {
- self.status = .restarting
- self.recentCrashes.append(Date())
- self.startIfNeeded()
- } else {
- self.status = .failed(error.localizedDescription)
- }
- }
-
- private func shouldGiveUpAfterCrashes() -> Bool {
- self.recentCrashes = self.recentCrashes.filter { Date().timeIntervalSince($0) < self.crashWindow }
- return self.recentCrashes.count >= self.maxCrashes
- }
-
- private func stream(output: AsyncBufferSequence, label: String) async {
- do {
- for try await line in output.lines() {
- await MainActor.run {
- self.appendLog(line + "\n")
- }
- }
- } catch {
- await MainActor.run {
- self.appendLog("[gateway \(label)] stream error: \(error.localizedDescription)\n")
+ // Best-effort: wait for the gateway to accept connections.
+ let deadline = Date().addingTimeInterval(6)
+ while Date() < deadline {
+ if !self.desiredActive { return }
+ do {
+ _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500)
+ let instance = await PortGuardian.shared.describe(port: port)
+ let details = instance.map { "pid \($0.pid)" }
+ self.status = .running(details: details)
+ self.refreshLog()
+ return
+ } catch {
+ try? await Task.sleep(nanoseconds: 400_000_000)
}
}
+
+ self.status = .failed("Gateway did not start in time")
+ self.lastFailureReason = "launchd start timeout"
}
private func appendLog(_ chunk: String) {
@@ -379,20 +230,7 @@ final class GatewayProcessManager {
func clearLog() {
self.log = ""
- }
-
- private func makeEnvironment() -> Environment {
- let merged = CommandResolver.preferredPaths().joined(separator: ":")
- return .inherit.updating([
- "PATH": merged,
- "PNPM_HOME": FileManager.default.homeDirectoryForCurrentUser
- .appendingPathComponent("Library/pnpm").path,
- "CLAWDIS_PROJECT_ROOT": CommandResolver.projectRoot().path,
- ])
- }
-
- private func defaultProjectRoot() -> URL {
- CommandResolver.projectRoot()
+ try? FileManager.default.removeItem(atPath: LogLocator.launchdGatewayLogPath)
}
func setProjectRoot(path: String) {
@@ -402,4 +240,12 @@ final class GatewayProcessManager {
func projectRootPath() -> String {
CommandResolver.projectRootPath()
}
+
+ private static func readGatewayLog(path: String, limit: Int) -> String {
+ guard FileManager.default.fileExists(atPath: path) else { return "" }
+ guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return "" }
+ let text = String(data: data, encoding: .utf8) ?? ""
+ if text.count <= limit { return text }
+ return String(text.suffix(limit))
+ }
}
diff --git a/apps/macos/Sources/Clawdis/GeneralSettings.swift b/apps/macos/Sources/Clawdis/GeneralSettings.swift
index 391d62e49..9b0a5aee0 100644
--- a/apps/macos/Sources/Clawdis/GeneralSettings.swift
+++ b/apps/macos/Sources/Clawdis/GeneralSettings.swift
@@ -339,12 +339,6 @@ struct GeneralSettings: View {
.foregroundStyle(.red)
}
- if let exitCode = self.gatewayManager.lastExitCode {
- Text("Last exit code: \(exitCode)")
- .font(.caption2)
- .foregroundStyle(.secondary)
- }
-
HStack(spacing: 10) {
Button {
Task { await self.installGateway() }
@@ -352,7 +346,7 @@ struct GeneralSettings: View {
if self.gatewayInstalling {
ProgressView().controlSize(.small)
} else {
- Text("Install/Update gateway")
+ Text("Enable Gateway daemon")
}
}
.buttonStyle(.borderedProminent)
@@ -365,7 +359,7 @@ struct GeneralSettings: View {
Text(self
.gatewayInstallMessage ??
- "Installs the global \"clawdis\" package and expects the gateway on port 18789.")
+ "Enables the bundled Gateway via launchd (\(gatewayLaunchdLabel)). No Node install required.")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
@@ -407,10 +401,10 @@ struct GeneralSettings: View {
self.gatewayInstalling = true
defer { self.gatewayInstalling = false }
self.gatewayInstallMessage = nil
- let expected = GatewayEnvironment.expectedGatewayVersion()
- await GatewayEnvironment.installGlobal(version: expected) { message in
- Task { @MainActor in self.gatewayInstallMessage = message }
- }
+ let port = GatewayEnvironment.gatewayPort()
+ let bundlePath = Bundle.main.bundleURL.path
+ let err = await GatewayLaunchAgentManager.set(enabled: true, bundlePath: bundlePath, port: port)
+ self.gatewayInstallMessage = err ?? "Gateway enabled and started on port \(port)"
self.refreshGatewayStatus()
}
diff --git a/apps/macos/Sources/Clawdis/LogLocator.swift b/apps/macos/Sources/Clawdis/LogLocator.swift
index 75ebb6e76..35306f969 100644
--- a/apps/macos/Sources/Clawdis/LogLocator.swift
+++ b/apps/macos/Sources/Clawdis/LogLocator.swift
@@ -3,6 +3,7 @@ import Foundation
enum LogLocator {
private static let logDir = URL(fileURLWithPath: "/tmp/clawdis")
private static let stdoutLog = logDir.appendingPathComponent("clawdis-stdout.log")
+ private static let gatewayLog = logDir.appendingPathComponent("clawdis-gateway.log")
private static func modificationDate(for url: URL) -> Date {
(try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
@@ -27,4 +28,9 @@ enum LogLocator {
static var launchdLogPath: String {
stdoutLog.path
}
+
+ /// Path to use for the embedded Gateway launchd job stdout/err.
+ static var launchdGatewayLogPath: String {
+ gatewayLog.path
+ }
}
diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift
index 14b2eca4c..0ccb1ce34 100644
--- a/apps/macos/Sources/Clawdis/MenuBar.swift
+++ b/apps/macos/Sources/Clawdis/MenuBar.swift
@@ -240,7 +240,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationWillTerminate(_ notification: Notification) {
- GatewayProcessManager.shared.stop()
PresenceReporter.shared.stop()
NodePairingApprovalPrompter.shared.stop()
MacNodeModeCoordinator.shared.stop()