import AppKit import Foundation struct CompanionBootstrap: Codable { struct Gateway: Codable { let url: String let token: String? } struct Node: Codable { let nodeId: String let role: String let platform: String let capabilities: [String] } struct Runtime: Codable { let heartbeatSeconds: Int let handoffTimeoutMs: Int let autoReconnect: Bool } let schemaVersion: Int let generatedAt: String let gateway: Gateway let node: Node let runtime: Runtime } final class CompanionProcessController { private var process: Process? private var isRunning = false private(set) var lastError: String? private let bootstrap: CompanionBootstrap init(bootstrap: CompanionBootstrap) { self.bootstrap = bootstrap } func start() { guard process == nil else { return } let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/env") var args: [String] = [ "flynn", "companion", "--url", bootstrap.gateway.url, "--node-id", bootstrap.node.nodeId, "--role", bootstrap.node.role, "--platform", bootstrap.node.platform, "--heartbeat", String(bootstrap.runtime.heartbeatSeconds), "--handoff-timeout", String(bootstrap.runtime.handoffTimeoutMs), ] if let token = bootstrap.gateway.token, !token.isEmpty { args += ["--token", token] } for capability in bootstrap.node.capabilities { args += ["--capability", capability] } if !bootstrap.runtime.autoReconnect { args.append("--once") } proc.arguments = args proc.terminationHandler = { [weak self] p in DispatchQueue.main.async { self?.process = nil self?.isRunning = false if p.terminationStatus != 0 { self?.lastError = "companion exited with status \(p.terminationStatus)" } } } do { try proc.run() process = proc isRunning = true lastError = nil } catch { lastError = error.localizedDescription process = nil isRunning = false } } func stop() { process?.terminate() process = nil isRunning = false } func runHandoff(message: String) { guard !message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } let proc = Process() proc.executableURL = URL(fileURLWithPath: "/usr/bin/env") var args: [String] = [ "flynn", "companion", "--once", "--url", bootstrap.gateway.url, "--node-id", bootstrap.node.nodeId, "--role", bootstrap.node.role, "--platform", bootstrap.node.platform, "--handoff-timeout", String(bootstrap.runtime.handoffTimeoutMs), "--handoff", message, ] if let token = bootstrap.gateway.token, !token.isEmpty { args += ["--token", token] } proc.arguments = args do { try proc.run() } catch { lastError = "handoff failed: \(error.localizedDescription)" } } var running: Bool { isRunning } } final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! private var startItem: NSMenuItem! private var stopItem: NSMenuItem! private var handoffItem: NSMenuItem! private var errorItem: NSMenuItem! private var controller: CompanionProcessController! private var refreshTimer: Timer? func applicationDidFinishLaunching(_ notification: Notification) { let bootstrap = Self.loadBootstrap() controller = CompanionProcessController(bootstrap: bootstrap) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let menu = NSMenu() startItem = NSMenuItem(title: "Start Companion", action: #selector(startCompanion), keyEquivalent: "s") stopItem = NSMenuItem(title: "Stop Companion", action: #selector(stopCompanion), keyEquivalent: "x") handoffItem = NSMenuItem(title: "Send Handoff...", action: #selector(sendHandoff), keyEquivalent: "h") errorItem = NSMenuItem(title: "Last error: (none)", action: nil, keyEquivalent: "") errorItem.isEnabled = false menu.addItem(startItem) menu.addItem(stopItem) menu.addItem(handoffItem) menu.addItem(NSMenuItem.separator()) menu.addItem(errorItem) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "q")) startItem.target = self stopItem.target = self handoffItem.target = self statusItem.menu = menu refreshUI() refreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.refreshUI() } } func applicationWillTerminate(_ notification: Notification) { refreshTimer?.invalidate() controller.stop() } @objc private func startCompanion() { controller.start() refreshUI() } @objc private func stopCompanion() { controller.stop() refreshUI() } @objc private func sendHandoff() { let alert = NSAlert() alert.messageText = "Companion Handoff" alert.informativeText = "Send a one-shot handoff message through Flynn companion." let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) input.placeholderString = "message" alert.accessoryView = input alert.addButton(withTitle: "Send") alert.addButton(withTitle: "Cancel") let response = alert.runModal() if response == .alertFirstButtonReturn { controller.runHandoff(message: input.stringValue) refreshUI() } } @objc private func quitApp() { NSApplication.shared.terminate(nil) } private func refreshUI() { if controller.running { statusItem.button?.title = "F*" startItem.isEnabled = false stopItem.isEnabled = true } else { statusItem.button?.title = controller.lastError == nil ? "F" : "F!" startItem.isEnabled = true stopItem.isEnabled = false } let errorText = controller.lastError ?? "(none)" errorItem.title = "Last error: \(errorText)" } static func loadBootstrap() -> CompanionBootstrap { guard let url = Bundle.module.url(forResource: "companion.bootstrap", withExtension: "json"), let data = try? Data(contentsOf: url), let decoded = try? JSONDecoder().decode(CompanionBootstrap.self, from: data) else { fatalError("Failed to load companion.bootstrap.json resource") } return decoded } } let app = NSApplication.shared let delegate = AppDelegate() app.setActivationPolicy(.accessory) app.delegate = delegate app.run()