[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C4EEMB4ZRCEP9EDB2X00TG":3,"skill-same:01M6C4EEMB4ZRCEP9EDB2X00TG":58},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":30,"linkOnly":31,"markdownContent":32,"markdownBody":33,"frontMatter":34,"refName":35,"commitSha":36,"skillDirectory":37,"forksCount":38,"repositoryHtmlUrl":39,"repositoryHomepageUrl":6,"ownerHtmlUrl":40,"ownerBlogUrl":41,"ownerDescription":42,"updatedAt":43,"install":44,"resources":57},"01M6C4EEMB4ZRCEP9EDB2X00TG","app-intent-driven-development",null,"WHEN designing or debugging App Intent-first iOS features for Siri, Shortcuts, widgets, Live Activities, or reuse of an existing intent in SwiftUI; NOT for actions with no system surface; provides AppEntity, metadata, prompting, and view-reuse patterns.","mintuz\u002Fskills","mintuz","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F447031?v=4","01M6BVWJYSVR4XQC9AQCEMWQRG","Adam Bulmer","01M6C4DCXKNYZB281YB3PZDJJG",35,"MIT","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fapp\u002Fskills\u002Fapp-intent-driven-development\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29],"development","app intent driven development","debugging","metadata","actions","ios","first","system","swiftui","view","live activities","2026-11-10 10:12:30",false,"---\nname: app-intent-driven-development\ndescription: WHEN designing or debugging App Intent-first iOS features for Siri, Shortcuts, widgets, Live Activities, or reuse of an existing intent in SwiftUI; NOT for actions with no system surface; provides AppEntity, metadata, prompting, and view-reuse patterns.\n---\n\n# App Intent-First Driven Development\n\nDesign features as App Intents first, then reuse those intents across Shortcuts, widgets, and SwiftUI views so automation and UI stay in lockstep.\n\n## Scope Check\n\nAn action earns an App Intent when it needs a system surface, e.g. Siri, Shortcuts, App Shortcuts, Spotlight, widgets, Controls, the Action button, Focus filters, Live Activities (an open set).\n\n- The UI framework does not decide: a UIKit app can adopt App Intents, and a SwiftUI app can hold actions that stay inside one screen. A refusal is a product decision about surfaces, not a limit of the UI framework.\n- Keep an action with no system surface in the shared domain service the screen already calls; do not add an intent for consistency alone (`principle-foundational-thinking`).\n- An unused intent still costs: wider discoverability in system surfaces, App Intents metadata and Shortcuts cache upkeep, and system-facing verification with no user benefit.\n- When a system surface becomes a requirement, extract the intent around the same, unchanged service.\n- A scope decision is a deliverable: record the missing surface, the cost the refusal avoids, and the condition that reverses the decision.\n\n## Core Ideas\n\n- **Entities first**: model the data users act on (events, categories, records) as `AppEntity` boundary types so intents, widgets, and the app use the same identifiers and display language.\n- **Intent-first feature**: build the App Intent + entity\u002Fquery path before UI; SwiftUI screens call the same action path instead of duplicating service code (`principle-reuse-before-building`).\n- **Single action, single intent**: keep intents focused; avoid mega-intents, which are hard to compose in Shortcuts.\n- **Flat entity graph**: keep entities as serializable snapshots (ids, primitive fields, display strings, icons); avoid nesting one `AppEntity` inside another.\n- **Context-scoped choices**: when an enum or selected run\u002Fprofile determines valid values, build those choices from that context; never let Shortcuts offer every historical record by accident.\n- **Predictable UI**: supply `DisplayRepresentation`, `typeDisplayRepresentation`, dialogs, and icons so Siri\u002FShortcuts can render useful cards without guessing.\n- **Fast queries**: `EntityQuery` must be quick and cancellable; keep expensive SwiftData or network work out of global suggestions.\n- **Reuse business logic**: intents call the same services your views use; do not fork logic inside the intent.\n- **Metadata is behaviour**: App Intents metadata and Shortcuts caches are part of the feature; validate them after changing parameter or entity shape.\n\n## Minimal Entity Blueprint\n\n```swift\nimport AppIntents\n\nstruct TaskEntity: AppEntity, Identifiable {\n    let id: UUID\n    let title: String\n    let isComplete: Bool\n\n    nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation {\n        \"Task\"\n    }\n\n    nonisolated static var defaultQuery: TaskQuery {\n        TaskQuery()\n    }\n\n    var displayRepresentation: DisplayRepresentation {\n        DisplayRepresentation(\n            title: title,\n            subtitle: isComplete ? \"Completed\" : \"Open\",\n            image: .init(systemName: isComplete ? \"checkmark.circle.fill\" : \"circle\")\n        )\n    }\n}\n\nstruct TaskQuery: EntityQuery {\n    @MainActor\n    func entities(for identifiers: [TaskEntity.ID]) async throws -> [TaskEntity] {\n        try await TaskStore.shared.fetch(ids: identifiers) \u002F\u002F fast path\n    }\n\n    @MainActor\n    func suggestedEntities() async throws -> [TaskEntity] {\n        try await TaskStore.shared.fetchRecent()\n    }\n}\n```\n\n## Entity Design Rules\n\n- Treat `AppEntity` as a Shortcuts\u002Fsystem boundary, not the real model object. When the app needs a SwiftData model, resolve the entity by stable `id` in the current `ModelContext` (`principle-hide-representations`).\n- Prefer primitive values, dates, ids, and display strings on entities; use an entity-typed property only when the system needs that relationship. To refer to another model, store `profileID` and `profileName`, not `profile: ProfileEntity`.\n- Start with plain `EntityQuery`. Add `EntityStringQuery` only when search is required, then verify generated metadata.\n- Keep `suggestedEntities()` conservative: for child values such as laps\u002Fresults, prefer active-run\u002Fcurrent-profile suggestions over global history.\n- Entity IDs need not be UUIDs. Use a `String` id when the selectable thing can be a not-yet-persisted domain candidate, such as a `\"final\"` lap.\n- Use an identifier stable across launches and never reused for a different record. Never derive it from an array index, row position, or display name: a saved shortcut stores the identifier and would resolve to whatever record later takes that place.\n- From `entities(for:)`, return only records that still exist; omit an identifier you cannot resolve and never substitute a nearby record. The system then treats the value as unavailable, and the intent can report the missing item or prompt again (`principle-parse-at-the-boundary`).\n- Keep entity display independent of app-only formatting state; Shortcuts may render it without your SwiftUI environment.\n\n## Intent Pattern\n\n```swift\nimport AppIntents\n\nstruct CompleteTaskIntent: AppIntent {\n    nonisolated static let title: LocalizedStringResource = \"Complete Task\"\n    nonisolated static let description = IntentDescription(\"Marks a task as done and returns the updated item.\")\n    nonisolated static var supportedModes: IntentModes { .foreground }\n\n    @Parameter(title: \"Task\", requestValueDialog: \"Which task should I complete?\")\n    var task: TaskEntity\n\n    init(task: TaskEntity) { self.task = task }\n    init() {}\n\n    @MainActor\n    func perform() async throws -> some IntentResult & ReturnsValue\u003CTaskEntity> & ProvidesDialog {\n        let updated = try await TaskStore.shared.complete(task.id)\n        return .result(value: updated, dialog: \"Completed \\(updated.title).\")\n    }\n\n    nonisolated static var parameterSummary: some ParameterSummary {\n        Summary(\"Complete \\(\\.$task)\")\n    }\n}\n```\n\n- **Parameters**: keep them few. Make a value the action cannot work without a required, non-optional parameter with a `requestValueDialog`; the system then asks for it before `perform()` runs.\n- **Missing values**: never substitute a guessed or remembered default for a value the user did not supply. An optional parameter is not requested automatically: call `try await $parameter.requestValue(...)` or `$parameter.requestDisambiguation(among:)` inside `perform()`, or throw `$parameter.needsValueError(...)`. Define the behaviour for an optional value that stays nil.\n- **Callers that already know a value**: a widget, Control, or view constructs the same intent with that value, so no prompt appears; do not add a second intent for the pre-filled path. A widget or Control cannot show a request-value prompt, so it must supply every value the action needs.\n- **Results**: return entities when the system should render the updated object, and only while the metadata graph stays simple and useful. When returned entities cause metadata complexity in a mutation intent, a dialog-only `.result()` is acceptable.\n- **Dialogs**: use `ProvidesDialog` for expected conflicts and success states; prefer a helpful dialog over throwing for normal user-facing conditions such as \"already running\".\n- **Isolation**: mark `perform()` or helpers `@MainActor` when touching SwiftData contexts, app settings, Live Activities, or UI-bound services. Mark static metadata `nonisolated` when Swift 6 isolation complains.\n\n## Conditional Prompts\n\nUse `requestDisambiguation` when a later choice is required only after another parameter has a specific value.\n\n```swift\n@Parameter(title: \"Resolution\")\nvar resolution: StopResolution\n\n@Parameter(title: \"Benchmark Lap\")\nvar benchmarkLap: BenchmarkLapEntity?\n\n@MainActor\nfunc perform() async throws -> some IntentResult & ProvidesDialog {\n    let run = try BenchmarkIntentStore.resolvedRun()\n\n    switch resolution {\n    case .saveAsBenchmark:\n        let lap = try await selectedBenchmarkLap(for: run)\n        try SaveDecisionModule.saveAsBenchmark(run: run, lapID: lap.id)\n        return .result(dialog: \"Saved benchmark.\")\n    case .saveRun:\n        try SaveDecisionModule.keepInHistory(run: run)\n        return .result(dialog: \"Saved run.\")\n    case .discardRun:\n        try SaveDecisionModule.discard(run: run)\n        return .result(dialog: \"Discarded run.\")\n    }\n}\n\n@MainActor\nprivate func selectedBenchmarkLap(for run: BenchmarkRun) async throws -> BenchmarkLapEntity {\n    let candidates = try SaveDecisionModule.snapshot(for: run)\n        .candidates\n        .map(BenchmarkLapEntity.init)\n\n    if let benchmarkLap,\n       let current = candidates.first(where: { $0.id == benchmarkLap.id }) {\n        return current\n    }\n\n    return try await $benchmarkLap.requestDisambiguation(\n        among: candidates,\n        dialog: \"Which lap should be saved as the benchmark?\"\n    )\n}\n```\n\n- Build disambiguation candidates after resolving the current run\u002Fprofile, not from a global entity query.\n- For a Shortcuts editor dropdown, use `DynamicOptionsProvider` with `@IntentParameterDependency` on the parameters that define the context. At runtime, rebuild the scoped candidates and accept a supplied entity only when its id is still in that set; otherwise disambiguate again.\n- Align the entity identifier with the domain candidate identifier so selected values map back to the save\u002Fresolve module without guessing.\n- When there are scoped candidates, use `requestDisambiguation`; `requestValue` on an optional parameter may not produce the desired editor or runtime behaviour.\n\n## Reusing Intents in SwiftUI\n\n- Prefer calling intents from UI so automation and in-app flows share one path.\n- Use `Button(intent:)` or `Toggle(isOn:intent:)` (available when importing both `AppIntents` and `SwiftUI`) for direct, fire-and-forget actions; the system runs the intent and owns its result handling.\n- That system-run button exposes no progress or errors to the view. When the row must show in-flight state, an error alert, or navigation, call the intent with `callAsFunction(donate:)` inside your own `Button`; do not reimplement the action against the service behind the intent's back.\n- Translate entity selections into view state by stable id; do not copy entity fields into a SwiftData model object.\n\n```swift\nimport AppIntents\nimport SwiftUI\n\nstruct EventRow: View {\n    let event: EventEntity\n\n    var body: some View {\n        HStack {\n            Text(event.name)\n            Spacer()\n            Button(intent: UndoLastEventOccuranceIntent(event: event)) {\n                Label(\"Undo\", systemImage: \"arrow.uturn.backward\")\n            }\n        }\n    }\n}\n```\n\n- `callAsFunction(donate:)` resolves parameters, runs `perform()`, and returns the result value, so a row that owns progress and errors keeps the single action path. Hold the `Task` in `@State`: ignore taps while it runs, cancel it when the row disappears, and publish no error after cancellation (`principle-narrow-state-lifetime`):\n\n```swift\nimport AppIntents\nimport SwiftUI\n\nstruct EventRow: View {\n    @State private var undoTask: Task\u003CVoid, Never>?\n    @State private var error: Error?\n\n    let event: EventEntity\n\n    var body: some View {\n        HStack {\n            Text(event.name)\n            Spacer()\n            Button {\n                guard undoTask == nil else { return } \u002F\u002F one undo in flight\n                undoTask = Task {\n                    defer { undoTask = nil }\n                    do {\n                        _ = try await UndoLastEventOccuranceIntent(event: event)()\n                    } catch {\n                        \u002F\u002F Cancellation is not a failure: show no alert for it.\n                        if !Task.isCancelled { self.error = error }\n                    }\n                }\n            } label: {\n                if undoTask != nil {\n                    ProgressView()\n                } else {\n                    Label(\"Undo\", systemImage: \"arrow.uturn.backward\")\n                }\n            }\n        }\n        .onDisappear { undoTask?.cancel() }\n        .alert(\"Undo failed\", isPresented: .init(\n            get: { error != nil },\n            set: { if !$0 { error = nil } }\n        )) {\n            Button(\"OK\", role: .cancel) { error = nil }\n        } message: {\n            Text(error?.localizedDescription ?? \"Unknown error\")\n        }\n    }\n}\n```\n\n- Return the app's own SwiftUI view as the intent result snippet with `.result(value:view:)` (which conforms the result to `ShowsSnippetView`); do not rebuild a second copy of the layout for Siri and Shortcuts. Put that shared view in a target or module both the app and the intent build against.\n- A snippet renders outside the app's view hierarchy: pass resolved values into the shared view and remove `@Environment` app objects and `ModelContext` fetches from it.\n- Return the value alongside the snippet so a following Shortcuts action still receives it. A `.result(value:view:)` snippet is static; an interactive snippet needs a `SnippetIntent` on iOS 26 or later.\n- Keep the intent signature identical between Shortcuts and SwiftUI usage.\n- Route service calls through the intent, not the view, to keep analytics, validation, and side effects consistent.\n- To navigate to a live SwiftData model after an intent returns, resolve the returned entity `id` in the view's `ModelContext`. If opaque `perform()` result typing makes that awkward, choose explicitly between `Button(intent:)` and `callAsFunction(donate:)`; do not create a second hidden implementation path.\n\n## Shortcuts Provider\n\nExpose user-facing actions with `AppShortcutsProvider`.\n\n```swift\nimport AppIntents\n\nstruct TimelineShortcuts: AppShortcutsProvider {\n    @AppShortcutsBuilder\n    nonisolated static var appShortcuts: [AppShortcut] {\n        AppShortcut(\n            intent: CompleteTaskIntent(),\n            phrases: [\"Complete task in \\(.applicationName)\"],\n            shortTitle: \"Complete Task\",\n            systemImageName: \"checkmark.circle.fill\"\n        )\n    }\n\n    nonisolated static var shortcutTileColor: ShortcutTileColor {\n        .blue\n    }\n}\n```\n\n- When shortcuts depend on dynamic app data, call `TimelineShortcuts.updateAppShortcutParameters()` at app launch and again from the code path that adds, renames, or deletes that data; otherwise the phrase offers old values until the next launch.\n- Declare `supportedModes` for the execution the action needs:\n  - `.background`: the action finishes without app UI.\n  - `.foreground`: bring the app forward before the action runs.\n  - `.foreground(.dynamic)`: start in the background and possibly continue in the foreground. The intent stays in the background until it calls `continueInForeground(_:alwaysConfirm:)` or throws `needsToContinueInForegroundError(_:alwaysConfirm:)`.\n- Adopt `LiveActivityIntent` for an action that starts or updates a Live Activity, so it runs in the app process without opening the app.\n- `openAppWhenRun` is deprecated from iOS 26; keep it only for deployment targets before iOS 26.\n- After changing an intent parameter type or entity graph, tell the user to delete and re-add existing Shortcuts actions, which can hold stale serialized entity tokens.\n\n## Metadata And Cache Checks\n\nCompilation is not enough; verify the system-facing metadata (`principle-prove-it-works`).\n\n- Run an `xcodebuild build` for the app target and confirm App Intents metadata extraction succeeds.\n- When Shortcuts says an entity is missing, inspect generated or installed `Metadata.appintents`\u002F`extract.actionsdata`: the entity should appear under `entities`, and the parameter should point at that entity type.\n- Install and launch the rebuilt app on the target simulator\u002Fdevice before testing in Shortcuts, then terminate\u002Freopen Shortcuts if it holds stale metadata.\n- If simulator Debug builds compile but Shortcuts cannot discover the provider, check build settings such as `ENABLE_DEBUG_DYLIB = NO`.\n- Treat Shortcuts cache issues as real debugging state: after changing parameter shapes, recreate the action in Shortcuts before judging the code path.\n\n## Development Flow\n\n1. Inventory the user actions in SwiftUI views and decide which ones should become system actions.\n2. Model action inputs\u002Foutputs as flat `AppEntity` snapshots with display representations and scoped queries.\n3. Implement focused `AppIntent`s that call shared domain services or modules; keep expected conflicts in dialogs.\n4. Add context-aware prompting with `requestDisambiguation` for conditional choices.\n5. Expose shortcuts through `AppShortcutsProvider` and refresh dynamic parameters at launch.\n6. Reuse the same action path in SwiftUI via `Button(intent:)` or `callAsFunction(donate:)`.\n7. Validate build, App Intents metadata, installed simulator\u002Fdevice metadata, and Shortcuts cache behaviour; cover query and perform paths with tests or builds.\n8. Localize strings early (`LocalizedStringResource`) to keep Siri responses natural in all supported languages.\n","# App Intent-First Driven Development\n\nDesign features as App Intents first, then reuse those intents across Shortcuts, widgets, and SwiftUI views so automation and UI stay in lockstep.\n\n## Scope Check\n\nAn action earns an App Intent when it needs a system surface, e.g. Siri, Shortcuts, App Shortcuts, Spotlight, widgets, Controls, the Action button, Focus filters, Live Activities (an open set).\n\n- The UI framework does not decide: a UIKit app can adopt App Intents, and a SwiftUI app can hold actions that stay inside one screen. A refusal is a product decision about surfaces, not a limit of the UI framework.\n- Keep an action with no system surface in the shared domain service the screen already calls; do not add an intent for consistency alone (`principle-foundational-thinking`).\n- An unused intent still costs: wider discoverability in system surfaces, App Intents metadata and Shortcuts cache upkeep, and system-facing verification with no user benefit.\n- When a system surface becomes a requirement, extract the intent around the same, unchanged service.\n- A scope decision is a deliverable: record the missing surface, the cost the refusal avoids, and the condition that reverses the decision.\n\n## Core Ideas\n\n- **Entities first**: model the data users act on (events, categories, records) as `AppEntity` boundary types so intents, widgets, and the app use the same identifiers and display language.\n- **Intent-first feature**: build the App Intent + entity\u002Fquery path before UI; SwiftUI screens call the same action path instead of duplicating service code (`principle-reuse-before-building`).\n- **Single action, single intent**: keep intents focused; avoid mega-intents, which are hard to compose in Shortcuts.\n- **Flat entity graph**: keep entities as serializable snapshots (ids, primitive fields, display strings, icons); avoid nesting one `AppEntity` inside another.\n- **Context-scoped choices**: when an enum or selected run\u002Fprofile determines valid values, build those choices from that context; never let Shortcuts offer every historical record by accident.\n- **Predictable UI**: supply `DisplayRepresentation`, `typeDisplayRepresentation`, dialogs, and icons so Siri\u002FShortcuts can render useful cards without guessing.\n- **Fast queries**: `EntityQuery` must be quick and cancellable; keep expensive SwiftData or network work out of global suggestions.\n- **Reuse business logic**: intents call the same services your views use; do not fork logic inside the intent.\n- **Metadata is behaviour**: App Intents metadata and Shortcuts caches are part of the feature; validate them after changing parameter or entity shape.\n\n## Minimal Entity Blueprint\n\n```swift\nimport AppIntents\n\nstruct TaskEntity: AppEntity, Identifiable {\n    let id: UUID\n    let title: String\n    let isComplete: Bool\n\n    nonisolated static var typeDisplayRepresentation: TypeDisplayRepresentation {\n        \"Task\"\n    }\n\n    nonisolated static var defaultQuery: TaskQuery {\n        TaskQuery()\n    }\n\n    var displayRepresentation: DisplayRepresentation {\n        DisplayRepresentation(\n            title: title,\n            subtitle: isComplete ? \"Completed\" : \"Open\",\n            image: .init(systemName: isComplete ? \"checkmark.circle.fill\" : \"circle\")\n        )\n    }\n}\n\nstruct TaskQuery: EntityQuery {\n    @MainActor\n    func entities(for identifiers: [TaskEntity.ID]) async throws -> [TaskEntity] {\n        try await TaskStore.shared.fetch(ids: identifiers) \u002F\u002F fast path\n    }\n\n    @MainActor\n    func suggestedEntities() async throws -> [TaskEntity] {\n        try await TaskStore.shared.fetchRecent()\n    }\n}\n```\n\n## Entity Design Rules\n\n- Treat `AppEntity` as a Shortcuts\u002Fsystem boundary, not the real model object. When the app needs a SwiftData model, resolve the entity by stable `id` in the current `ModelContext` (`principle-hide-representations`).\n- Prefer primitive values, dates, ids, and display strings on entities; use an entity-typed property only when the system needs that relationship. To refer to another model, store `profileID` and `profileName`, not `profile: ProfileEntity`.\n- Start with plain `EntityQuery`. Add `EntityStringQuery` only when search is required, then verify generated metadata.\n- Keep `suggestedEntities()` conservative: for child values such as laps\u002Fresults, prefer active-run\u002Fcurrent-profile suggestions over global history.\n- Entity IDs need not be UUIDs. Use a `String` id when the selectable thing can be a not-yet-persisted domain candidate, such as a `\"final\"` lap.\n- Use an identifier stable across launches and never reused for a different record. Never derive it from an array index, row position, or display name: a saved shortcut stores the identifier and would resolve to whatever record later takes that place.\n- From `entities(for:)`, return only records that still exist; omit an identifier you cannot resolve and never substitute a nearby record. The system then treats the value as unavailable, and the intent can report the missing item or prompt again (`principle-parse-at-the-boundary`).\n- Keep entity display independent of app-only formatting state; Shortcuts may render it without your SwiftUI environment.\n\n## Intent Pattern\n\n```swift\nimport AppIntents\n\nstruct CompleteTaskIntent: AppIntent {\n    nonisolated static let title: LocalizedStringResource = \"Complete Task\"\n    nonisolated static let description = IntentDescription(\"Marks a task as done and returns the updated item.\")\n    nonisolated static var supportedModes: IntentModes { .foreground }\n\n    @Parameter(title: \"Task\", requestValueDialog: \"Which task should I complete?\")\n    var task: TaskEntity\n\n    init(task: TaskEntity) { self.task = task }\n    init() {}\n\n    @MainActor\n    func perform() async throws -> some IntentResult & ReturnsValue\u003CTaskEntity> & ProvidesDialog {\n        let updated = try await TaskStore.shared.complete(task.id)\n        return .result(value: updated, dialog: \"Completed \\(updated.title).\")\n    }\n\n    nonisolated static var parameterSummary: some ParameterSummary {\n        Summary(\"Complete \\(\\.$task)\")\n    }\n}\n```\n\n- **Parameters**: keep them few. Make a value the action cannot work without a required, non-optional parameter with a `requestValueDialog`; the system then asks for it before `perform()` runs.\n- **Missing values**: never substitute a guessed or remembered default for a value the user did not supply. An optional parameter is not requested automatically: call `try await $parameter.requestValue(...)` or `$parameter.requestDisambiguation(among:)` inside `perform()`, or throw `$parameter.needsValueError(...)`. Define the behaviour for an optional value that stays nil.\n- **Callers that already know a value**: a widget, Control, or view constructs the same intent with that value, so no prompt appears; do not add a second intent for the pre-filled path. A widget or Control cannot show a request-value prompt, so it must supply every value the action needs.\n- **Results**: return entities when the system should render the updated object, and only while the metadata graph stays simple and useful. When returned entities cause metadata complexity in a mutation intent, a dialog-only `.result()` is acceptable.\n- **Dialogs**: use `ProvidesDialog` for expected conflicts and success states; prefer a helpful dialog over throwing for normal user-facing conditions such as \"already running\".\n- **Isolation**: mark `perform()` or helpers `@MainActor` when touching SwiftData contexts, app settings, Live Activities, or UI-bound services. Mark static metadata `nonisolated` when Swift 6 isolation complains.\n\n## Conditional Prompts\n\nUse `requestDisambiguation` when a later choice is required only after another parameter has a specific value.\n\n```swift\n@Parameter(title: \"Resolution\")\nvar resolution: StopResolution\n\n@Parameter(title: \"Benchmark Lap\")\nvar benchmarkLap: BenchmarkLapEntity?\n\n@MainActor\nfunc perform() async throws -> some IntentResult & ProvidesDialog {\n    let run = try BenchmarkIntentStore.resolvedRun()\n\n    switch resolution {\n    case .saveAsBenchmark:\n        let lap = try await selectedBenchmarkLap(for: run)\n        try SaveDecisionModule.saveAsBenchmark(run: run, lapID: lap.id)\n        return .result(dialog: \"Saved benchmark.\")\n    case .saveRun:\n        try SaveDecisionModule.keepInHistory(run: run)\n        return .result(dialog: \"Saved run.\")\n    case .discardRun:\n        try SaveDecisionModule.discard(run: run)\n        return .result(dialog: \"Discarded run.\")\n    }\n}\n\n@MainActor\nprivate func selectedBenchmarkLap(for run: BenchmarkRun) async throws -> BenchmarkLapEntity {\n    let candidates = try SaveDecisionModule.snapshot(for: run)\n        .candidates\n        .map(BenchmarkLapEntity.init)\n\n    if let benchmarkLap,\n       let current = candidates.first(where: { $0.id == benchmarkLap.id }) {\n        return current\n    }\n\n    return try await $benchmarkLap.requestDisambiguation(\n        among: candidates,\n        dialog: \"Which lap should be saved as the benchmark?\"\n    )\n}\n```\n\n- Build disambiguation candidates after resolving the current run\u002Fprofile, not from a global entity query.\n- For a Shortcuts editor dropdown, use `DynamicOptionsProvider` with `@IntentParameterDependency` on the parameters that define the context. At runtime, rebuild the scoped candidates and accept a supplied entity only when its id is still in that set; otherwise disambiguate again.\n- Align the entity identifier with the domain candidate identifier so selected values map back to the save\u002Fresolve module without guessing.\n- When there are scoped candidates, use `requestDisambiguation`; `requestValue` on an optional parameter may not produce the desired editor or runtime behaviour.\n\n## Reusing Intents in SwiftUI\n\n- Prefer calling intents from UI so automation and in-app flows share one path.\n- Use `Button(intent:)` or `Toggle(isOn:intent:)` (available when importing both `AppIntents` and `SwiftUI`) for direct, fire-and-forget actions; the system runs the intent and owns its result handling.\n- That system-run button exposes no progress or errors to the view. When the row must show in-flight state, an error alert, or navigation, call the intent with `callAsFunction(donate:)` inside your own `Button`; do not reimplement the action against the service behind the intent's back.\n- Translate entity selections into view state by stable id; do not copy entity fields into a SwiftData model object.\n\n```swift\nimport AppIntents\nimport SwiftUI\n\nstruct EventRow: View {\n    let event: EventEntity\n\n    var body: some View {\n        HStack {\n            Text(event.name)\n            Spacer()\n            Button(intent: UndoLastEventOccuranceIntent(event: event)) {\n                Label(\"Undo\", systemImage: \"arrow.uturn.backward\")\n            }\n        }\n    }\n}\n```\n\n- `callAsFunction(donate:)` resolves parameters, runs `perform()`, and returns the result value, so a row that owns progress and errors keeps the single action path. Hold the `Task` in `@State`: ignore taps while it runs, cancel it when the row disappears, and publish no error after cancellation (`principle-narrow-state-lifetime`):\n\n```swift\nimport AppIntents\nimport SwiftUI\n\nstruct EventRow: View {\n    @State private var undoTask: Task\u003CVoid, Never>?\n    @State private var error: Error?\n\n    let event: EventEntity\n\n    var body: some View {\n        HStack {\n            Text(event.name)\n            Spacer()\n            Button {\n                guard undoTask == nil else { return } \u002F\u002F one undo in flight\n                undoTask = Task {\n                    defer { undoTask = nil }\n                    do {\n                        _ = try await UndoLastEventOccuranceIntent(event: event)()\n                    } catch {\n                        \u002F\u002F Cancellation is not a failure: show no alert for it.\n                        if !Task.isCancelled { self.error = error }\n                    }\n                }\n            } label: {\n                if undoTask != nil {\n                    ProgressView()\n                } else {\n                    Label(\"Undo\", systemImage: \"arrow.uturn.backward\")\n                }\n            }\n        }\n        .onDisappear { undoTask?.cancel() }\n        .alert(\"Undo failed\", isPresented: .init(\n            get: { error != nil },\n            set: { if !$0 { error = nil } }\n        )) {\n            Button(\"OK\", role: .cancel) { error = nil }\n        } message: {\n            Text(error?.localizedDescription ?? \"Unknown error\")\n        }\n    }\n}\n```\n\n- Return the app's own SwiftUI view as the intent result snippet with `.result(value:view:)` (which conforms the result to `ShowsSnippetView`); do not rebuild a second copy of the layout for Siri and Shortcuts. Put that shared view in a target or module both the app and the intent build against.\n- A snippet renders outside the app's view hierarchy: pass resolved values into the shared view and remove `@Environment` app objects and `ModelContext` fetches from it.\n- Return the value alongside the snippet so a following Shortcuts action still receives it. A `.result(value:view:)` snippet is static; an interactive snippet needs a `SnippetIntent` on iOS 26 or later.\n- Keep the intent signature identical between Shortcuts and SwiftUI usage.\n- Route service calls through the intent, not the view, to keep analytics, validation, and side effects consistent.\n- To navigate to a live SwiftData model after an intent returns, resolve the returned entity `id` in the view's `ModelContext`. If opaque `perform()` result typing makes that awkward, choose explicitly between `Button(intent:)` and `callAsFunction(donate:)`; do not create a second hidden implementation path.\n\n## Shortcuts Provider\n\nExpose user-facing actions with `AppShortcutsProvider`.\n\n```swift\nimport AppIntents\n\nstruct TimelineShortcuts: AppShortcutsProvider {\n    @AppShortcutsBuilder\n    nonisolated static var appShortcuts: [AppShortcut] {\n        AppShortcut(\n            intent: CompleteTaskIntent(),\n            phrases: [\"Complete task in \\(.applicationName)\"],\n            shortTitle: \"Complete Task\",\n            systemImageName: \"checkmark.circle.fill\"\n        )\n    }\n\n    nonisolated static var shortcutTileColor: ShortcutTileColor {\n        .blue\n    }\n}\n```\n\n- When shortcuts depend on dynamic app data, call `TimelineShortcuts.updateAppShortcutParameters()` at app launch and again from the code path that adds, renames, or deletes that data; otherwise the phrase offers old values until the next launch.\n- Declare `supportedModes` for the execution the action needs:\n  - `.background`: the action finishes without app UI.\n  - `.foreground`: bring the app forward before the action runs.\n  - `.foreground(.dynamic)`: start in the background and possibly continue in the foreground. The intent stays in the background until it calls `continueInForeground(_:alwaysConfirm:)` or throws `needsToContinueInForegroundError(_:alwaysConfirm:)`.\n- Adopt `LiveActivityIntent` for an action that starts or updates a Live Activity, so it runs in the app process without opening the app.\n- `openAppWhenRun` is deprecated from iOS 26; keep it only for deployment targets before iOS 26.\n- After changing an intent parameter type or entity graph, tell the user to delete and re-add existing Shortcuts actions, which can hold stale serialized entity tokens.\n\n## Metadata And Cache Checks\n\nCompilation is not enough; verify the system-facing metadata (`principle-prove-it-works`).\n\n- Run an `xcodebuild build` for the app target and confirm App Intents metadata extraction succeeds.\n- When Shortcuts says an entity is missing, inspect generated or installed `Metadata.appintents`\u002F`extract.actionsdata`: the entity should appear under `entities`, and the parameter should point at that entity type.\n- Install and launch the rebuilt app on the target simulator\u002Fdevice before testing in Shortcuts, then terminate\u002Freopen Shortcuts if it holds stale metadata.\n- If simulator Debug builds compile but Shortcuts cannot discover the provider, check build settings such as `ENABLE_DEBUG_DYLIB = NO`.\n- Treat Shortcuts cache issues as real debugging state: after changing parameter shapes, recreate the action in Shortcuts before judging the code path.\n\n## Development Flow\n\n1. Inventory the user actions in SwiftUI views and decide which ones should become system actions.\n2. Model action inputs\u002Foutputs as flat `AppEntity` snapshots with display representations and scoped queries.\n3. Implement focused `AppIntent`s that call shared domain services or modules; keep expected conflicts in dialogs.\n4. Add context-aware prompting with `requestDisambiguation` for conditional choices.\n5. Expose shortcuts through `AppShortcutsProvider` and refresh dynamic parameters at launch.\n6. Reuse the same action path in SwiftUI via `Button(intent:)` or `callAsFunction(donate:)`.\n7. Validate build, App Intents metadata, installed simulator\u002Fdevice metadata, and Shortcuts cache behaviour; cover query and perform paths with tests or builds.\n8. Localize strings early (`LocalizedStringResource`) to keep Siri responses natural in all supported languages.\n",{"name":5,"description":7},"main","4e0a12b5048eac78a3ace4feee657873cdd530ef","src\u002Fapp\u002Fskills\u002Fapp-intent-driven-development",7,"https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills","https:\u002F\u002Fgithub.com\u002Fmintuz","http:\u002F\u002Fwww.mintuz.com","Working at @monzo, Previously worked at Moonpig, @booking and @bbc","2026-11-13 00:57:20",{"source":8,"skillName":5,"commands":45,"prompt":55,"zipAvailable":56},[46,49,52],{"tool":47,"command":48},"npx","npx skills add mintuz\u002Fskills --skill app-intent-driven-development",{"tool":50,"command":51},"bunx","bunx skills add mintuz\u002Fskills --skill app-intent-driven-development",{"tool":53,"command":54},"pnpm","pnpm dlx skills add mintuz\u002Fskills --skill app-intent-driven-development","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fapp\u002Fskills\u002Fapp-intent-driven-development\u002FSKILL.md",true,[],[59,82,105,118,139,156],{"skillNo":60,"name":61,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":73,"publishedAt":30},"01M6C4PDGB6ZZWBS35N3DF7PZZ","xstate","WHEN React state changes in response to an external answer — async submit, in-flight or loading flags, retries, cancellation, polling, optimistic updates, stale responses — or to an interaction with an interruptible middle, such as a drag or a gesture; or WHEN building, reviewing, testing, or visualising XState 5 actors and @xstate\u002Freact integrations; NOT for a value one event sets completely, such as a text draft, a focus flag, or a plain open and closed toggle; models typed lifecycles and renders legal Mermaid stateDiagram-v2 charts.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Fxstate\u002FSKILL.md",[65,70],{"categoryNo":66,"categoryName":67,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":71,"categoryName":72,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000009","前端开发",[61,74,75,76,77,78,79,80,81],"react","text","state","integrations","Charts","mermaid","testing","open",{"skillNo":83,"name":84,"nameZh":6,"description":85,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":96,"publishedAt":30},"01M6C4PB6VVC2X19B63B7YNH0T","web-design","WHEN refining UI layout, typography, color, or polish; NOT code implementation; provides concise principles for intentional, legible design.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Fweb-design\u002FSKILL.md",[88,93],{"categoryNo":89,"categoryName":90,"taxonomyType":91,"taxonomyTypeDesc":92},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",{"categoryNo":94,"categoryName":95,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000015","视觉设计",[97,98,99,100,101,102,103,104],"web","design","web design","code implementation","polish","ui","typography","code",{"skillNo":106,"name":107,"nameZh":6,"description":108,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":109,"categories":110,"keywords":112,"publishedAt":30},"01M6C4P8D92GJDA49V7X1CZXEV","tailwind","USE WHEN writing, fixing or reviewing styling in a project that uses Tailwind CSS, even a one-class change or a request that does not name Tailwind: colours, focus rings, spacing, class names chosen by a prop, map or CVA variants, theme tokens and dark mode in the Tailwind stylesheet or tailwind.config, setup or upgrades (if unsure whether the project uses Tailwind, check package.json or the main stylesheet first); NOT WHEN writing plain CSS or Sass in a project without Tailwind; produces class names Tailwind can find, syntax for the installed major version, visible focus indicators, and no unrequested dependency or invented brand value.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Ftailwind\u002FSKILL.md",[111],{"categoryNo":94,"categoryName":95,"taxonomyType":68,"taxonomyTypeDesc":69},[113,114,115,116,98,117],"Tailwind","libraries","component","css","design systems",{"skillNo":119,"name":120,"nameZh":6,"description":121,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":127,"publishedAt":30},"01M6C4P5VWQ69Q32DA0634GDJY","refactoring","WHEN you need a plan to refactor, restructure, or clean up working code and the test result is known, either green or a recorded failing set; NOT for new feature delivery or any behavior change; reversible-baseline safety with prioritized improvements.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Frefactoring\u002FSKILL.md",[124],{"categoryNo":125,"categoryName":126,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000007","代码审查",[120,128,129,130,131,132,133,134,135,136,104,137,138],"Safety","baseline","refactor","behavior","change","plan","new feature","SET","up","feature delivery","test",{"skillNo":140,"name":74,"nameZh":6,"description":141,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":142,"categories":143,"keywords":146,"publishedAt":30},"01M6C4P345APEFEX00J8T7QMXQ","USE WHEN changing the logic, data, props, state, Effects or rendered text of a React component, hook or page, even a one-line change; NOT WHEN the request is styling only (class names, Tailwind, CSS, dark mode, spacing, shadows) or tests only, or when it models submit, in-flight, loading, retry or failure states, even for a named React form (xstate covers that, including its React wiring); applies React 19 composition, state, Effect, structure, API and performance rules with only the project's installed libraries.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Freact\u002FSKILL.md",[144,145],{"categoryNo":66,"categoryName":67,"taxonomyType":68,"taxonomyTypeDesc":69},{"categoryNo":71,"categoryName":72,"taxonomyType":68,"taxonomyTypeDesc":69},[74,104,116,113,147,80,148,149,150,151,152,76,138,153,154,155],"performance","hooks","components","architecture","api","react components","state management","styling","management",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":164,"publishedAt":30},"01M6C4P0DK385QBG3B3S86TQV2","react-testing","USE WHEN writing, fixing or reviewing a React Testing Library test for a component, hook or context provider, even one small test, or when asked for an end-to-end journey inside a Vitest or React Testing Library suite; NOT WHEN changing the component itself (react), testing XState actors or @xstate\u002Freact (xstate), or writing a Playwright or Cypress test; covers renderHook, providers, async results, forms and false-pass anti-patterns, and declines end-to-end journeys with the testable slices.","https:\u002F\u002Fgithub.com\u002Fmintuz\u002Fskills\u002Fblob\u002Fmain\u002Fsrc\u002Fweb\u002Fskills\u002Freact-testing\u002FSKILL.md",[162,163],{"categoryNo":66,"categoryName":67,"taxonomyType":68,"taxonomyTypeDesc":69},{"categoryNo":71,"categoryName":72,"taxonomyType":68,"taxonomyTypeDesc":69},[165,80,74,166,148,167,168,169,149,170,171,152],"react testing","anti-patterns","e2e","library","testing library","Providers","context"]