Skip to content
Merged
1 change: 1 addition & 0 deletions docs/docs/Advanced/onePageInputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,4 @@ Behavior:
- Preflight may import user script modules to statically read `quickadd.inputs`. This can execute module top-level code.
- Inline scripts aren’t scanned for input declarations yet.
- If needed, you can still prompt ad-hoc (e.g., using inputPrompt or suggester) and those values will skip future one-page prompts due to being prefilled.
- Closing the modal without submitting triggers `MacroAbortError("Input cancelled by user")`, which stops the macro unless you catch it.
32 changes: 20 additions & 12 deletions docs/docs/QuickAddAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Opens a one-page modal to collect multiple inputs in one go. Values already pres
**Behavior:**
- Uses existing values for any ids that already exist in `variables` (including empty strings).
- Prompts only for missing (`undefined`/`null`) inputs.
- If the user closes the modal without submitting, the promise rejects with `MacroAbortError("Input cancelled by user")`.

**Field Types:**
- `text`: Single-line text input
Expand Down Expand Up @@ -115,18 +116,25 @@ Opens a prompt that asks for text input.
- `placeholder`: (Optional) Placeholder text in the input field
- `value`: (Optional) Default value

**Returns:** Promise resolving to the entered string, or `null` if cancelled
**Returns:** Promise resolving to the entered string.

**Cancellation:** If the user cancels or presses Escape, the promise rejects with `MacroAbortError("Input cancelled by user")`. Letting it bubble will stop the macro automatically. Catch it only if your script wants to handle the cancellation itself.

**Example:**
```javascript
const name = await quickAddApi.inputPrompt(
"What's your name?",
"Enter your full name",
"John Doe"
);

if (name) {
console.log(`Hello, ${name}!`);
try {
const name = await quickAddApi.inputPrompt(
"What's your name?",
"Enter your full name",
"John Doe"
);
console.log(`Hello, ${name}!`);
} catch (error) {
if (error?.name === "MacroAbortError") {
// Optional: perform cleanup before QuickAdd aborts the macro
return;
}
throw error;
}
```

Expand All @@ -135,7 +143,7 @@ Opens a wider prompt for longer text input (multi-line).

**Parameters:** Same as `inputPrompt`

**Returns:** Promise resolving to the entered string, or `null` if cancelled
**Returns:** Promise resolving to the entered string. Cancelling rejects with `MacroAbortError` (same as `inputPrompt`).

**Example:**
```javascript
Expand All @@ -153,7 +161,7 @@ Opens a confirmation dialog with Yes/No buttons.
- `header`: The dialog title
- `text`: (Optional) Additional explanation text

**Returns:** Promise resolving to `true` (Yes) or `false` (No)
**Returns:** Promise resolving to `true` (Yes) or `false` (No). If the user closes the dialog without answering, the promise rejects with `MacroAbortError`.

**Example:**
```javascript
Expand Down Expand Up @@ -196,7 +204,7 @@ Opens a selection prompt with searchable options. Can optionally allow custom in
- `allowCustomInput`: (Optional) When `true`, allows users to enter custom text not in `actualItems`. Defaults to `false`
- `options.renderItem`: (Optional) Custom renderer `(value, el) => void` to control how each suggestion row is drawn

**Returns:** Promise resolving to the selected value or custom input, or `null` if cancelled
**Returns:** Promise resolving to the selected value or custom input. Cancelling rejects with `MacroAbortError`.

**Examples:**

Expand Down
47 changes: 33 additions & 14 deletions docs/docs/UserScripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,36 +287,55 @@ module.exports = async (params) => {
**When to use `params.abort()`:**
- Input validation failures
- Missing required configuration
- User cancels a confirmation prompt
- You want to provide a custom message after catching a `MacroAbortError`
- Prerequisites not met (e.g., required plugin not installed)

Prompt cancellations already throw `MacroAbortError` and halt macros automatically, so only call `abort()` in those scenarios if you need to surface a custom message or you're stopping for a non-prompt reason.

**What happens when you call `abort()`:**
- Macro execution stops immediately
- A message is logged: "Macro execution aborted: [your message]"
- Remaining commands in the macro are skipped
- No error is thrown to the user

**QuickAdd API methods that can be cancelled:**
- `inputPrompt()` - Returns `undefined` if cancelled
- `wideInputPrompt()` - Returns `undefined` if cancelled
- `yesNoPrompt()` - Returns `undefined` if cancelled
- `suggester()` - Aborts macro if cancelled
- `checkboxPrompt()` - Returns `undefined` if cancelled
- `inputPrompt()`
- `wideInputPrompt()`
- `yesNoPrompt()`
- `suggester()`
- `checkboxPrompt()`

Each of these now rejects with `MacroAbortError("Input cancelled by user")` when the user presses Escape or closes the dialog. If you do nothing, the macro will automatically stop (matching user expectations). If you want to handle cancellation in your script, wrap the call in `try/catch` and intercept the error before it reaches the macro engine.

```javascript
try {
const name = await quickAddApi.inputPrompt("Your name:");
} catch (error) {
if (error?.name === "MacroAbortError") {
// Optional custom handling (e.g., cleanup) before the macro aborts
return;
}
throw error; // real errors should still bubble up
}
```

**Important:** When using the QuickAdd API, check for `undefined` to handle cancellations gracefully:
**Important:** Because cancellations now throw, you should only call `abort()` yourself when you want to provide a custom message or stop execution for a non-prompt reason.

Comment thread
chhoumann marked this conversation as resolved.
```javascript
module.exports = async (params) => {
const { quickAddApi, abort } = params;

const name = await quickAddApi.inputPrompt("Your name:");

// Handle cancellation
if (!name) {
abort("Name is required");
let name;
try {
name = await quickAddApi.inputPrompt("Your name:");
} catch (error) {
if (error?.name === "MacroAbortError") {
abort("Name is required");
return;
}
throw error;
}

// Safe to use name here
console.log(`Processing: ${name}`);
};
```
Expand Down Expand Up @@ -906,4 +925,4 @@ For complete working examples, see:
**API methods returning undefined:**
- Ensure you're using `await` with async methods
- Check that QuickAdd plugin is enabled
- Verify you're accessing the API correctly through `params.quickAddApi`
- Verify you're accessing the API correctly through `params.quickAddApi`
12 changes: 12 additions & 0 deletions src/IChoiceExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import type IChoice from "./types/choices/IChoice";
import type { MacroAbortError } from "./errors/MacroAbortError";

export interface IChoiceExecutor {
execute(choice: IChoice): Promise<void>;
variables: Map<string, unknown>;
/**
* Records that the most recent choice execution aborted so orchestrators can react.
* Engines that handle cancellations without throwing should call this immediately after
* {@link handleMacroAbort} returns true.
*/
signalAbort?(error: MacroAbortError): void;
/**
* Returns and clears any pending abort signal. Callers should invoke this right after
* awaiting {@link execute} to determine whether the child choice stopped early.
*/
consumeAbortSignal?(): MacroAbortError | null;
}
12 changes: 12 additions & 0 deletions src/choiceExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ import { isCancellationError } from "./utils/errorUtils";

export class ChoiceExecutor implements IChoiceExecutor {
public variables: Map<string, unknown> = new Map<string, unknown>();
private pendingAbort: MacroAbortError | null = null;

constructor(private app: App, private plugin: QuickAdd) {}

signalAbort(error: MacroAbortError) {
this.pendingAbort = error;
}

consumeAbortSignal(): MacroAbortError | null {
const abort = this.pendingAbort;
this.pendingAbort = null;
return abort ?? null;
}

async execute(choice: IChoice): Promise<void> {
this.pendingAbort = null;
// One-page preflight honoring per-choice override
const globalEnabled = settingsStore.getState().onePageInputEnabled;
const override = choice.onePageInput;
Expand Down
1 change: 1 addition & 0 deletions src/engine/CaptureChoiceEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
defaultReason: "Capture aborted",
})
) {
this.choiceExecutor.signalAbort?.(err as MacroAbortError);
return;
}
reportError(err, `Error running capture choice "${this.choice.name}"`);
Expand Down
Loading