import AppKit import Foundation import ServiceManagement import SwiftUI private let workspaceRoot = URL(fileURLWithPath: "/Users/david/Developer/fidelity-ai-workspace", isDirectory: true) struct ProfileConfig { let id: String let displayName: String let knowledgeDir: String static func discoverDefault() -> ProfileConfig { let profilesRoot = workspaceRoot.appendingPathComponent("profiles", isDirectory: true) let candidates = (try? FileManager.default.contentsOfDirectory(at: profilesRoot, includingPropertiesForKeys: nil)) ?? [] let configs = candidates.compactMap { directory -> ProfileConfig? in let configURL = directory.appendingPathComponent("workspace.json") guard let data = try? Data(contentsOf: configURL), let decoded = try? JSONDecoder().decode(ProfileWorkspaceConfig.self, from: data) else { return nil } return ProfileConfig( id: decoded.profile ?? directory.lastPathComponent, displayName: decoded.displayName ?? decoded.profile ?? directory.lastPathComponent, knowledgeDir: decoded.knowledgeDir ?? "workspaces/\(decoded.profile ?? directory.lastPathComponent)/project-knowledge" ) } if let fidelity = configs.first(where: { $0.id == "fidelity" }) { return fidelity } if let first = configs.sorted(by: { $0.id < $1.id }).first { return first } return ProfileConfig(id: "fidelity", displayName: "Fidelity", knowledgeDir: "workspaces/fidelity/project-knowledge") } } private struct ProfileWorkspaceConfig: Decodable { let profile: String? let displayName: String? let knowledgeDir: String? enum CodingKeys: String, CodingKey { case profile case displayName = "display_name" case knowledgeDir = "knowledge_dir" } } @main struct AIWorkspaceApp: App { @StateObject private var model = ServiceStatusModel() var body: some Scene { MenuBarExtra { ServiceMenuView(model: model) .task { await model.refresh() } } label: { Label("AI Workspace", systemImage: model.menuBarSymbol) } .menuBarExtraStyle(.window) } } @MainActor final class ServiceStatusModel: ObservableObject { @Published private(set) var report: StatusReport? @Published private(set) var lastError: String? @Published private(set) var lanIP: String? @Published private(set) var loginItemStatus: SMAppService.Status = .notRegistered @Published private(set) var isRefreshing = false @Published private(set) var activeAction: String? let profile: ProfileConfig init() { self.profile = ProfileConfig.discoverDefault() } var menuBarSymbol: String { guard let report else { return "circle.dashed" } if report.services.contains(where: { $0.status == "unhealthy" || ($0.enabled && $0.status == "stopped") }) { return "exclamationmark.triangle" } if report.services.contains(where: { $0.status == "running" }) { return "checkmark.circle" } return "circle" } func refresh() async { isRefreshing = true defer { isRefreshing = false } do { let data = try await ServiceManager.run(["status", "--profile", profile.id, "--json"]) report = try JSONDecoder().decode(StatusReport.self, from: data) lanIP = await NetworkInfo.primaryLANIP() loginItemStatus = SMAppService.mainApp.status lastError = nil } catch { lastError = String(describing: error) } } var startAtLoginEnabled: Bool { loginItemStatus == .enabled } var startAtLoginStatusText: String { switch loginItemStatus { case .enabled: "enabled" case .notRegistered: "off" case .notFound: "not found" case .requiresApproval: "requires approval" @unknown default: "unknown" } } func setStartAtLogin(_ enabled: Bool) { do { if enabled { try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() } loginItemStatus = SMAppService.mainApp.status lastError = nil } catch { loginItemStatus = SMAppService.mainApp.status lastError = "Start at Login: \(error.localizedDescription)" } } func startProfile() { runAction("Starting services…", ["start", "--profile", profile.id]) } func stopProfile() { runAction("Stopping services…", ["stop", "--profile", profile.id]) } func primaryServiceAction() { if allManagedServicesRunning { stopProfile() } else { startProfile() } } func restartMCP() { runAction("Restarting MCP…", ["restart", "aiw-context-mcp", "--profile", profile.id]) } func runDoctor() { runAction("Running doctor…", ["doctor", "--profile", profile.id]) } func copyDoctorJSON() { Task { do { let data = try await ServiceManager.run(["doctor", "--profile", profile.id, "--json"]) copyToPasteboard(String(data: data, encoding: .utf8) ?? "") } catch { lastError = String(describing: error) } } } func copyPhotoInboxURL() { let host = lanIP ?? "127.0.0.1" copyToPasteboard("http://\(host):8787/upload") } func copyRecentLogs() { Task { do { var chunks: [String] = [] for service in report?.services ?? [] { let data = try await ServiceManager.run(["logs", service.name, "--profile", profile.id, "--lines", "30"]) if let text = String(data: data, encoding: .utf8), !text.isEmpty { chunks.append(text) } } copyToPasteboard(chunks.joined(separator: "\n\n")) } catch { lastError = String(describing: error) } } } func openMCPHealth() { if let url = URL(string: "http://127.0.0.1:8765/health") { NSWorkspace.shared.open(url) } } func openLogsFolder() { NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(".aiw/runtime/logs/\(profile.id)", isDirectory: true)) } func openProjectKnowledge() { NSWorkspace.shared.open(workspaceRoot.appendingPathComponent(profile.knowledgeDir, isDirectory: true)) } func openMattermost() { runAction("Launching Mattermost…", ["start", "mattermost-desktop", "--profile", profile.id]) } var allManagedServicesRunning: Bool { guard let services = report?.services else { return false } let managed = services.filter { $0.enabled && $0.kind != "app-launcher" } return !managed.isEmpty && managed.allSatisfy { $0.status == "running" || $0.status == "externally running" } } var primaryActionTitle: String { allManagedServicesRunning ? "Stop Services" : "Start Services" } var primaryActionSymbol: String { allManagedServicesRunning ? "stop.fill" : "play.fill" } var isBusy: Bool { isRefreshing || activeAction != nil } private func copyToPasteboard(_ value: String) { NSPasteboard.general.clearContents() NSPasteboard.general.setString(value, forType: .string) } private func runAction(_ label: String, _ arguments: [String]) { Task { activeAction = label defer { activeAction = nil } do { _ = try await ServiceManager.run(arguments) await refresh() } catch { lastError = String(describing: error) } } } } struct ServiceMenuView: View { @ObservedObject var model: ServiceStatusModel var body: some View { VStack(alignment: .leading, spacing: 14) { HStack(alignment: .firstTextBaseline) { VStack(alignment: .leading, spacing: 3) { Text("AI Workspace") .font(.title3.bold()) Text("Profile: \(model.profile.displayName)") .font(.caption) .foregroundStyle(.secondary) } Spacer() if model.isBusy { ProgressView() .controlSize(.small) } Button { Task { await model.refresh() } } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.borderless) .disabled(model.isBusy) .help("Refresh") } if let activeAction = model.activeAction { Label(activeAction, systemImage: "hourglass") .font(.caption) .foregroundStyle(.secondary) } Divider() if let report = model.report { VStack(spacing: 8) { ForEach(report.services) { service in ServiceRow(service: service) } } } else if let error = model.lastError { Label("Status unavailable", systemImage: "exclamationmark.triangle") Text(error) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(3) } else { Label("Loading status...", systemImage: "hourglass") } if let lanIP = model.lanIP { HStack(spacing: 8) { Image(systemName: "wifi") .foregroundStyle(.secondary) Text("Photo Inbox LAN") Spacer() Text("http://\(lanIP):8787/upload") .font(.caption.monospaced()) .foregroundStyle(.secondary) } .textSelection(.enabled) } Divider() Button(role: model.allManagedServicesRunning ? .destructive : nil, action: model.primaryServiceAction) { Label(model.primaryActionTitle, systemImage: model.primaryActionSymbol) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(model.isBusy) LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { ActionButton(title: "Restart MCP", systemImage: "arrow.triangle.2.circlepath", disabled: model.isBusy, action: model.restartMCP) ActionButton(title: "Mattermost via Proxy", systemImage: "message.badge", disabled: model.isBusy, action: model.openMattermost) } Divider() LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { ActionButton(title: "Run Doctor", systemImage: "stethoscope", disabled: model.isBusy, action: model.runDoctor) ActionButton(title: "Copy Doctor JSON", systemImage: "doc.on.doc", disabled: model.isBusy, action: model.copyDoctorJSON) ActionButton(title: "Copy Photo URL", systemImage: "link", action: model.copyPhotoInboxURL) ActionButton(title: "Copy Logs", systemImage: "doc.text", disabled: model.isBusy, action: model.copyRecentLogs) ActionButton(title: "MCP Health", systemImage: "heart.text.square", action: model.openMCPHealth) ActionButton(title: "Logs Folder", systemImage: "folder", action: model.openLogsFolder) } ActionButton(title: "Open Project Knowledge", systemImage: "books.vertical", action: model.openProjectKnowledge) Divider() VStack(alignment: .leading, spacing: 6) { Toggle(isOn: Binding( get: { model.startAtLoginEnabled }, set: { model.setStartAtLogin($0) } )) { Label("Start at Login", systemImage: "poweron") } .toggleStyle(.switch) Text("Login item: \(model.startAtLoginStatusText)") .font(.caption2) .foregroundStyle(.secondary) } Divider() HStack { if let error = model.lastError { Label(error, systemImage: "exclamationmark.triangle") .font(.caption) .foregroundStyle(.orange) .lineLimit(2) } Spacer() Button("Quit") { NSApplication.shared.terminate(nil) } .keyboardShortcut("q") } } .padding(18) .frame(width: 430) } } struct ActionButton: View { let title: String let systemImage: String var role: ButtonRole? var disabled = false let action: () -> Void var body: some View { Button(role: role, action: action) { Label(title, systemImage: systemImage) .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.bordered) .controlSize(.small) .disabled(disabled) } } struct ServiceRow: View { let service: ServiceStatus var body: some View { HStack(spacing: 10) { Image(systemName: symbol) .foregroundStyle(color) .frame(width: 16, alignment: .center) VStack(alignment: .leading, spacing: 2) { Text(service.displayName) .font(.body.weight(.medium)) Text(service.detail) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 12) StatusBadge(text: service.compactStatus, color: color) } .padding(.vertical, 7) .padding(.horizontal, 10) .background(.quaternary.opacity(0.6), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) .help(service.detail) } private var symbol: String { switch service.status { case "running": "checkmark.circle" case "launcher": "arrow.up.forward.app" case "externally running": "link.circle" case "unhealthy": "exclamationmark.triangle" case "disabled": "minus.circle" default: "xmark.circle" } } private var color: Color { switch service.status { case "running", "launcher", "externally running": .green case "unhealthy": .orange case "disabled": .secondary default: .red } } } struct StatusBadge: View { let text: String let color: Color var body: some View { Text(text) .font(.caption.weight(.semibold)) .foregroundStyle(color) .padding(.horizontal, 8) .padding(.vertical, 4) .background(color.opacity(0.14), in: Capsule()) } } enum ServiceManager { static func run(_ arguments: [String]) async throws -> Data { try await Task.detached(priority: .userInitiated) { let process = Process() process.currentDirectoryURL = workspaceRoot process.executableURL = URL(fileURLWithPath: "/usr/bin/env") process.arguments = ["python3", "scripts/aiw/services.py"] + arguments var environment = ProcessInfo.processInfo.environment let guiSafePATH = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin" if let existingPATH = environment["PATH"], !existingPATH.isEmpty { environment["PATH"] = "\(guiSafePATH):\(existingPATH)" } else { environment["PATH"] = guiSafePATH } environment["AIW_WORKSPACE_ROOT"] = workspaceRoot.path process.environment = environment let output = Pipe() let error = Pipe() process.standardOutput = output process.standardError = error try process.run() process.waitUntilExit() let data = output.fileHandleForReading.readDataToEndOfFile() let errorData = error.fileHandleForReading.readDataToEndOfFile() guard process.terminationStatus == 0 else { let message = String(data: errorData.isEmpty ? data : errorData, encoding: .utf8) ?? "service command failed" throw ServiceManagerError.commandFailed(message.trimmingCharacters(in: .whitespacesAndNewlines)) } return data }.value } } enum NetworkInfo { static func primaryLANIP() async -> String? { for interface in ["en0", "en1"] { if let value = try? await runIPConfig(interface), !value.isEmpty { return value } } return nil } private static func runIPConfig(_ interface: String) async throws -> String { try await Task.detached(priority: .utility) { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/sbin/ipconfig") process.arguments = ["getifaddr", interface] let output = Pipe() process.standardOutput = output process.standardError = Pipe() try process.run() process.waitUntilExit() let data = output.fileHandleForReading.readDataToEndOfFile() guard process.terminationStatus == 0 else { return "" } return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" }.value } } enum ServiceManagerError: LocalizedError { case commandFailed(String) var errorDescription: String? { switch self { case .commandFailed(let message): message } } } struct StatusReport: Decodable { let profile: String let workspace: String let runtime: String let services: [ServiceStatus] } struct ServiceStatus: Decodable, Identifiable { var id: String { name } let name: String let enabled: Bool let kind: String let status: String let pid: Int? let command: [String] let health: Health let state: [String: JSONValue] var displayName: String { switch name { case "aiw-context-mcp": "Context MCP" case "mattermost-proxy": "Mattermost Proxy" case "mattermost-desktop": "Mattermost Desktop via Proxy" case "photo-inbox": "Photo Inbox" default: name } } var detail: String { if name == "mattermost-desktop" { return "Launches Mattermost through local proxy 127.0.0.1:8080" } return health.detail } var compactStatus: String { switch status { case "externally running": "external" default: status } } } struct Health: Decodable { let ok: Bool? let detail: String } enum JSONValue: Decodable { case string(String) case number(Double) case bool(Bool) case object([String: JSONValue]) case array([JSONValue]) case null init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null } else if let value = try? container.decode(Bool.self) { self = .bool(value) } else if let value = try? container.decode(Double.self) { self = .number(value) } else if let value = try? container.decode(String.self) { self = .string(value) } else if let value = try? container.decode([String: JSONValue].self) { self = .object(value) } else { self = .array(try container.decode([JSONValue].self)) } } }