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")
+ }
+}