diff --git a/docs/components/workflows.md b/docs/components/workflows.md index c39159bf7..a27defb60 100644 --- a/docs/components/workflows.md +++ b/docs/components/workflows.md @@ -27,7 +27,15 @@ reviewer reports findings with a verdict, and the two loop until the verdict is `clean` or the round cap is hit. A `.pwlworkflow` directory contains `workflow.yaml` with `schema: prowl.workflow/v1`, -and optional local script actions and assets. Workflow definitions are loaded from bundle directories. Roles have a `source`: +and optional local script actions and assets. Workflow definitions are loaded from bundle directories. + +Prowl registers `.pwlworkflow` as a macOS document package (`com.onevcat.prowl.workflow`). +Finder shows a bundle as one item with a Prowl workflow icon; use **Show Package +Contents** to browse its files. A bundle is still an ordinary directory for the CLI, +editors, and Git. Prowl does not open a bundle from Finder: a double-click reports +that no application is set to open it. Open or run workflows from Prowl. + +Roles have a `source`: | `source` | Meaning | How it is chosen | |---|---|---| @@ -325,7 +333,7 @@ The detail page owns these controls and explanations: | **Roles** | every `current`, `pick`, and `launch` role with a plain-language behavior summary. Only a `launch` role has a **Preferred Agent Profile** menu; **Choose Automatically** forgets the preference and lets Prowl resolve a qualifying profile at start. Unqualified profiles remain visible with the reason but cannot be selected. **Manage Agent Profiles…** appears once per page. | | **Run Setup** | **Follow Workflow**, **Always Review Before Running**, or **Run Directly When Possible**. The last choice starts immediately only when profiles, required role choices, inputs, and validation are already resolved; otherwise the review sheet still opens. | | **Validation** | every diagnostic as message, source location, and stable code. Saving the YAML revalidates automatically; there is no separate Validate button. | -| **Source File** | **Open Workflow** uses the default YAML app; the folder button reveals it in Finder. **Delete Workflow…** asks for confirmation, then moves a personal or repository workflow to Trash and returns to the list. Failed deletions keep the detail open with an error. Built-ins are read-only and cannot be deleted. YAML remains the source of truth—Settings does not embed an editor. | +| **Source File** | **Open Workflow** opens `workflow.yaml` in the default YAML app; the folder button selects the bundle in Finder, where **Show Package Contents** exposes its files. **Delete Workflow…** asks for confirmation, then moves a personal or repository workflow to Trash and returns to the list. Failed deletions keep the detail open with an error. Built-ins are read-only and cannot be deleted. YAML remains the source of truth—Settings does not embed an editor. | Starting from Settings keeps the review panel in the Settings window. Cancelling returns to the same detail without bringing the main window or terminal forward. diff --git a/supacode/Info.plist b/supacode/Info.plist index ffb3ab14a..5d4f246b8 100644 --- a/supacode/Info.plist +++ b/supacode/Info.plist @@ -42,8 +42,49 @@ $(PROWL_POSTHOG_API_KEY) ProwlPostHogHost $(PROWL_POSTHOG_HOST) + CFBundleDocumentTypes + + + CFBundleTypeName + Prowl Workflow + CFBundleTypeRole + None + LSHandlerRank + Owner + LSTypeIsPackage + + CFBundleTypeIconSystemGenerated + + LSItemContentTypes + + com.onevcat.prowl.workflow + + + UTExportedTypeDeclarations + + UTTypeIdentifier + com.onevcat.prowl.workflow + UTTypeDescription + Prowl Workflow + UTTypeConformsTo + + com.apple.package + + UTTypeIcons + + UTTypeIconText + Workflow + + UTTypeTagSpecification + + public.filename-extension + + pwlworkflow + + + UTTypeIdentifier com.onevcat.prowl.ghosttySurfaceId diff --git a/supacodeTests/WorkflowDocumentTypeTests.swift b/supacodeTests/WorkflowDocumentTypeTests.swift new file mode 100644 index 000000000..c9dc5cbb6 --- /dev/null +++ b/supacodeTests/WorkflowDocumentTypeTests.swift @@ -0,0 +1,46 @@ +import Foundation +import Testing + +@testable import supacode + +/// The test host is the app, so `Bundle.main` carries the Info.plist that Launch Services reads. +struct WorkflowDocumentTypeTests { + private static let identifier = "com.onevcat.prowl.workflow" + + @Test func exportedTypeIsAPackageForTheWorkflowExtension() throws { + let exported = try #require( + Bundle.main.object(forInfoDictionaryKey: "UTExportedTypeDeclarations") as? [[String: Any]]) + let type = try #require(exported.first { $0["UTTypeIdentifier"] as? String == Self.identifier }) + #expect(type["UTTypeConformsTo"] as? [String] == ["com.apple.package"]) + + let tags = try #require(type["UTTypeTagSpecification"] as? [String: Any]) + let request = WorkflowStarterTemplate.Request(name: "Demo", id: "demo", icon: nil, kind: .singleAgent) + let created = URL(filePath: request.fileName).pathExtension + #expect(tags["public.filename-extension"] as? [String] == [created]) + } + + /// Prowl has no handler for an opened workflow. With any role other than `None`, a double-click + /// in Finder brings Prowl forward and does nothing. + @Test func documentTypeDoesNotClaimToOpenWorkflows() throws { + let documents = try #require( + Bundle.main.object(forInfoDictionaryKey: "CFBundleDocumentTypes") as? [[String: Any]]) + let document = try #require( + documents.first { ($0["LSItemContentTypes"] as? [String])?.contains(Self.identifier) == true }) + #expect(document["CFBundleTypeRole"] as? String == "None") + } + + /// No icon file is bundled: Finder composes the icon from the app icon and this label. + @Test func iconIsSystemGeneratedWithAWorkflowLabel() throws { + let documents = try #require( + Bundle.main.object(forInfoDictionaryKey: "CFBundleDocumentTypes") as? [[String: Any]]) + let document = try #require( + documents.first { ($0["LSItemContentTypes"] as? [String])?.contains(Self.identifier) == true }) + #expect(document["CFBundleTypeIconSystemGenerated"] as? Bool == true) + + let exported = try #require( + Bundle.main.object(forInfoDictionaryKey: "UTExportedTypeDeclarations") as? [[String: Any]]) + let type = try #require(exported.first { $0["UTTypeIdentifier"] as? String == Self.identifier }) + let icons = try #require(type["UTTypeIcons"] as? [String: Any]) + #expect(icons["UTTypeIconText"] as? String == "Workflow") + } +}