Repository Public
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Forge Studio — GUI companion for the Forge LLM training framework.4import SwiftUI56@main7struct ForgeStudioApp: App {8 @State private var model = AppModel()910 var body: some Scene {11 WindowGroup("Forge Studio") {12 ContentView()13 .environment(model)14 .frame(minWidth: 980, minHeight: 640)15 }16 Settings {17 SettingsView()18 .environment(model)19 }20 }21}2223@Observable24@MainActor25final class AppModel {26 var store: RunStore27 var supervisor: RunSupervisor28 var forgeDevice: String?29 var forgeError: String?3031 init() {32 let workspace = ForgeBinaryLocator.savedWorkspaceURL33 ?? FileManager.default.homeDirectoryForCurrentUser34 .appendingPathComponent("ForgeStudioWorkspace")35 let store = RunStore(workspace: workspace)36 self.store = store37 self.supervisor = RunSupervisor(store: store)38 recoverOrphans()39 Task { await self.validateForge() }40 }4142 /// Crash recovery: a run persisted as active means the app died mid-run.43 /// If its forge PID is still alive we leave the process alone and record44 /// the truth (stopped tracking); if it's gone, the run is marked stopped45 /// with an honest reason. A run never stays in a lying state.46 private func recoverOrphans() {47 for var run in store.runs where run.state.isActive {48 if let pid = run.pid, kill(pid, 0) == 0 {49 run.failureReason =50 "L'app a quitté pendant le run ; forge (pid \(pid)) tourne encore — "51 + "métriques reprises depuis log.csv, contrôle du process perdu."52 } else {53 run.failureReason =54 "L'app a quitté pendant le run ; forge n'est plus actif. "55 + "Reprise possible depuis ckpt_latest.bin."56 }57 run.state = .stopped58 run.pid = nil59 store.upsert(run)60 }61 }6263 func validateForge() async {64 guard let binary = ForgeBinaryLocator.savedBinaryURL65 ?? ForgeBinaryLocator.candidates().first else {66 forgeError = "Binaire forge introuvable — choisissez-le dans les Réglages."67 return68 }69 switch await ForgeBinaryLocator.validate(binary: binary) {70 case .success(let v):71 forgeDevice = v.version72 forgeError = nil73 ForgeBinaryLocator.save(binary: binary)74 case .failure(let e):75 forgeError = e.localizedDescription76 }77 }7879 func datasets() -> [Dataset] {80 let dataDir = store.workspaceURL.appendingPathComponent("data")81 let forgeData = ForgeBinaryLocator.savedBinaryURL?82 .deletingLastPathComponent().deletingLastPathComponent()83 .appendingPathComponent("data")84 var found: [Dataset] = []85 for root in [dataDir, forgeData].compactMap({ $0 }) {86 guard let subdirs = try? FileManager.default.contentsOfDirectory(87 at: root, includingPropertiesForKeys: nil) else { continue }88 for dir in subdirs {89 if let ds = Dataset.scan(directory: dir),90 !found.contains(where: { $0.path == ds.path }) {91 found.append(ds)92 }93 }94 }95 return found.sorted { $0.name < $1.name }96 }97}98