File size: 24,148 Bytes
f592f80 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | import SwiftUI
import AppKit
import UniformTypeIdentifiers
func pretty(_ x: Any) -> String { guard let d = try? JSONSerialization.data(withJSONObject: x, options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]) else { return "{}" }; return String(decoding:d,as:UTF8.self) }
@MainActor final class Studio: ObservableObject {
@Published var geometries: [[String:Any]] = []
@Published var geometry: [String:Any] = [:]
@Published var editor = ""
@Published var showWeights = false
@Published var source = ""
@Published var output = ""
@Published var report = ""
@Published var status = "Starting AFFECTOR…"
@Published var busy = false
@Published var backend = "Local Qwen"
@Published var rewriteStyle = "Restrained"
@Published var model = "gpt-6-luna"
@Published var key = ""
@Published var evaluator = "Local Qwen"
@Published var evalModel = "gpt-6-luna"
@Published var evalKey = ""
@Published var temperature = 0.0
@Published var topP = 0.95
@Published var topK = 40
@Published var tokens = 512
@Published var verification: [String:Any] = [:]
@Published var hasRun = false
@Published var runSummary = ""
@Published var support = ""
@Published var error: String?
private var process: Process?
private var input: FileHandle?
private var buffer = Data()
func start() {
let resources = Bundle.main.resourceURL!
let p = Process(); let stdin = Pipe(); let stdout = Pipe(); let stderr = Pipe()
p.executableURL = resources.appendingPathComponent("Python.framework/Versions/3.14/bin/python3.14")
p.arguments = ["-u",resources.appendingPathComponent("engine/bridge.py").path]
p.currentDirectoryURL = resources.appendingPathComponent("engine")
var env = ProcessInfo.processInfo.environment
env["PYTHONHOME"] = resources.appendingPathComponent("Python.framework/Versions/3.14").path
env["PYTHONPATH"] = resources.appendingPathComponent("packages").path
env["HF_HOME"] = FileManager.default.urls(for:.cachesDirectory,in:.userDomainMask)[0].appendingPathComponent("AFFECTOR/huggingface").path
env["AFFECTOR_MODEL"] = resources.appendingPathComponent("engine/.hf/hub/models--Qwen--Qwen3.5-0.8B/snapshots/2fc06364715b967f1860aea9cf38778875588b17").path
env["PYTHONNOUSERSITE"] = "1"; env["PYTHONDONTWRITEBYTECODE"] = "1"; env["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
p.environment = env; p.standardInput = stdin; p.standardOutput = stdout; p.standardError = stderr
input = stdin.fileHandleForWriting; process = p
stdout.fileHandleForReading.readabilityHandler = { [weak self] h in
let d = h.availableData
if !d.isEmpty { Task { @MainActor in self?.receive(d) } }
}
stderr.fileHandleForReading.readabilityHandler = { h in _ = h.availableData }
p.terminationHandler = { [weak self] p in Task { @MainActor in self?.busy = false; self?.status = "Engine stopped (\(p.terminationStatus)). Reopen AFFECTOR to restart." } }
do { try p.run() } catch { self.error = error.localizedDescription }
}
func stopProcess() { input?.closeFile(); process?.terminate() }
func receive(_ d:Data) {
buffer.append(d)
while let end = buffer.firstIndex(of:10) {
let line = buffer[..<end]; buffer.removeSubrange(...end)
guard let obj = (try? JSONSerialization.jsonObject(with:Data(line))) as? [String:Any] else { continue }
let data = obj["data"] as? [String:Any] ?? [:]
switch obj["event"] as? String {
case "ready", "result":
if data["reset_text"] as? Bool == true { source = ""; output = ""; report = ""; verification = [:]; runSummary = ""; hasRun = false; error = nil }
if let a = data["geometries"] as? [[String:Any]] { geometries = a; if geometry.isEmpty, let first = a.first { select(first) } }
if let g = data["geometry"] as? [String:Any] { select(g) }
if let s = data["support"] as? String { support = s }
if let t = data["text"] as? String { source = t }
if let t = data["input"] as? String { source = t }
if let style = data["rewrite_style"] as? String { rewriteStyle = style }
if let b = data["backend"] as? String { backend = b }
if let m = data["model"] as? String { model = m }
if let s = data["sampling"] as? [String:Any] { temperature = s["temperature"] as? Double ?? 0; topP = s["top_p"] as? Double ?? 0.95; topK = s["top_k"] as? Int ?? 40; tokens = s["max_tokens"] as? Int ?? 512 }
status = data["message"] as? String ?? "Ready"
case "geometry":
select(data)
if let i=geometries.firstIndex(where:{($0["id"] as? String)==(data["id"] as? String)}) {geometries[i]=data}
case "progress": status = obj["message"] as? String ?? "Working…"
case "run": runSummary = (data["summary"] as? [String:Any])?["message"] as? String ?? ""; output = obj["output"] as? String ?? ""; report = displayReport(data); verification = data["verification"] as? [String:Any] ?? [:]; hasRun = true
case "error": error = obj["message"] as? String; status = error ?? "Error"
case "saved": status = "Transfer checkpoint saved."
case "idle": busy = false
default: break
}
}
}
func select(_ g:[String:Any]) { geometry = g; showWeights = false; var display=g; display.removeValue(forKey:"basis"); editor = pretty(display) }
func toggleWeights() {
guard var g=edited() else {return}
showWeights.toggle()
if !showWeights {geometry["basis"]=g["basis"];g.removeValue(forKey:"basis")}
editor=pretty(g)
}
func displayReport(_ data:[String:Any])->String {
var d=data
if var g=d["geometry"] as? [String:Any] {g["basis"]="Compiled weights included in exported JSON; edit in Geometry + weights.";d["geometry"]=g}
let result=pretty(d)
return result.count > 80000 ? String(result.prefix(80000))+"\n… Export the report to view all passages." : result
}
func edited() -> [String:Any]? {
guard let d = editor.data(using:.utf8), var g = (try? JSONSerialization.jsonObject(with:d)) as? [String:Any] else { error = "Geometry must be valid JSON. Check commas and brackets."; return nil }
if g["basis"] == nil {g["basis"] = geometry["basis"] ?? NSNull()}
return g
}
func send(_ q:[String:Any]) {
guard let input, let d = try? JSONSerialization.data(withJSONObject:q) else { return }
if q["op"] as? String != "cancel" && q["op"] as? String != "save_run" { busy = true }
do { try input.write(contentsOf:d + Data([10])) } catch { busy = false; self.error = error.localizedDescription }
}
func open(_ op:String, document:Bool = false) {
let panel = NSOpenPanel(); panel.canChooseDirectories = false; panel.allowsMultipleSelection = false
panel.allowedContentTypes = document ? [UTType.plainText,UTType.rtf,UTType(filenameExtension:"docx")!] : [.json]
if panel.runModal() == .OK, let url = panel.url { send(["op":op,"path":url.path]) }
}
func save(_ op:String, document:Bool = false) {
let panel = NSSavePanel(); panel.nameFieldStringValue = document ? "transformed.txt" : (op == "export_geometry" ? "emotional-shape.json" : "affector-transfer.json")
panel.allowedContentTypes = document ? [.plainText,.rtf,UTType(filenameExtension:"docx")!] : [.json]
panel.allowsOtherFileTypes = false
if panel.runModal() == .OK, let url = panel.url {
var q:[String:Any] = ["op":op,"path":url.path]
if op == "export_geometry" { guard let g = edited() else { return }; q["geometry"] = g }
send(q)
}
}
func transform() {
guard let g = edited() else { return }
send(["op":"transform","rewrite_style":rewriteStyle,"geometry":g,"text":source,"backend":backend,"model":model,"key":key,"sampling":["temperature":temperature,"top_p":topP,"top_k":topK,"max_tokens":tokens]])
}
func geometryAction(_ op:String) { if let g = edited() { send(["op":op,"geometry":g]) } }
}
struct Trajectory: View {
@Environment(\.colorScheme) private var colorScheme
let geometry:[String:Any]
var body: some View {
Canvas { ctx,size in
let points = geometry["points"] as? [[String:Double]] ?? []
func project(_ x:Double,_ y:Double,_ z:Double)->CGPoint { CGPoint(x:size.width*0.48 + CGFloat(x-y*0.45)*size.width*0.28,y:size.height*0.52 - CGFloat(z+y*0.35)*size.height*0.32) }
let axes:[(String,(Double,Double,Double))] = [("X · valence",(1.35,0,0)),("Y · direction",(0,1.35,0)),("Z · intensity",(0,0,1.35))]
for (name,a) in axes { var p=Path(); p.move(to:project(0,0,0)); p.addLine(to:project(a.0,a.1,a.2)); ctx.stroke(p,with:.color(.gray.opacity(0.6)),lineWidth:1); ctx.draw(Text(name).font(.system(size:10,design:.monospaced)),at:project(a.0,a.1,a.2),anchor:a.0 > 0 ? .trailing : .bottom) }
var path=Path()
for (i,p) in points.enumerated() { let pos=project(p["v"] ?? 0,p["d"] ?? 0,2*(p["i"] ?? 0.5)-1); if i==0 {path.move(to:pos)} else {path.addLine(to:pos)}; ctx.fill(Path(ellipseIn:CGRect(x:pos.x-3,y:pos.y-3,width:6,height:6)),with:.color(.primary)) }
ctx.stroke(path,with:.color(.primary),lineWidth:2)
}.frame(height:205).background(colorScheme == .dark ? Color.black : Color.white).overlay(Rectangle().stroke(.primary.opacity(0.15)))
}
}
struct ContentView: View {
@EnvironmentObject var s:Studio
@AppStorage("appearance") private var appearance = "light"
private var darkMode: Bool { appearance == "dark" }
@State private var tab = 0
@State private var confirmDelete = false
let providers = ["Local Qwen","OpenAI","VT ARC"]
var body: some View {
VStack(alignment:.leading,spacing:0) {
HStack(alignment:.top) {
VStack(alignment:.leading,spacing:1) { Text("AFFECTOR").font(.system(size:53,weight:.black,design:.default)).tracking(-3); Text("EMOTIONAL GEOMETRY → WRITTEN FORM").font(.system(size:10,design:.monospaced)).tracking(2) }
Spacer()
VStack(alignment:.trailing,spacing:8) { Text(s.status).font(.system(size:12,design:.monospaced)).multilineTextAlignment(.trailing).frame(maxWidth:390,minHeight:35); if s.busy { Button("Stop") { s.send(["op":"cancel"]) } }
Picker("Appearance",selection:$appearance) {Text("Light").tag("light");Text("Dark").tag("dark")}.pickerStyle(.segmented).labelsHidden().frame(width:170).accessibilityLabel("Appearance")
}
}.padding(24)
Divider().background(.primary)
HStack(alignment:.top,spacing:0) {
VStack(alignment:.leading,spacing:12) {
label("SHAPE LIBRARY")
ScrollView { VStack(alignment:.leading,spacing:3) { ForEach(s.geometries.indices,id:\.self) { i in
let g=s.geometries[i]
Button { s.select(g) } label: { VStack(alignment:.leading,spacing:5) { Text(g["name"] as? String ?? "Untitled").font(.system(size:13,weight:.medium)); Text(String((g["updated_at"] as? String ?? "").prefix(19))).font(.system(size:9,design:.monospaced)) }.frame(maxWidth:.infinity,alignment:.leading).padding(10).background((g["id"] as? String)==(s.geometry["id"] as? String) ? Color.primary.opacity(0.10):Color.clear) }.buttonStyle(.plain).disabled(s.busy)
} } }.frame(minHeight:100,maxHeight:240)
HStack { Button("New") { s.send(["op":"new"]) }; Button("Open…") { s.open("import") } }.disabled(s.busy)
HStack { Button("Export…") { s.save("export_geometry") }; Button("Delete") { confirmDelete=true } }.disabled(s.busy || s.geometry.isEmpty)
Trajectory(geometry:s.geometry)
Text("Import Emotional Atlas sessions or object/evolution JSON with emotional coordinates. The line follows the text from beginning to end.").font(.system(size:11)).foregroundStyle(.secondary)
Spacer()
Text("LOCAL STEERING · API TRANSFER\nQWEN3.5 / 0.8B\nEXPERIMENTAL RESEARCH SOFTWARE").font(.system(size:9,design:.monospaced)).lineSpacing(4)
}.padding(20).frame(width:255)
Divider()
VStack(alignment:.leading,spacing:16) {
Picker("Workspace",selection:$tab) { Text("01 Document").tag(0); Text("02 Geometry + weights").tag(1); Text("03 Verification").tag(2) }.pickerStyle(.segmented)
if tab==0 { document }
if tab==1 { geometry }
if tab==2 { verification }
}.padding(22).frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.top)
}
}.background(darkMode ? Color.black : Color.white).foregroundStyle(.primary).preferredColorScheme(darkMode ? .dark : .light).frame(minWidth:1080,minHeight:770)
.alert("AFFECTOR",isPresented:Binding(get:{s.error != nil},set:{if !$0 {s.error=nil}})) { Button("OK") {s.error=nil} } message: {Text(s.error ?? "")}
.alert("Delete this geometry?",isPresented:$confirmDelete) { Button("Delete",role:.destructive) {s.send(["op":"delete","id":s.geometry["id"] ?? ""]);s.geometry=[:];s.editor=""};Button("Cancel",role:.cancel){} } message:{Text("Export a copy first if you want to retain its trajectory and calibrated weights.")}
}
func label(_ text:String)->some View { Text(text).font(.system(size:10,weight:.semibold,design:.monospaced)).tracking(1.5) }
var document: some View {
VStack(alignment:.leading,spacing:14) {
HStack { Button("Open document…") {s.open("read_doc",document:true)}; Button("Open transfer…") {s.open("load_run")}; Button("Save transfer…") {s.save("save_run")}.disabled(!s.hasRun); Button("Reset text") {s.send(["op":"reset_text"])}.help("Clear both texts and the automatic checkpoint without saving. Keeps geometry, settings and manually saved files."); Spacer();Button("Export output…") {s.save("export_doc",document:true)}.disabled(!s.hasRun) }.disabled(s.busy)
if !s.runSummary.isEmpty { Text(s.runSummary).font(.system(size:11,design:.monospaced)).textSelection(.enabled) }
HStack(alignment:.top,spacing:18) {
VStack(alignment:.leading) {label("INPUT");TextEditor(text:$s.source).font(.system(size:15,design:.serif)).padding(6).overlay(Rectangle().stroke(.gray.opacity(0.4))).disabled(s.busy)}
VStack(alignment:.leading) {label("TRANSFORMED TEXT");ScrollView {Text(s.output.isEmpty ? "Your transformed document will appear here." : s.output).font(.system(size:15,design:.serif)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading).padding(12)}.frame(maxWidth:.infinity,maxHeight:.infinity).overlay(Rectangle().stroke(.gray.opacity(0.4)))}
}.frame(minHeight:190)
Picker("Rewrite style", selection:$s.rewriteStyle) { Text("Minimal").tag("Minimal"); Text("Restrained").tag("Restrained"); Text("Balanced").tag("Balanced"); Text("Creative").tag("Creative") }.pickerStyle(.segmented).disabled(s.busy)
Text(["Minimal":"Light edits to a few words or phrases; preserve nearly all original wording.", "Restrained":"Preserve meaning and details; adjust emotional tone through wording and rhythm.", "Balanced":"Reshape phrasing and structure with modest imagery; preserve facts and events.", "Creative":"Allow new imagery, perspectives and events; meaning may change."][s.rewriteStyle] ?? "").font(.system(size:11)).foregroundStyle(.secondary)
Text("TXT / RTF / DOCX formatting is not preserved. Generated drafts are saved, including repetitions and incomplete endings. Requested steering is preserved. Incomplete and repetitive endings are trimmed to complete sentences; raw drafts remain in the report. Output aims for the original length; paragraphs share a 125% ceiling.").font(.system(size:10,design:.monospaced)).foregroundStyle(.secondary)
Divider()
HStack { Picker("Transform with",selection:$s.backend) {ForEach(providers,id:\.self){Text($0)}}.frame(width:260);Spacer();Button("Transform / resume") {s.transform()}.buttonStyle(.bordered).foregroundStyle(.primary).disabled(s.busy || s.geometry.isEmpty) }.disabled(s.busy)
if s.backend != "Local Qwen" { HStack {TextField("Model",text:$s.model);SecureField("API key · kept in memory only",text:$s.key)}.textFieldStyle(.roundedBorder).disabled(s.busy);Text("API mode uses coordinate instructions. Hosted APIs do not expose transformer activations.").font(.system(size:11)) }
else { HStack {Text("Sampling").font(.system(size:11,design:.monospaced));Text("Temperature");TextField("Temperature",value:$s.temperature,format:.number).frame(width:55);Text("Top-p").padding(.leading,12);TextField("Top-p",value:$s.topP,format:.number).frame(width:55);Text("Top-k").padding(.leading,12);TextField("Top-k",value:$s.topK,format:.number).frame(width:50)}.font(.system(size:11)).disabled(s.busy) }
HStack {Text("Output tokens").font(.system(size:11));TextField("Output tokens",value:$s.tokens,format:.number).frame(width:65);Text("base token allowance (32–2048); expanded automatically for longer passages").font(.system(size:11));Spacer();Button("Save progress…") {s.save("save_run")}.disabled(!s.hasRun)}
}.onChange(of:s.backend) { _,b in if b=="VT ARC" {s.model="gpt-oss-120b"} else if b=="OpenAI" {s.model="gpt-6-luna"} }
}
var geometry: some View {
VStack(alignment:.leading,spacing:12) {
Text("A shape is a trajectory and a transformer module.").font(.system(size:24,weight:.semibold))
Text("Edit the name, points, selected layers (0–23), controls, or compiled basis vectors below. Save to update the preview. Recalibrate after changing layers or model identity.").font(.system(size:12))
Text("t = position · v = valence [−1,1] · d = direction [−1,1] · i = intensity [0,1]\nstrength = injection scale · budget = maximum relative RMS · inertia = temporal smoothing\naxis_top_k / axis_mass / axis_temperature select and reshape activation coefficients, independently of token sampling.").font(.system(size:11,design:.monospaced)).lineSpacing(5)
Text("Extreme values for strength, budget, and inertia may cause generation failures.").font(.system(size:12)).foregroundStyle(.red)
HStack {
control("Strength", "strength", 0...1)
control("Budget", "budget", 0...1)
control("Inertia", "inertia", 0...0.99)
}.disabled(s.busy)
TextEditor(text:$s.editor).font(.system(size:11,design:.monospaced)).padding(6).overlay(Rectangle().stroke(.gray.opacity(0.4))).disabled(s.busy)
HStack {Button("Save geometry") {s.geometryAction("save")};Button("Calibrate local module") {s.geometryAction("calibrate")};Button(s.showWeights ? "Hide numerical weights" : "Edit numerical weights") {s.toggleWeights()};Spacer();Text("Calibration is experimental; emotional fit is measured separately.").font(.system(size:10)).foregroundStyle(.secondary)}.disabled(s.busy || s.geometry.isEmpty)
}
}
func control(_ title:String,_ key:String,_ range:ClosedRange<Double>)->some View {
let binding=Binding<Double>(get:{
let obj=(try? JSONSerialization.jsonObject(with:Data(s.editor.utf8))) as? [String:Any]
return (obj?["controls"] as? [String:Any])?[key] as? Double ?? 0
},set:{ value in
guard var g=(try? JSONSerialization.jsonObject(with:Data(s.editor.utf8))) as? [String:Any],var c=g["controls"] as? [String:Any] else {return}
c[key]=value;g["controls"]=c;s.editor=pretty(g)
})
return VStack(alignment:.leading){Text("\(title) \(binding.wrappedValue, specifier:"%.2f")").font(.system(size:11,design:.monospaced));Slider(value:binding,in:range).tint(.primary)}
}
var verification: some View {
VStack(alignment:.leading,spacing:14) {
Text("Measure the change, passage by passage.").font(.system(size:24,weight:.semibold))
Text("Evaluate original and revised text with the Emotional Atlas scoring prompt. Similarity measures proximity to the requested coordinates; it is not a probability or a measure of semantic fidelity.").font(.system(size:12))
HStack {Picker("Evaluator",selection:$s.evaluator){ForEach(providers,id:\.self){Text($0)}}.frame(width:260);Spacer();Button("Verify output") {s.send(["op":"verify","provider":s.evaluator,"model":s.evalModel,"key":s.evalKey])}.disabled(s.busy || !s.hasRun);Button("Export report…") {s.save("export_report")}.disabled(s.busy || !s.hasRun)}
if s.evaluator != "Local Qwen" {HStack {TextField("Evaluator model",text:$s.evalModel);SecureField("Evaluator API key",text:$s.evalKey)}.textFieldStyle(.roundedBorder)}
HStack(spacing:30) { score("ORIGINAL",s.verification["before_similarity"] as? Double);score("TRANSFORMED",s.verification["after_similarity"] as? Double);score("IMPROVEMENT",s.verification["improvement"] as? Double);VStack(alignment:.leading){label("PAIRED COVERAGE");Text("\(s.verification["paired_count"] as? Int ?? 0) / \(s.verification["total_count"] as? Int ?? 0)").font(.system(size:26,weight:.light))} }.padding(.vertical,12)
Text("Similarity = 1 − distance / (2√3), with z = 2 × intensity − 1. Unknown directions and failed evaluations are excluded. Local 0.8B scoring may be unreliable; compare with an independent API evaluator.").font(.system(size:11)).foregroundStyle(.secondary)
label("TRANSFER + EVALUATION RECORD · FULL WEIGHTS IN EXPORT")
ScrollView {Text(s.report.isEmpty ? "Run a transfer, then verify its output." : s.report).font(.system(size:11,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading).padding(12)}.frame(maxWidth:.infinity,maxHeight:.infinity).overlay(Rectangle().stroke(.gray.opacity(0.4)))
}.onChange(of:s.evaluator) {_,b in if b=="VT ARC" {s.evalModel="gpt-oss-120b"} else if b=="OpenAI" {s.evalModel="gpt-6-luna"} }
}
func score(_ name:String,_ value:Double?)->some View {VStack(alignment:.leading){label(name);Text(value.map{String(format:"%.3f",$0)} ?? "—").font(.system(size:26,weight:.light,design:.monospaced))}}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
var studio:Studio?
func applicationShouldTerminate(_ sender:NSApplication)->NSApplication.TerminateReply {
if studio?.busy == true {let a=NSAlert();a.messageText="Stop the active operation and quit?";a.informativeText="Generated drafts are saved, including repetitions and incomplete endings. Requested steering is preserved. Incomplete and repetitive endings are trimmed to complete sentences; raw drafts remain in the report. Output aims for the original length; paragraphs share a 125% ceiling. The current passage may need to be repeated.";a.addButton(withTitle:"Quit");a.addButton(withTitle:"Keep working");if a.runModal() != .alertFirstButtonReturn{return .terminateCancel}}
studio?.stopProcess();return .terminateNow
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender:NSApplication)->Bool {true}
}
@main struct AffectorApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject private var studio=Studio()
var body: some Scene {WindowGroup {ContentView().environmentObject(studio).onAppear{if delegate.studio == nil {delegate.studio=studio;studio.start()}}}.defaultSize(width:1230,height:850).commands {CommandGroup(replacing:.newItem){};CommandGroup(after:.saveItem){Button("Save transfer…"){studio.save("save_run")}.keyboardShortcut("s").disabled(!studio.hasRun)}}}
}
|