native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395import ComposableArchitectureimport Foundationimport SwiftUI
/// Settings → Agents → Workflows. The root is intentionally a compact index; every control/// whose effect is scoped to one workflow lives on the pushed detail page.struct WorkflowsSettingsView: View { let appLocale: Locale @State private var showsHistory = false @State private var historyStore = Store(initialState: WorkflowHistoryFeature.State()) { WorkflowHistoryFeature() } @Bindable var store: StoreOf<WorkflowsSettingsFeature> var body: some View { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { Form { WorkflowSettingsSections(store: store, showsIntroduction: true) WorkflowHistorySummarySection(store: historyStore) } .formStyle(.grouped) .navigationTitle("Workflows") .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .task { store.send(.task) } .alert($store.scope(state: \.alert, action: \.alert)) .sheet(isPresented: $store.isAuthoringPromptPresented.sending(\.setAuthoringPromptPresented)) { AskAgentHelpView( strings: workflowAuthoringPromptStrings( directory: store.workflowDirectory, appLocale: appLocale, systemLocale: AskAgentHelpPrompt.systemPreferredLocale() ) ) { store.send(.setAuthoringPromptPresented(false)) } } .sheet( isPresented: Binding(get: { store.newWorkflow != nil }, set: { if !$0 { store.send(.dismissNewWorkflow) } }) ) { NewWorkflowSheet(appLocale: appLocale, store: store) } } destination: { detailStore in WorkflowSettingsDetailView(store: detailStore) } .onDisappear { store.send(.teardown) } }}
/// Settings › Workflows › Run History: what the archive holds and a way to clear it. Inspecting/// individual runs is the toolbar's Workflow History popover; retention is automatic.private struct WorkflowHistorySummarySection: View { @Bindable var store: StoreOf<WorkflowHistoryFeature>
var body: some View { Section { HStack(alignment: .firstTextBaseline) { Text(summary) Spacer() if store.isBusy { ProgressView().controlSize(.small) } Button("Clear History…", role: .destructive) { store.send(.clearTapped) } .disabled(store.isBusy || store.removableCount == 0) .help("Delete every finished run from Workflow History") } if let error = store.error { Label(error, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.orange) .font(.callout) .textSelection(.enabled) } else if let result = store.result { Text(result) .font(.callout) .foregroundStyle(.secondary) } } header: { Text("Run History") } footer: { Text( """ Runs are listed in the toolbar's Workflow History. Records are stored in your home directory, \ outside project folders, and finished runs expire automatically. """) } .task { store.send(.refresh) } .alert($store.scope(state: \.alert, action: \.alert)) }
private var summary: String { guard store.hasLoaded else { return String(localized: "Loading run history…") } let count = store.runCount let size = ByteCountFormatter.string(fromByteCount: store.totalBytes, countStyle: .file) if count == 0 { return String(localized: "No workflow runs recorded.") } if count == 1 { return String(localized: "\(count) run · \(size) on disk") } return String(localized: "\(count) runs · \(size) on disk") }}
/// Shared compact index used by both Agents → Workflows and each Repository Settings page./// The surrounding Form/NavigationStack belongs to the host so repository workflows do not/// need an intermediate destination.struct WorkflowSettingsSections: View { @Bindable var store: StoreOf<WorkflowsSettingsFeature> let showsIntroduction: Bool
var body: some View { if showsIntroduction { Section { Text( "Workflows coordinate agents for repeatable tasks. Select one to review its setup or run it." ) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } }
if let blocker = store.cliBlocker { cliSection(blocker) }
if let loadError = store.loadError { Section { Label(loadError, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.orange) .font(.callout) } }
switch store.settingsScope { case .global: if !store.catalog.bundle.isEmpty { workflowSection(title: "Built-in", rows: store.catalog.bundle, includesActions: false) } workflowSection(title: "Your Workflows", rows: store.catalog.user, includesActions: true)
case .repository: workflowSection(title: "Workflows", rows: store.displayedRows, includesActions: true) } }
private func workflowSection( title: LocalizedStringKey, rows: [WorkflowSettingsRow], includesActions: Bool ) -> some View { Section(title) { if rows.isEmpty { Text(emptyMessage) .foregroundStyle(.secondary) .font(.callout) } ForEach(rows) { row in NavigationLink( state: WorkflowSettingsDetailFeature.State(row: row, runTargets: store.runTargets) ) { WorkflowCompactRow(row: row) } } if includesActions { actionRow } } }
private var emptyMessage: String { switch store.settingsScope { case .global: String(localized: "No personal workflows yet. Create one or ask an agent to write it.") case .repository: String(localized: "No workflows for this repository.") } }
private var actionRow: some View { HStack(spacing: 8) { Button { store.send(.newWorkflowTapped) } label: { Label("New Workflow…", systemImage: "plus") } .help("Name a workflow, pick a starter, and open it in your default editor")
Button("Create with Agent…") { store.send(.askAgentTapped) } .help("Copy a prompt that asks your coding agent to write a workflow for you")
Spacer()
Button { store.send(.revealUserFolderTapped) } label: { Image(systemName: "folder") } .buttonStyle(.borderless) .help("Show \(abbreviated(store.workflowDirectory)) in Finder") .accessibilityLabel("Show Workflows Folder") } }
private func cliSection(_ blocker: WorkflowsSettingsFeature.CLIBlocker) -> some View { Section { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 8) { switch blocker { case .cliUnusable(let status): Label( "Workflows need the prowl command line tool.", systemImage: "exclamationmark.triangle.fill" ) .foregroundStyle(.orange) Spacer() if let title = status.installActionTitle { Button(title) { store.send(.installCLITapped) } .help("\(title) the prowl command line tool at /usr/local/bin/prowl") .buttonStyle(.bordered) .controlSize(.small) } case .socketUnavailable: Label( "Prowl is not listening for the prowl command.", systemImage: "exclamationmark.triangle.fill" ) .foregroundStyle(.orange) } } switch blocker { case .cliUnusable(let status): Text(status.workflowBlockerCopy) .foregroundStyle(.secondary) case .socketUnavailable(let reason): Text(reason) .foregroundStyle(.secondary) } } .font(.callout) .fixedSize(horizontal: false, vertical: true) } }
private func abbreviated(_ url: URL) -> String { (url.path(percentEncoded: false) as NSString).abbreviatingWithTildeInPath }}
struct WorkflowCompactRow: View { let row: WorkflowSettingsRow
var body: some View { HStack(spacing: 10) { WorkflowIconImage(icon: row.icon, pointSize: 18) .frame(width: 22, height: 22)
VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline, spacing: 6) { Text(row.name) .foregroundStyle(.primary) if let workflowID = row.workflowID, workflowID != row.name { Text(workflowID) .font(.caption.monospaced()) .foregroundStyle(.secondary) } } if let description = row.description, !description.isEmpty { Text(description) .font(.callout) .foregroundStyle(.secondary) .lineLimit(2) } }
Spacer(minLength: 12) WorkflowStatusLabel(status: row.status) } .padding(.vertical, 2) .contentShape(.rect) }}
struct WorkflowIconImage: View { let icon: String? let pointSize: CGFloat
var body: some View { Image(systemName: icon ?? "point.3.connected.trianglepath.dotted") .font(.system(size: pointSize)) .foregroundStyle(.secondary) .accessibilityHidden(true) }}
struct WorkflowStatusLabel: View { let status: WorkflowSettingsRow.Status
var body: some View { Label(title, systemImage: symbol) .font(.caption) .foregroundStyle(color) .lineLimit(1) .help(helpText) }
private var title: String { switch status { case .invalid(let errors): if errors == 1 { String(localized: "Invalid · \(errors) error") } else { String(localized: "Invalid · \(errors) errors") } case .disabled: String(localized: "Disabled") case .superseded: String(localized: "Superseded") case .readyWithWarnings(let warnings): if warnings == 1 { String(localized: "Ready · \(warnings) warning") } else { String(localized: "Ready · \(warnings) warnings") } case .ready: String(localized: "Ready") } }
private var symbol: String { switch status { case .invalid: "xmark.octagon.fill" case .disabled: "pause.circle.fill" case .superseded: "arrow.trianglehead.branch" case .readyWithWarnings: "exclamationmark.triangle.fill" case .ready: "checkmark.circle.fill" } }
private var color: Color { switch status { case .invalid: .red case .disabled, .superseded: .secondary case .readyWithWarnings: .orange case .ready: .green } }
private var helpText: String { switch status { case .invalid(let errors): if errors == 1 { String(localized: "This workflow has \(errors) validation error.") } else { String(localized: "This workflow has \(errors) validation errors.") } case .disabled: String(localized: "This workflow is hidden from launch surfaces and cannot run.") case .superseded: String(localized: "Another file with the same workflow ID takes precedence.") case .readyWithWarnings(let warnings): if warnings == 1 { String(localized: "Ready with \(warnings) warning.") } else { String(localized: "Ready with \(warnings) warnings.") } case .ready: String(localized: "Ready to run.") } }}
func workflowAuthoringPromptStrings( directory: URL, draft: WorkflowStarterTemplate.Request? = nil, appLocale: Locale, systemLocale: Locale) -> AskAgentHelpStrings { let documentation = WorkflowStarterTemplate.bundledDocumentation var draft = draft draft?.documentation = documentation return WorkflowAuthoringPrompt.strings( skillPath: documentation.skillPath, manualPath: documentation.manualPath, workflowsDirectory: directory.path(percentEncoded: false), draft: draft, appLocale: appLocale, systemLocale: systemLocale )}
extension WorkflowStarterTemplate { /// The manual and skill inside this app bundle, falling back to the standard install path. nonisolated static var bundledDocumentation: Documentation { let resources = ProwlPaths.bundledDocsURL?.deletingLastPathComponent() return Documentation( manualPath: ProwlPaths.bundledDocsURL? .appending(path: "components/workflows.md", directoryHint: .notDirectory) .path(percentEncoded: false) ?? Documentation.standard.manualPath, skillPath: resources? .appending(path: "skills/prowl-workflow/SKILL.md", directoryHint: .notDirectory) .path(percentEncoded: false) ?? Documentation.standard.skillPath) }}