Skip to content

feat(uri): support x-callback-url for the QuickAdd URI (#1070)#1339

Merged
chhoumann merged 2 commits into
masterfrom
chhoumann/1070-uri-x-callback-url
Jun 13, 2026
Merged

feat(uri): support x-callback-url for the QuickAdd URI (#1070)#1339
chhoumann merged 2 commits into
masterfrom
chhoumann/1070-uri-x-callback-url

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Closes #1070.

Lets external callers (e.g. Apple Shortcuts) trigger a Template or Capture choice via obsidian://quickadd and receive a callback when it finishes — the x-callback-url convention.

What it does

  • New default-off setting Allow URI x-callback-url (enableUriCallbacks). With it off, or when no x-* params are present, the handler runs the exact legacy path — no behaviour change.
  • x-success / x-error / x-cancel (plus a legacy single-URL x-callback-url shorthand that fires on success and cancel).
  • Result payload: x-successstatus=success + the affected note's path and an obsidian://open url. x-errorstatus=error + a stable errorCode (choice-not-found, unsupported-choice-type, execution-failed, execution-aborted, bad-callback-url) — never the raw message, so vault internals don't leak. x-cancelstatus=cancel.
  • Scope: Template + Capture. Macro/Multi return x-error unsupported-choice-type (full propagation needs a larger refactor — tracked as follow-up).

How it works

  • New ChoiceExecutor.executeWithOutcome() returns a typed ChoiceOutcome; IChoiceExecutor.execute() is unchanged (no stub churn). Engines record success at the content-commit point (before cosmetic steps), so a later cosmetic failure can't downgrade the outcome and make a caller retry/duplicate.
  • New UserCancelError distinguishes a genuine user prompt-dismissal (x-cancel) from a script/config abort (x-error execution-aborted); handleMacroAbort classifies via instanceof with a message-string fallback for back-compat.
  • Editor-insertion helpers now report whether the insertion landed, so a capture with no active editor reports failure instead of a false success.
  • Security: all provided callback URLs are validated before anything runs (a bad URL can't half-execute), and restricted to an allow-list of shortcuts: and obsidian: schemes.
  • Pure URL logic in src/uri/uriCallback.ts (unit-tested).

Verification

  • tsc, eslint, 1934 vitest tests, production build, and docs build all pass.
  • Dev-vault e2e against the real plugin: success (+path/url), unsupported-choice-type, choice-not-found, bad-scheme → no execution, gate-off / no-callback → legacy, and caller-supplied status correctly overridden — all behave as designed.
  • Reviewed via an adversarial diff pass; the one substantive finding (a nested {{MACRO}} could leak its outcome into the outer choice) is fixed by scoping the result slot per execute() frame.

Docs

Full section added to docs/docs/Advanced/ObsidianUri.md, including the double-encoding requirement (Obsidian's URI parser truncates an under-encoded callback value — the issue's own example is affected).

Release / migration

  • feat: → minor release. New setting defaults off; Object.assign supplies it for existing users (no migration).
  • obsidian://quickadd behaviour is unchanged unless a user opts in.

Summary by CodeRabbit

  • New Features
    • Added opt-in Allow URI x-callback-url setting enabling obsidian://quickadd result callbacks for Template and Capture choices (success, error, cancel).
  • Documentation
    • Documented callback parameter behavior, required percent-encoding rules, supported callback URL schemes, and mobile notes.
  • Bug Fixes
    • Improved user-cancellation handling and outcome reporting so cancellation and insertion failures behave more predictably.
  • Tests
    • Updated and added coverage for cancellation notices and URI callback parsing/encoding.

Let external callers (e.g. Apple Shortcuts) trigger a Template or Capture choice
via obsidian://quickadd and receive a callback when it finishes, following the
x-callback-url convention (x-success / x-error / x-cancel, plus a legacy
single-URL x-callback-url shorthand).

- New default-off setting `enableUriCallbacks`; with it off, or when no x-*
  params are present, the URI handler runs the exact legacy path (no behaviour
  change).
- x-success carries `status=success` and, for Template/Capture, the affected
  note's vault `path` and an `obsidian://open` `url`. x-error carries a stable
  `errorCode` (no raw message, so vault internals never leak). x-cancel carries
  `status=cancel`.
- Outcome is surfaced via a new `ChoiceExecutor.executeWithOutcome()` returning a
  typed `ChoiceOutcome`; `IChoiceExecutor.execute()` is unchanged. Engines record
  success at the content-commit point so a later cosmetic failure can't downgrade
  the outcome (and make a caller retry / duplicate the capture).
- New `UserCancelError` distinguishes a genuine user prompt-dismissal (x-cancel)
  from a script/config abort (x-error execution-aborted); `handleMacroAbort`
  classifies via instanceof with a message-string fallback for back-compat.
- Editor-insertion helpers now report whether the insertion landed, so a capture
  with no active editor reports failure instead of a false success.
- Security: callbacks are validated before anything runs (so a bad URL can't
  half-execute) and restricted to an allow-list of `shortcuts:` and `obsidian:`
  schemes. Macro and Multi choices are not supported and return
  `unsupported-choice-type`.

Pure URL logic lives in src/uri/uriCallback.ts with unit tests. Docs added to
docs/docs/Advanced/ObsidianUri.md.

Closes #1070
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0db5291-74fa-4311-976d-5afb7cbeed18

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6a194 and 8673c76.

📒 Files selected for processing (1)
  • docs/docs/Advanced/ObsidianUri.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/docs/Advanced/ObsidianUri.md

📝 Walkthrough

Walkthrough

Adds opt-in x-callback-url support to obsidian://quickadd URI links. A new UserCancelError subclass distinguishes user dismissals from macro aborts; a ChoiceOutcome union type carries structured success/error/cancelled results from Template and Capture engines through ChoiceExecutor to the URI handler in main.ts, which validates callback URL schemes and fires x-success/x-error/x-cancel targets. A settings toggle and documentation are included.

Changes

x-callback-url URI Support

Layer / File(s) Summary
UserCancelError, ChoiceOutcome, and editor insertion booleans
src/errors/UserCancelError.ts, src/types/ChoiceOutcome.ts, src/utilityObsidian.ts
UserCancelError is introduced as a MacroAbortError subclass; ChoiceOutcome defines success/error/cancelled discriminants; editor insertion helpers now return booleans for downstream outcome detection.
UserCancelError propagation across formatters, engines, and abort handler
src/IChoiceExecutor.ts, src/formatters/completeFormatter.ts, src/ai/AIAssistant.ts, src/quickAddApi.ts, src/engine/MacroChoiceEngine.ts, src/engine/TemplateEngine.ts, src/utils/macroAbortHandler.ts
All isCancellationError branches across prompt/suggest/AI paths now throw UserCancelError instead of MacroAbortError; IChoiceExecutor gains the optional recordExecutionResult hook; macroAbortHandler uses instanceof UserCancelError as primary classifier.
ChoiceExecutor outcome recording and executeWithOutcome
src/choiceExecutor.ts
Adds pendingResult state and recordExecutionResult(); execute() snapshots/restores pendingResult to prevent nesting leaks; new executeWithOutcome() runs Template/Capture choices and maps error types to typed ChoiceOutcome values including swallowed-failure detection.
TemplateChoiceEngine and CaptureChoiceEngine outcome recording
src/engine/TemplateChoiceEngine.ts, src/engine/CaptureChoiceEngine.ts, src/engine/TemplateChoiceEngine.notice.test.ts, src/engine/CaptureChoiceEngine.notice.test.ts, src/engine/CaptureChoiceEngine.selection.test.ts, src/engine/MacroChoiceEngine.notice.test.ts
Both engines call recordExecutionResult({status:"success"}) immediately after file commit, before cosmetic steps; cancellation in file-exists/folder/tag prompts throws UserCancelError; editor insertion failures record an error result and return early; notice and selection tests updated accordingly.
uriCallback.ts pure helper module and tests
src/uri/uriCallback.ts, src/uri/uriCallback.test.ts
New runtime-free module: parseCallbackTargets (with legacy x-callback-url fallback), isCallbackUrlAllowed (scheme allow-list), buildCallbackUrl (percent-encoded params), buildObsidianOpenUrl; full Vitest coverage for parsing, security validation, encoding, and precedence rules.
enableUriCallbacks setting, UI toggle, and main.ts URI handler
src/settings.ts, src/quickAddSettingsTab.ts, src/main.ts
Adds enableUriCallbacks: false to settings and an "Allow URI x-callback-url" toggle in AI & Online; refactors the quickadd protocol handler to validate callback URLs, restrict to Template/Capture, call executeWithOutcome, and dispatch to fireUriSuccess/fireUriError/fireUriCancel via openUriCallback; legacy path preserved via runUriChoiceLegacy.
x-callback-url documentation
docs/docs/Advanced/ObsidianUri.md
Adds a full "Getting a result back" section covering the opt-in setting, supported choice types, callback parameter slots, allowed URL schemes, result query parameters per outcome, double-encoding requirements with examples, an end-to-end example, and a mobile/iOS note.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over ExternalApp,Obsidian: Initiating callback
    ExternalApp->>Obsidian: obsidian://quickadd?choice=Daily+Log&x-success=shortcuts%3A%2F%2F...
  end
  rect rgba(255, 228, 196, 0.5)
    Note over Obsidian,isCallbackUrlAllowed: Validation
    Obsidian->>parseCallbackTargets: resolve success/error/cancel targets
    Obsidian->>isCallbackUrlAllowed: validate each callback URL (allow: shortcuts:, obsidian:)
    isCallbackUrlAllowed-->>Obsidian: allowed / rejected
  end
  rect rgba(144, 238, 144, 0.5)
    Note over Obsidian,ChoiceExecutor: Execution
    Obsidian->>ChoiceExecutor: executeWithOutcome(TemplateChoice | CaptureChoice)
    ChoiceExecutor->>Engine: execute(choice)
    Engine->>ChoiceExecutor: recordExecutionResult({status:"success", file})
    ChoiceExecutor-->>Obsidian: ChoiceOutcome
  end
  rect rgba(255, 182, 193, 0.5)
    Note over Obsidian,ExternalApp: Callback dispatch
    alt status = success
      Obsidian->>ExternalApp: window.open(x-success?status=success&path=...&url=...)
    else status = error
      Obsidian->>ExternalApp: window.open(x-error?status=error&errorCode=...)
    else status = cancelled
      Obsidian->>ExternalApp: window.open(x-cancel?status=cancel)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chhoumann/quickadd#976: Modifies the same ChoiceExecutor, signalAbort/consumeAbortSignal, and engine cancellation paths that this PR extends with UserCancelError and ChoiceOutcome.

Suggested labels

released

Poem

🐰 Hop, hop, a callback arrives!
The rabbit sends x-success and thrives.
With shortcuts: URLs allowed,
No MacroAbortError cloud —
Just UserCancelError, clean and bright,
And Obsidian opens the right shortcut tonight! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding x-callback-url support for QuickAdd’s URI handler.
Linked Issues check ✅ Passed The PR implements the requested QuickAdd URI x-callback-url flow for Shortcuts, including callbacks and returned result data.
Out of Scope Changes check ✅ Passed The changes appear focused on URI callback support and supporting cancellation/outcome plumbing, with no unrelated additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/1070-uri-x-callback-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8673c76
Status: ✅  Deploy successful!
Preview URL: https://2b89b193.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1070-uri-x-callbac.quickadd.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main.ts (1)

35-44: ⚡ Quick win

Use type-only imports per coding guidelines.

Lines 35-36 (ITemplateChoice, ICaptureChoice) and line 43 (CallbackTargets) import types that are only used in type positions (line 254 cast and line 198/354/363/369 annotations). Per the coding guidelines, "Prefer type-only imports in TypeScript files."

♻️ Proposed refactor to use type-only imports
-import type ITemplateChoice from "./types/choices/ITemplateChoice";
-import type ICaptureChoice from "./types/choices/ICaptureChoice";
+import type { type ITemplateChoice } from "./types/choices/ITemplateChoice";
+import type { type ICaptureChoice } from "./types/choices/ICaptureChoice";
 import {
 	buildCallbackUrl,
 	buildObsidianOpenUrl,
 	callbackUrls,
 	isCallbackUrlAllowed,
 	parseCallbackTargets,
-	type CallbackTargets,
 } from "./uri/uriCallback";
+import type { CallbackTargets } from "./uri/uriCallback";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 35 - 44, The imports in src/main.ts should follow
the type-only import guideline: keep only runtime values in the normal import
list from "./uri/uriCallback", and move ITemplateChoice, ICaptureChoice, and
CallbackTargets to type-only imports because they are used only in
annotations/casts. Update the existing import declarations near the top of
main.ts so the type symbols are clearly separated from value imports, while
preserving the current runtime imports like buildCallbackUrl,
buildObsidianOpenUrl, callbackUrls, isCallbackUrlAllowed, and
parseCallbackTargets.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/docs/Advanced/ObsidianUri.md`:
- Around line 91-93: The fenced code blocks in the document are missing language
identifiers (info strings) which causes markdownlint MD040 violations. Add a
language tag to each code block fence - the URI examples shown in the diff and
the other occurrences mentioned in the comment should use `text` or another
appropriate language identifier (such as `uri`, `bash`, etc. depending on the
actual content of each block) immediately after the opening triple backticks to
satisfy the markdownlint rule and improve readability.

---

Nitpick comments:
In `@src/main.ts`:
- Around line 35-44: The imports in src/main.ts should follow the type-only
import guideline: keep only runtime values in the normal import list from
"./uri/uriCallback", and move ITemplateChoice, ICaptureChoice, and
CallbackTargets to type-only imports because they are used only in
annotations/casts. Update the existing import declarations near the top of
main.ts so the type symbols are clearly separated from value imports, while
preserving the current runtime imports like buildCallbackUrl,
buildObsidianOpenUrl, callbackUrls, isCallbackUrlAllowed, and
parseCallbackTargets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2896de3-7510-40a3-8ee1-f0d0a79969b0

📥 Commits

Reviewing files that changed from the base of the PR and between c2ee449 and 4e6a194.

📒 Files selected for processing (23)
  • docs/docs/Advanced/ObsidianUri.md
  • src/IChoiceExecutor.ts
  • src/ai/AIAssistant.ts
  • src/choiceExecutor.ts
  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/MacroChoiceEngine.notice.test.ts
  • src/engine/MacroChoiceEngine.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/engine/TemplateEngine.ts
  • src/errors/UserCancelError.ts
  • src/formatters/completeFormatter.ts
  • src/main.ts
  • src/quickAddApi.ts
  • src/quickAddSettingsTab.ts
  • src/settings.ts
  • src/types/ChoiceOutcome.ts
  • src/uri/uriCallback.test.ts
  • src/uri/uriCallback.ts
  • src/utilityObsidian.ts
  • src/utils/macroAbortHandler.ts

Comment thread docs/docs/Advanced/ObsidianUri.md Outdated
@chhoumann chhoumann merged commit f111d45 into master Jun 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE REQUEST] QuickAdd URI Support X-callback-url

1 participant