// // JarvisView.swift // Confab // // Main modal for managing Jarvis (oAI-Web) agents and usage. // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen import SwiftUI // MARK: - Editor context (avoids .sheet timing bug) private struct AgentEditContext: Identifiable { let id = UUID() let agent: JarvisAgent? // nil → new } // MARK: - JarvisView struct JarvisView: View { @Environment(\.dismiss) var dismiss @State private var selectedTab = 0 @State private var agents: [JarvisAgent] = [] @State private var runs: [String: [JarvisAgentRun]] = [:] @State private var usageStats: [JarvisUsageStat] = [] @State private var credits: JarvisCreditsResponse? = nil @State private var queueStatus: JarvisQueueStatus? = nil @State private var isLoadingAgents = false @State private var isLoadingUsage = false @State private var selectedAgent: JarvisAgent? = nil @State private var editContext: AgentEditContext? = nil @State private var selectedRun: JarvisAgentRun? = nil @State private var errorMessage: String? = nil @State private var actionInProgress: Set = [] private let service = JarvisService.shared var body: some View { VStack(spacing: 0) { // Header HStack(alignment: .center) { Label("Jarvis", systemImage: "server.rack") .font(.system(size: 18, weight: .bold)) Spacer() if let qs = queueStatus { queueStatusBadge(qs) } Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.title2).foregroundStyle(.secondary) } .buttonStyle(.plain) .keyboardShortcut(.escape, modifiers: []) } .padding(.horizontal, 24) .padding(.top, 20) .padding(.bottom, 10) // Tab picker Picker("", selection: $selectedTab) { Text("Agents").tag(0) Text("Usage").tag(1) } .pickerStyle(.segmented) .padding(.horizontal, 24) .padding(.bottom, 10) Divider() if let err = errorMessage { HStack { Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) Text(err).font(.callout).foregroundStyle(.secondary) Spacer() Button("Dismiss") { errorMessage = nil }.buttonStyle(.borderless) } .padding(.horizontal, 24) .padding(.vertical, 8) Divider() } switch selectedTab { case 0: agentsTab default: usageTab } } .frame(minWidth: 840, idealWidth: 960, minHeight: 560, idealHeight: 720) .task { await loadAll() } .sheet(item: $editContext) { ctx in JarvisAgentEditorSheet(agent: ctx.agent, onSave: { input in await saveAgent(existing: ctx.agent, input: input) }) } .sheet(item: $selectedRun) { run in JarvisRunDetailSheet(run: run) } } // MARK: - Agents Tab private var agentsTab: some View { NavigationSplitView { agentList } detail: { if let agent = selectedAgent { agentDetailView(agent) } else { ContentUnavailableView("Select an Agent", systemImage: "server.rack", description: Text("Choose an agent from the list to view details and run history")) } } .navigationSplitViewStyle(.balanced) } private var agentList: some View { VStack(spacing: 0) { // Toolbar HStack(spacing: 8) { Button { editContext = AgentEditContext(agent: nil) } label: { Label("New Agent", systemImage: "plus") } .buttonStyle(.bordered) .controlSize(.small) Spacer() if isLoadingAgents { ProgressView().scaleEffect(0.7).frame(width: 14, height: 14) } Button { Task { await loadAgents() } } label: { Image(systemName: "arrow.clockwise") } .buttonStyle(.borderless) .help("Refresh agents") // Queue control if let qs = queueStatus { Button { Task { do { if qs.paused == true { try await service.resumeAll() } else { try await service.pauseAll() } await loadQueueStatus() } catch { errorMessage = error.localizedDescription } } } label: { Image(systemName: qs.paused == true ? "play.fill" : "pause.fill") } .buttonStyle(.borderless) .help(qs.paused == true ? "Resume queue" : "Pause queue") } } .padding(.horizontal, 12) .padding(.vertical, 8) Divider() if agents.isEmpty && !isLoadingAgents { ContentUnavailableView("No Agents", systemImage: "server.rack") .frame(maxWidth: .infinity, maxHeight: .infinity) } else { List(agents, id: \.id, selection: $selectedAgent) { agent in agentRow(agent) .tag(agent) } .listStyle(.plain) } } } private func agentRow(_ agent: JarvisAgent) -> some View { HStack(spacing: 8) { // Status dot Circle() .fill(agent.isRunning == true ? Color.green : Color.secondary.opacity(0.4)) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: 2) { Text(agent.name) .font(.system(size: 13, weight: .medium)) .lineLimit(1) if let schedule = agent.schedule, !schedule.isEmpty { Text(schedule) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } } .frame(maxWidth: .infinity, alignment: .leading) // Enable toggle Toggle("", isOn: Binding( get: { agent.enabled }, set: { _ in Task { await toggleAgent(agent) } } )) .toggleStyle(.switch) .controlSize(.mini) .labelsHidden() // Run / Stop button if agent.isRunning == true { Button { Task { await stopAgent(agent) } } label: { Image(systemName: "stop.fill") .font(.system(size: 11)) .foregroundStyle(.red) } .buttonStyle(.borderless) .disabled(actionInProgress.contains(agent.id)) .help("Stop agent") } else { Button { Task { await runAgent(agent) } } label: { Image(systemName: actionInProgress.contains(agent.id) ? "ellipsis" : "play.fill") .font(.system(size: 11)) .foregroundStyle(.green) } .buttonStyle(.borderless) .disabled(actionInProgress.contains(agent.id) || !agent.enabled) .help("Run agent now") } } .padding(.vertical, 2) .contentShape(Rectangle()) } @ViewBuilder private func agentDetailView(_ agent: JarvisAgent) -> some View { VStack(alignment: .leading, spacing: 0) { // Agent info header VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { Circle() .fill(agent.isRunning == true ? Color.green : Color.secondary.opacity(0.4)) .frame(width: 10, height: 10) Text(agent.name) .font(.system(size: 16, weight: .semibold)) if !agent.enabled { Text("Disabled") .font(.caption) .padding(.horizontal, 6).padding(.vertical, 2) .background(Color.orange.opacity(0.15)) .foregroundStyle(.orange) .cornerRadius(4) } } Text(agent.model) .font(.caption) .foregroundStyle(.secondary) } Spacer() Button { editContext = AgentEditContext(agent: agent) } label: { Label("Edit", systemImage: "pencil") } .buttonStyle(.bordered) .controlSize(.small) Button(role: .destructive) { Task { await deleteAgent(agent) } } label: { Image(systemName: "trash") } .buttonStyle(.bordered) .controlSize(.small) } if !agent.description.isEmpty { Text(agent.description) .font(.callout) .foregroundStyle(.secondary) } if let schedule = agent.schedule, !schedule.isEmpty { Label(schedule, systemImage: "clock") .font(.caption) .foregroundStyle(.secondary) } } .padding(16) Divider() // Prompt preview DisclosureGroup("Prompt") { ScrollView { Text(agent.prompt) .font(.system(size: 12, design: .monospaced)) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .padding(8) } .frame(maxHeight: 120) .background(Color.gray.opacity(0.06)) .cornerRadius(6) .padding(.horizontal, 16) .padding(.bottom, 8) } .padding(.horizontal, 16) .padding(.top, 8) Divider() // Run history Text("Run History") .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.secondary) .padding(.horizontal, 16) .padding(.top, 10) .padding(.bottom, 4) let agentRuns = runs[agent.id] ?? [] if agentRuns.isEmpty { Text("No runs yet") .font(.callout) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } else { List(agentRuns) { run in RunHistoryRow(run: run) .contentShape(Rectangle()) .onTapGesture { selectedRun = run } } .listStyle(.plain) } } .task(id: agent.id) { await loadRuns(for: agent) } } // MARK: - Usage Tab private var usageTab: some View { VStack(spacing: 0) { // Credits card if let creds = credits, let balance = creds.remainingBalance { HStack(spacing: 16) { VStack(alignment: .leading, spacing: 4) { Text("OpenRouter Balance") .font(.caption) .foregroundStyle(.secondary) Text(String(format: "$%.4f", balance)) .font(.system(size: 22, weight: .semibold, design: .monospaced)) .foregroundStyle(balance < 1.0 ? .orange : .primary) } Spacer() if let total = creds.totalCredits { VStack(alignment: .trailing, spacing: 4) { Text("Total Credits") .font(.caption).foregroundStyle(.secondary) Text(String(format: "$%.2f", total)) .font(.callout).foregroundStyle(.secondary) } } if let used = creds.totalUsage { VStack(alignment: .trailing, spacing: 4) { Text("Total Used") .font(.caption).foregroundStyle(.secondary) Text(String(format: "$%.4f", used)) .font(.callout).foregroundStyle(.secondary) } } } .padding(16) .background(Color.blue.opacity(0.06)) .cornerRadius(10) .padding(.horizontal, 16) .padding(.top, 12) .padding(.bottom, 8) } if isLoadingUsage { ProgressView("Loading usage…") .frame(maxWidth: .infinity, maxHeight: .infinity) } else if usageStats.isEmpty { ContentUnavailableView("No Usage Data", systemImage: "chart.bar", description: Text("Run some agents to see usage statistics")) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { // Header row HStack { Text("Agent") .frame(maxWidth: .infinity, alignment: .leading) Text("Runs") .frame(width: 50, alignment: .trailing) Text("Input") .frame(width: 80, alignment: .trailing) Text("Output") .frame(width: 80, alignment: .trailing) Text("Cost") .frame(width: 80, alignment: .trailing) } .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) .padding(.horizontal, 16) .padding(.vertical, 6) Divider() ScrollView { VStack(spacing: 0) { ForEach(usageStats) { stat in usageRow(stat) Divider().padding(.leading, 16) } // Totals row let totalRuns = usageStats.compactMap(\.runCount).reduce(0, +) let totalInput = usageStats.compactMap(\.totalInputTokens).reduce(0, +) let totalOutput = usageStats.compactMap(\.totalOutputTokens).reduce(0, +) let totalCost = usageStats.compactMap(\.totalCostUsd).reduce(0, +) HStack { Text("Total") .font(.system(size: 13, weight: .semibold)) .frame(maxWidth: .infinity, alignment: .leading) Text("\(totalRuns)") .frame(width: 50, alignment: .trailing) .font(.system(size: 13, weight: .semibold)) Text(formatTokens(totalInput)) .frame(width: 80, alignment: .trailing) .font(.system(size: 13, weight: .semibold)) Text(formatTokens(totalOutput)) .frame(width: 80, alignment: .trailing) .font(.system(size: 13, weight: .semibold)) Text(String(format: "$%.4f", totalCost)) .frame(width: 80, alignment: .trailing) .font(.system(size: 13, weight: .semibold, design: .monospaced)) } .padding(.horizontal, 16) .padding(.vertical, 10) .background(Color.gray.opacity(0.07)) } } HStack { Spacer() Button { Task { await loadUsage() } } label: { Label("Refresh", systemImage: "arrow.clockwise") } .buttonStyle(.borderless) .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal, 16) .padding(.vertical, 6) } } } private func usageRow(_ stat: JarvisUsageStat) -> some View { HStack { Text(stat.displayName) .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) Text("\(stat.runCount ?? 0)") .frame(width: 50, alignment: .trailing) .foregroundStyle(.secondary) Text(formatTokens(stat.totalInputTokens ?? 0)) .frame(width: 80, alignment: .trailing) .foregroundStyle(.secondary) Text(formatTokens(stat.totalOutputTokens ?? 0)) .frame(width: 80, alignment: .trailing) .foregroundStyle(.secondary) Text(stat.totalCostUsd.map { String(format: "$%.4f", $0) } ?? "—") .frame(width: 80, alignment: .trailing) .font(.system(size: 13, design: .monospaced)) } .font(.system(size: 13)) .padding(.horizontal, 16) .padding(.vertical, 8) } // MARK: - Queue Status Badge private func queueStatusBadge(_ qs: JarvisQueueStatus) -> some View { HStack(spacing: 6) { if qs.paused == true { Label("Paused", systemImage: "pause.fill") .foregroundStyle(.orange) } else if let running = qs.runningCount, running > 0 { Label("^[\(running) running](inflect: true)", systemImage: "circle.fill") .foregroundStyle(.green) } else { Label("Idle", systemImage: "checkmark.circle") .foregroundStyle(.secondary) } } .font(.caption) .padding(.horizontal, 10) .padding(.vertical, 4) .background(Color.gray.opacity(0.1)) .cornerRadius(6) } // MARK: - Data Loading private func loadAll() async { await withTaskGroup(of: Void.self) { group in group.addTask { await self.loadAgents() } group.addTask { await self.loadUsage() } group.addTask { await self.loadQueueStatus() } } } private func loadAgents() async { isLoadingAgents = true do { let fetched = try await service.listAgents() agents = fetched if let current = selectedAgent { selectedAgent = fetched.first(where: { $0.id == current.id }) } } catch { errorMessage = error.localizedDescription } isLoadingAgents = false } private func loadRuns(for agent: JarvisAgent) async { do { let fetched = try await service.agentRuns(id: agent.id) runs[agent.id] = fetched } catch { errorMessage = error.localizedDescription } } private func loadUsage() async { isLoadingUsage = true do { async let statsTask = service.usage() async let creditsTask = service.credits() usageStats = try await statsTask credits = try? await creditsTask } catch { errorMessage = error.localizedDescription } isLoadingUsage = false } private func loadQueueStatus() async { queueStatus = try? await service.queueStatus() } // MARK: - Agent Actions private func toggleAgent(_ agent: JarvisAgent) async { do { let updated = try await service.toggleAgent(id: agent.id) updateAgent(updated) } catch { errorMessage = error.localizedDescription } } private func runAgent(_ agent: JarvisAgent) async { actionInProgress.insert(agent.id) do { try await service.runAgent(id: agent.id) try? await Task.sleep(for: .seconds(1)) await loadAgents() } catch { errorMessage = error.localizedDescription } actionInProgress.remove(agent.id) } private func stopAgent(_ agent: JarvisAgent) async { do { try await service.stopAgent(id: agent.id) try? await Task.sleep(for: .seconds(1)) await loadAgents() } catch { errorMessage = error.localizedDescription } } private func deleteAgent(_ agent: JarvisAgent) async { do { try await service.deleteAgent(id: agent.id) agents.removeAll { $0.id == agent.id } if selectedAgent?.id == agent.id { selectedAgent = nil } } catch { errorMessage = error.localizedDescription } } private func saveAgent(existing: JarvisAgent?, input: JarvisAgentInput) async { do { let result: JarvisAgent if let existing { result = try await service.updateAgent(id: existing.id, input) } else { result = try await service.createAgent(input) } updateAgent(result) selectedAgent = result } catch { errorMessage = error.localizedDescription } } private func updateAgent(_ updated: JarvisAgent) { if let idx = agents.firstIndex(where: { $0.id == updated.id }) { agents[idx] = updated } else { agents.append(updated) } if selectedAgent?.id == updated.id { selectedAgent = updated } } // MARK: - Formatting private func formatTokens(_ n: Int) -> String { if n >= 1_000_000 { return String(format: "%.1fM", Double(n) / 1_000_000) } if n >= 1_000 { return String(format: "%.1fK", Double(n) / 1_000) } return "\(n)" } } // MARK: - Run History Row private struct RunHistoryRow: View { let run: JarvisAgentRun var body: some View { HStack(spacing: 8) { statusIcon VStack(alignment: .leading, spacing: 1) { HStack(spacing: 6) { Text(run.formattedStarted) .font(.system(size: 12)) if let dur = run.formattedDuration { Text("· \(dur)") .font(.system(size: 12)) .foregroundStyle(.secondary) } } HStack(spacing: 8) { if run.totalTokens > 0 { Text("^[\(run.totalTokens) token](inflect: true)") .font(.caption2) .foregroundStyle(.secondary) } if let cost = run.costUsd, cost > 0 { Text(String(format: "$%.5f", cost)) .font(.caption2.monospaced()) .foregroundStyle(.secondary) } } } Spacer() // Tapping the row (wired by the caller) opens JarvisRunDetailSheet with the full, // untruncated output/error — this used to be an inline chevron-expand capped at // lineLimit(20), which isn't "complete" for long output. if run.output != nil || run.error != nil { Image(systemName: "chevron.right") .font(.caption2) .foregroundStyle(.tertiary) } } .padding(.vertical, 4) } @ViewBuilder private var statusIcon: some View { JarvisRunStatusIcon(status: run.status, size: 14) } } /// Shared between RunHistoryRow and JarvisRunDetailSheet so the two views can't drift apart. fileprivate struct JarvisRunStatusIcon: View { let status: String var size: CGFloat = 14 var body: some View { switch status { case "running": ProgressView().scaleEffect(size / 24).frame(width: size, height: size) case "completed", "success": Image(systemName: "checkmark.circle.fill") .font(.system(size: size)) .foregroundStyle(.green) case "failed", "error": Image(systemName: "xmark.circle.fill") .font(.system(size: size)) .foregroundStyle(.red) case "stopped": Image(systemName: "stop.circle.fill") .font(.system(size: size)) .foregroundStyle(.orange) default: Image(systemName: "circle") .font(.system(size: size)) .foregroundStyle(.secondary) } } } // MARK: - Run Detail Sheet /// Full, untruncated view of a single run — opened by tapping a row in the run history list. /// Output/error use an explicit Copy button rather than .textSelection(.enabled): a multi-line /// selectable Text can grab real AppKit first-responder status, and Escape then gets consumed by /// its own cancelOperation: handling instead of dismissing the sheet (silent beep, no log) — see /// the identical bug fixed in ModelInfoView. struct JarvisRunDetailSheet: View { let run: JarvisAgentRun @Environment(\.dismiss) var dismiss @State private var showOutputCopied = false @State private var showErrorCopied = false var body: some View { VStack(spacing: 0) { HStack { Text("Run Details") .font(.system(size: 18, weight: .bold)) Spacer() JarvisRunStatusIcon(status: run.status, size: 16) Text(run.status.capitalized) .font(.system(size: 13, weight: .medium)) .foregroundStyle(.secondary) Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.title2) .foregroundStyle(.secondary) } .buttonStyle(.plain) .padding(.leading, 8) .keyboardShortcut(.escape, modifiers: []) } .padding(.horizontal, 24) .padding(.top, 20) .padding(.bottom, 12) Divider() ScrollView { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 6) { detailRow("Started", run.formattedStarted) if let dur = run.formattedDuration { detailRow("Duration", dur) } if let trigger = run.triggerType { detailRow("Trigger", trigger.capitalized) } if run.totalTokens > 0 { detailRow("Tokens", "\(run.totalTokens.formatted()) (\((run.inputTokens ?? 0).formatted()) in / \((run.outputTokens ?? 0).formatted()) out)") } if let cost = run.costUsd, cost > 0 { detailRow("Cost", String(format: "$%.5f", cost)) } } if let err = run.error { Divider() outputBlock(title: "Error", text: err, color: .red, showCopied: $showErrorCopied) } if let out = run.output { Divider() outputBlock(title: "Output", text: out, color: .primary, showCopied: $showOutputCopied) } if run.output == nil && run.error == nil { Text("No output for this run.") .font(.callout) .foregroundStyle(.secondary) } } .padding(24) } } .frame(minWidth: 600, idealWidth: 700, minHeight: 450, idealHeight: 620) .onExitCommand { dismiss() } } private func detailRow(_ label: LocalizedStringKey, _ value: String) -> some View { HStack { Text(label) .font(.system(size: 13)) .foregroundStyle(.secondary) .frame(width: 90, alignment: .leading) Text(value) .font(.system(size: 13)) Spacer() } } @ViewBuilder private func outputBlock(title: LocalizedStringKey, text: String, color: Color, showCopied: Binding) -> some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 6) { Text(title) .font(.subheadline.weight(.medium)) .foregroundColor(.secondary) Spacer() Button(action: { copy(text, showCopied: showCopied) }) { HStack(spacing: 3) { Image(systemName: showCopied.wrappedValue ? "checkmark" : "doc.on.doc") .font(.system(size: 11)) if showCopied.wrappedValue { Text("Copied!") .font(.system(size: 11)) } } .foregroundColor(showCopied.wrappedValue ? .green : .secondary) } .buttonStyle(.plain) } Text(text) .font(.system(size: 12, design: .monospaced)) .foregroundStyle(color) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(color.opacity(0.06)) .cornerRadius(6) } } private func copy(_ text: String, showCopied: Binding) { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(text, forType: .string) withAnimation { showCopied.wrappedValue = true } DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { showCopied.wrappedValue = false } } } } // MARK: - Agent Editor Sheet struct JarvisAgentEditorSheet: View { let agent: JarvisAgent? let onSave: (JarvisAgentInput) async -> Void @Environment(\.dismiss) var dismiss @State private var name: String @State private var description: String @State private var model: String @State private var prompt: String @State private var schedule: String @State private var enabled: Bool @State private var maxToolCalls: String @State private var isSaving = false init(agent: JarvisAgent?, onSave: @escaping (JarvisAgentInput) async -> Void) { self.agent = agent self.onSave = onSave _name = State(initialValue: agent?.name ?? "") _description = State(initialValue: agent?.description ?? "") _model = State(initialValue: agent?.model ?? "") _prompt = State(initialValue: agent?.prompt ?? "") _schedule = State(initialValue: agent?.schedule ?? "") _enabled = State(initialValue: agent?.enabled ?? true) _maxToolCalls = State(initialValue: agent.flatMap(\.maxToolCalls).map(String.init) ?? "") } var isValid: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty && !model.trimmingCharacters(in: .whitespaces).isEmpty } var body: some View { VStack(spacing: 0) { // Header HStack { Text(agent == nil ? "New Agent" : "Edit Agent") .font(.system(size: 16, weight: .semibold)) Spacer() Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.title3).foregroundStyle(.secondary) } .buttonStyle(.plain) } .padding(.horizontal, 20) .padding(.top, 16) .padding(.bottom, 12) Divider() ScrollView { VStack(alignment: .leading, spacing: 16) { Group { editorField("Name") { TextField("Agent name", text: $name) .textFieldStyle(.roundedBorder) } editorField("Description") { TextField("Optional description", text: $description) .textFieldStyle(.roundedBorder) } editorField("Model") { TextField("e.g. anthropic/claude-sonnet-4-5", text: $model) .textFieldStyle(.roundedBorder) } editorField("Schedule") { TextField("Cron expression (e.g. 0 * * * *)", text: $schedule) .textFieldStyle(.roundedBorder) } editorField("Max Tool Calls") { TextField("Leave blank for default", text: $maxToolCalls) .textFieldStyle(.roundedBorder) .frame(maxWidth: 160) } editorField("Enabled") { Toggle("", isOn: $enabled) .toggleStyle(.switch) .labelsHidden() } } VStack(alignment: .leading, spacing: 6) { Text("Prompt") .font(.system(size: 13, weight: .medium)) TextEditor(text: $prompt) .font(.system(size: 13, design: .monospaced)) .frame(minHeight: 160) .overlay( RoundedRectangle(cornerRadius: 6) .strokeBorder(Color.gray.opacity(0.3)) ) } } .padding(20) } Divider() HStack(spacing: 10) { Spacer() Button("Cancel") { dismiss() } .keyboardShortcut(.escape, modifiers: []) Button { Task { isSaving = true let input = JarvisAgentInput( name: name.trimmingCharacters(in: .whitespaces), prompt: prompt, model: model.trimmingCharacters(in: .whitespaces), description: description, enabled: enabled, schedule: schedule.isEmpty ? nil : schedule, maxToolCalls: Int(maxToolCalls) ) await onSave(input) isSaving = false dismiss() } } label: { if isSaving { ProgressView().scaleEffect(0.8).frame(width: 16, height: 16) } else { Text(agent == nil ? "Create" : "Save") } } .buttonStyle(.borderedProminent) .disabled(!isValid || isSaving) .keyboardShortcut(.return, modifiers: .command) } .padding(.horizontal, 20) .padding(.vertical, 12) } .frame(minWidth: 560, idealWidth: 620, minHeight: 560, idealHeight: 700) } private func editorField(_ label: LocalizedStringKey, @ViewBuilder content: () -> Content) -> some View { HStack(alignment: .firstTextBaseline, spacing: 12) { Text(label) .font(.system(size: 13)) .frame(width: 100, alignment: .trailing) content() } } }