native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409import ComposableArchitectureimport ProwlCLISharedimport SwiftUI
struct WorkflowSettingsDetailView: View { @Bindable var store: StoreOf<WorkflowSettingsDetailFeature> @Environment(\.controlActiveState) private var controlActiveState
var body: some View { Group { if let row = store.row { Form { workflowSection(row) if row.scriptActionCount > 0 { Section("Script Actions") { Text( """ This bundle contains \(row.scriptActionCount) script action(s). Scripts run with your local user permissions. """ ) Button("Review Bundle…") { store.send(.reviewBundleTapped) } .help("Inspect all bundle files and approve this version before running scripts") } } runSection(row) if !row.roles.isEmpty { rolesSection(row) } runSetupSection(row) validationSection(row) sourceFileSection(row) } .formStyle(.grouped) .navigationTitle(row.name) } else { ContentUnavailableView { Label("Workflow Unavailable", systemImage: "doc.badge.ellipsis") } description: { Text("The workflow file was moved or deleted. Go back to choose another workflow.") } .navigationTitle("Workflow Unavailable") } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .task { store.send(.appeared) } .onChange(of: controlActiveState) { _, state in // Coming back from the main window: the selected worktree may have changed. if state == .key { store.send(.appeared) } } .alert($store.scope(state: \.alert, action: \.alert)) .sheet( isPresented: Binding(get: { store.bundleReview != nil }, set: { if !$0 { store.send(.dismissBundleReview) } }) ) { WorkflowBundleReviewView( review: store.bundleReview, selectFile: { store.send(.reviewFileSelected($0)) }, approve: { store.send(.approveBundleTapped) }, reveal: { store.send(.revealInFinderTapped) }, close: { store.send(.dismissBundleReview) }) } }
private func workflowSection(_ row: WorkflowSettingsRow) -> some View { Section("Workflow") { HStack(alignment: .top, spacing: 12) { WorkflowIconImage(icon: row.icon, pointSize: 24) .frame(width: 30, height: 30) VStack(alignment: .leading, spacing: 4) { Text(row.name) .font(.headline) if let description = row.description, !description.isEmpty { Text(description) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } } Spacer(minLength: 16) WorkflowStatusLabel(status: row.status) }
if let workflowID = row.workflowID { LabeledContent("ID") { Text(workflowID) .font(.body.monospaced()) .textSelection(.enabled) } }
if row.settingsKey != nil { Toggle( "Enabled", isOn: Binding( get: { row.isEnabled }, set: { store.send(.enabledChanged($0)) }) ) .help( row.isEnabled ? "Available from workflow launch surfaces and the prowl CLI" : "Hidden from workflow launch surfaces and rejected by the prowl CLI") } } }
@ViewBuilder private func runSection(_ row: WorkflowSettingsRow) -> some View { Section("Run") { if let target = store.preferredRunTarget { HStack(spacing: 6) { Button { store.send(.runTapped(worktreeID: target.id, forceSheet: false)) } label: { Label("Run in \(target.displayName)", systemImage: "play.fill") } .buttonStyle(.borderedProminent) .disabled(!canRun(row)) .help(runHelp(row, target: target))
Menu { Section("Run In") { ForEach(store.runTargets) { candidate in Button(candidate.displayName) { store.send(.runTapped(worktreeID: candidate.id, forceSheet: false)) } } } Divider() Button("Run with Options…") { store.send(.runTapped(worktreeID: target.id, forceSheet: true)) } } label: { Image(systemName: "chevron.down") .accessibilityHidden(true) } .menuStyle(.borderlessButton) .menuIndicator(.hidden) .fixedSize() .disabled(!canRun(row)) .help("Choose another worktree or review options before running") .accessibilityLabel("Workflow run options") } } else { Label("Open a worktree to run this workflow.", systemImage: "rectangle.stack.badge.plus") .foregroundStyle(.secondary) }
if !canRun(row), let reason = unavailableRunReason(row) { Text(reason) .font(.callout) .foregroundStyle(.secondary) } } }
@ViewBuilder private func rolesSection(_ row: WorkflowSettingsRow) -> some View { Section("Roles") { ForEach(row.roles) { role in VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline) { Text(role.name) .font(.body.monospaced()) Spacer() Text(sourceLabel(role.source)) .font(.callout) .foregroundStyle(.secondary) .help(sourceHelp(role.source)) } Text(role.behaviorDescription) .font(.callout) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true)
if let launch = role.launch { preferredProfileMenu(launch) } } .padding(.vertical, 2) }
if !row.launchRoles.isEmpty { Button("Manage Agent Profiles…") { store.send(.manageProfilesTapped) } .help("Open Settings → Agents → Profiles") } } }
private func preferredProfileMenu(_ role: WorkflowSettingsRow.LaunchRole) -> some View { LabeledContent("Preferred Agent Profile") { Menu { Button("Choose Automatically") { store.send(.preferredProfileChanged(role.memoryKey, nil)) } Divider() ForEach(role.candidates) { candidate in Button { store.send(.preferredProfileChanged(role.memoryKey, candidate.profileID)) } label: { HStack { Text(candidate.name) if candidate.profileID == role.rememberedProfileID { Image(systemName: "checkmark") .accessibilityHidden(true) } } } .disabled(candidate.unavailableReason != nil) .help(candidate.unavailableReason ?? candidate.agentToken) } if let rememberedProfileID = role.rememberedProfileID, !role.candidates.contains(where: { $0.profileID == rememberedProfileID }) { Divider() Text("Deleted Profile") } } label: { Text(preferredProfileName(role)) } .help( """ Preferred profile for this launch role. Choose Automatically lets Prowl resolve \ a qualifying profile when the workflow starts. """ ) } }
private func runSetupSection(_ row: WorkflowSettingsRow) -> some View { Section { Picker( "Run Setup", selection: Binding( get: { row.bindModeOverride }, set: { store.send(.runSetupChanged($0)) }) ) { Text("Follow Workflow").tag(Optional<WorkflowBindModeOverride.Mode>.none) Text("Always Review Before Running").tag(Optional(WorkflowBindModeOverride.Mode.ask)) Text("Run Directly When Possible").tag(Optional(WorkflowBindModeOverride.Mode.auto)) } .help( """ Controls whether Prowl presents the start sheet after resolving profiles, required role choices, \ and validation results. """ )
Text(runSetupDescription(row)) .font(.callout) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } header: { Text("Run Setup") } }
private func validationSection(_ row: WorkflowSettingsRow) -> some View { Section { if let shadowNote = row.shadowNote { Label(shadowNote, systemImage: "arrow.trianglehead.branch") .foregroundStyle(.secondary) } if let precedenceNote = row.precedenceNote { Label(precedenceNote, systemImage: "arrow.trianglehead.branch") .foregroundStyle(.secondary) } if row.diagnostics.isEmpty { Label("No validation issues", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } else { ForEach(Array(row.diagnostics.enumerated()), id: \.offset) { _, diagnostic in HStack(alignment: .firstTextBaseline, spacing: 8) { Image( systemName: diagnostic.severity == .error ? "xmark.octagon.fill" : "exclamationmark.triangle.fill" ) .foregroundStyle(diagnostic.severity == .error ? .red : .orange) .accessibilityLabel(diagnostic.severity == .error ? "Error" : "Warning")
VStack(alignment: .leading, spacing: 2) { Text(diagnostic.message) .fixedSize(horizontal: false, vertical: true) HStack(spacing: 8) { if let location = diagnostic.location { Text("Line \(location.line), column \(location.column)") } Text(diagnostic.code) } .font(.caption.monospaced()) .foregroundStyle(.secondary) } } } } } header: { Text("Validation") } footer: { Text("Prowl revalidates automatically when the workflow file changes.") } }
private func sourceFileSection(_ row: WorkflowSettingsRow) -> some View { Section("Source File") { HStack(spacing: 8) { Button("Open Workflow") { store.send(.openWorkflowTapped) } .help("Open \(row.fileName) in the default app for YAML files")
Button { store.send(.revealInFinderTapped) } label: { Image(systemName: "folder") } .buttonStyle(.borderless) .help("Show \(row.fileName) in Finder") .accessibilityLabel("Reveal Workflow in Finder")
Spacer()
if row.scope == .bundle { Text("Built-in · Read Only") .font(.callout) .foregroundStyle(.secondary) } else { Button("Delete Workflow…", role: .destructive) { store.send(.deleteTapped) } .help("Move \(row.fileName) to Trash after confirmation") } }
Text(abbreviated(row.url)) .font(.callout.monospaced()) .foregroundStyle(.secondary) .textSelection(.enabled) .lineLimit(2) .truncationMode(.middle) } }
private func canRun(_ row: WorkflowSettingsRow) -> Bool { row.isValid && row.isEnabled && row.shadowNote == nil && store.preferredRunTarget != nil }
private func unavailableRunReason(_ row: WorkflowSettingsRow) -> String? { if !row.isValid { return String(localized: "Fix the validation errors before running.") } if row.shadowNote != nil { return String(localized: "Another file with this ID takes precedence.") } if !row.isEnabled { return String(localized: "Enable this workflow before running.") } return nil }
private func runHelp(_ row: WorkflowSettingsRow, target: WorkflowSettingsRunTarget) -> String { unavailableRunReason(row) ?? String(localized: "Run this workflow in \(target.displayName)") }
private func sourceLabel(_ source: WorkflowRoleSource) -> String { switch source { case .current: String(localized: "Current Pane") case .pick: String(localized: "Choose Existing") case .launch: String(localized: "Launch New") } }
private func sourceHelp(_ source: WorkflowRoleSource) -> String { switch source { case .current: String(localized: "Uses the pane that starts the workflow and its detected agent.") case .pick: String(localized: "Selects an existing agent pane when the workflow starts.") case .launch: String(localized: "Creates a new agent pane using the role's requirements and preferred profile.") } }
private func preferredProfileName(_ role: WorkflowSettingsRow.LaunchRole) -> String { guard let id = role.rememberedProfileID else { return String(localized: "Choose Automatically") } return role.candidates.first { $0.profileID == id }?.name ?? String(localized: "Deleted Profile") }
private func runSetupDescription(_ row: WorkflowSettingsRow) -> String { switch row.bindModeOverride { case nil: switch row.declaredBind { case .ask: return String(localized: "Uses the workflow file's setting, which reviews choices before running.") case .auto: return String( localized: "Uses the workflow file's setting, which runs directly when all required choices can be resolved." ) case nil: return String(localized: "Uses the behavior declared in the workflow file.") } case .ask: return String(localized: "Always opens the review sheet before this workflow runs.") case .auto: return String( localized: "Starts directly when every required choice is available; otherwise Prowl opens the review sheet." ) } }
private func abbreviated(_ url: URL) -> String { (url.path(percentEncoded: false) as NSString).abbreviatingWithTildeInPath }}