Files
fidelity-ai-workspace/apps/mac/AIWorkspace/Sources/main.swift

600 lines
20 KiB
Swift

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))
}
}
}