Skip to content

feat(api): pick, look up, run by id and install choices from other plugins - #2247

Merged
chhoumann merged 5 commits into
masterfrom
feat/choice-picker-api
Oct 18, 2026
Merged

chhoumann merged 5 commits into
masterfrom
feat/choice-picker-api

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Why

A plugin that runs a QuickAdd choice has to ask its user to type the choice's name, then run it by that name. A rename or a second choice with the same name breaks that without a word. PodNotes wants its users to pick a QuickAdd choice for timestamp captures from QuickAdd's own picker, keep the choice's id, and add a ready-made capture choice in one click. This adds the four API members that need.

What changed

  • pickChoice(options?) opens the QuickAdd: Run picker in a pick mode, through a new onPick option on ChoiceSuggester. It resolves { id, name, type, path } for the picked Template, Capture, or Macro and runs nothing. Typing searches nested choices whatever Search nested choices says.
  • getChoice(ref) looks a choice up by name or { id }, and executeChoice now also takes { id }. Both use one lookup, findChoiceWithPath, which main.ts now uses as well.
  • importPackage(json) imports a package with no modal through the CLI's import body, moved to importPackageUnattended, which now owns the trust gate too. It skips choice ids and files that already exist, and it refuses a package that needs review.

Scope

  • No docs change, because the docs deploy from master. The text to add at release is below.
  • Not fixed here: every import (modal, CLI, and API) stores the choice list it read before writing files, so a choice edit that lands during those writes is lost. Splitting the choice merge from the file writes is a separate change.

Tradeoffs

  • importPackage refuses a package that runs code instead of installing it unreviewed. Plugins are trusted code, but a plugin that passes on someone else's package would skip the review that every other import path enforces.
  • Existing files are skipped as well as existing ids, so the API never overwrites anything.

Blast Radius

The modal, the CLI, and the API share the changed import code. applyPackageImport now decodes every file before the first write, so a package with an undecodable file fails before it changes anything, where it used to write the files before it. The CLI keeps its flags, gate, and report, and its tests pass unchanged. URI, command, and CLI lookups by name or id keep the same first-match order.

Verification

  • pnpm run lint, pnpm run build, pnpm run test (505 files), and the docs build with scripts/check-links.py pass.
  • The new e2e spec tests/e2e/api-pick-choice.test.ts runs on the isolated runner. It calls pickChoice from eval, types, and presses Enter. The promise resolves the nested choice, no prompt opens, and the note stays empty. After a rename, executeChoice({ id }) runs the choice. The full e2e suite passes: 100 files and 468 tests, with 2 files and 24 tests skipped because they need Templater.
  • Each of three mutants fails a test: pick mode that also runs the choice, { id } looked up by name, and importPackage that overwrites an existing id.

Release impact: a feat, so the next minor. No settings or storage change. The name form of executeChoice behaves as before.

Pick mode with Search nested choices off, after typing "time":

QuickAdd's picker in pick mode, showing the nested "Log timestamp" choice under its "Podcasts" folder

Docs at release

docs/src/content/docs/docs/QuickAddAPI.md

In the module table at the top, replace the Choice execution row and add a Packages row after it:

| [Choice execution](#choice-execution) | Pick, look up, or run a choice, or apply a template to the current note |
| [Packages](#packages) | Install a ready-made package that your plugin ships |

In Choice Execution, replace the executeChoice heading, its Cancellation line, and its choiceName parameter line, and add one example line:

### `executeChoice(choice: string | { id: string }, variables?: {[key: string]: any}, options?: { date?: string | Date }): Promise<void>`
**Cancellation:** If the target choice is cancelled or aborted, for example because you pressed Escape at one of its prompts, the promise rejects with `MacroAbortError`, and an uncaught rejection stops your script. If no choice has that name or id, QuickAdd reports an error and the promise resolves.
- `choice`: The choice's name, or `{ id }` with the choice's id. A name runs the first choice with that name, in the order the settings list shows them. An id still works after the user renames the choice, so store the id if your code runs the same choice later. `pickChoice` and `getChoice` return it.
// Run a choice by the id you stored
await quickAddApi.executeChoice({ id: choiceId }, { value: "Main agenda content" });

Then add these two sections after the executeChoice section, before applyTemplateToActiveFile:

### `getChoice(choice: string | { id: string }): { id: string; name: string; type: "Template" | "Capture" | "Macro" | "Multi"; path: string[] } | null`
Looks up a choice without running it. Pass the choice's name or `{ id }`. A name returns the first choice with that name, in the order the settings list shows them. `path` lists the folder names from the top level down, with the choice's own name last. If no choice matches, `getChoice` returns `null` and shows no notice.

**Example:**
```javascript
const choice = quickAddApi.getChoice({ id: choiceId });
if (choice === null) {
    // The user deleted the choice.
}
```

### `pickChoice(options?: { placeholder?: string }): Promise<{ id: string; name: string; type: "Template" | "Capture" | "Macro"; path: string[] }>`
Opens the picker that **QuickAdd: Run** opens and resolves with the choice the user picks. The picked choice does not run. Folders open as they do in **QuickAdd: Run**, and only a Template, Capture, or Macro can be picked. Typing searches the choices inside folders too and shows each one's folder, whatever **Search nested choices** is set to. The result has the same shape as `getChoice` returns.

**Cancellation:** If the user closes the picker, the promise rejects with a cancellation. If there are no choices to pick, QuickAdd shows the same notice as **QuickAdd: Run**, opens nothing, and the promise rejects with a cancellation.

**Parameters:**
- `options.placeholder`: (Optional) The hint in the empty search field. The default is "Select a choice".

**Example:**
```javascript
try {
    const choice = await quickAddApi.pickChoice({ placeholder: "Choose a capture choice" });
    settings.choiceId = choice.id;
} catch {
    // The user closed the picker.
}
```

Add this section after Choice Execution, before Formatting:

## Packages

### `importPackage(packageJson: string): Promise<{ addedChoiceIds: string[]; skippedChoiceIds: string[]; writtenAssets: string[]; skippedAssets: string[] }>`
Imports a [package](/docs/Choices/Packages/) without the **Import package** review, so your plugin can install a ready-made workflow it ships. `packageJson` is the text of a `.quickadd.json` package. QuickAdd adds only what the vault doesn't have yet. It skips a choice whose id is already in the vault and a file that already exists, and never overwrites them, so a second import of the same package changes nothing. QuickAdd registers the commands of the imported choices right away, and the promise resolves after QuickAdd saves its settings.

If `packageJson` isn't a valid package, or the package runs code, the promise rejects and nothing changes. A package with scripts can only be imported from QuickAdd's settings, where the user reviews it.

**Example:**
```javascript
const { addedChoiceIds, skippedChoiceIds } = await quickAddApi.importPackage(packageJson);
const choice = quickAddApi.getChoice({ id: "my-plugin-capture" });
```

docs/src/content/docs/docs/Advanced/APIOverview.md

Add this row to Common tasks, after Run another choice from a script:

| Let the user pick a choice, or install a ready-made one | Choice execution, packages | [Choice Execution](/docs/QuickAddAPI/#choice-execution), [Packages](/docs/QuickAddAPI/#packages) |

Note

Medium Risk
Touches shared package import (modal/CLI/API), choice picker execution paths, and settings mutation; import semantics changed to fail before writes on decode errors.

Overview
Adds QuickAdd API support for other plugins to pick, look up, and run choices by stable id, and to install bundled packages without the settings modal.

Choice execution & lookup: executeChoice now accepts a name or { id } via shared findChoice / findChoiceWithPath (also used by main.ts lookup). New getChoice(ref) returns { id, name, type, path } without running. New pickChoice() opens the Run QuickAdd picker in pick mode (ChoiceSuggester.onPick): it returns a summary and does not execute; nested search is forced while picking.

Packages: New importPackage(json) skips existing choice ids and vault files, refuses packages that need code review, and delegates to importPackageUnattended—the same path the CLI now uses after removing duplicated import logic. applyPackageImport decodes all asset content before any write so a bad file aborts with no partial changes.

Tests cover API import/pick, id-based execution, CLI behavior, and an e2e pick-then-run-by-id flow.

Reviewed by Cursor Bugbot for commit 4804745. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add getChoice, pickChoice and importPackage to the QuickAdd API

Adds new public API methods so other plugins can work with choices without accessing settings. getChoice returns a summary by name or id, pickChoice opens the choice picker without executing the choice, and importPackage imports a package without user interaction.

  • Choice lookup is centralized in findChoiceWithPath in choiceUtils.ts; it resolves by name (first match) or stable id and returns the folder path. executeChoice in quickAddApi.ts now accepts a ChoiceRef and reports missing choices without throwing.
  • ChoiceSuggester in choiceSuggester.ts gains an onPick callback for pick mode; folder rows still navigate and nested choices stay searchable even when nested search is disabled.
  • Unattended package import is shared by API and CLI through importPackageUnattended in packageImportUnattended.ts; imports run serially, skip existing choice ids and asset paths, and reject packages that require code review.
  • Risk: applyPackageImport in packageImportService.ts now decodes all assets before any write, so a decode failure aborts with no partial import; QuickAdd.getChoiceByName is replaced by the shared traversal, so out-of-tree callers using that method must switch to the new API.

Macroscope summarized 4804745.

Summary by CodeRabbit

  • New Features
    • Added API methods to find, describe, pick, and execute choices by ID or name.
    • Added package importing through the API and CLI, with options to skip existing choices and assets.
    • Choice picking supports nested search and returns the selected choice’s folder path without executing it.
  • Bug Fixes
    • Package imports now validate asset content before writing, preventing partial writes when an asset cannot be decoded.
    • Imports requiring review are held for acknowledgement before changes are applied.

The CLI imported a package without the modal: it analysed it, decided
every choice and file, applied the import, stored the choices, synced
their commands and saved. That body moves to
importPackageUnattended, which takes the decisions as a function of
whether each choice or file already exists. The CLI keeps its flags,
its trust gate and its report, and imports stay queued.
…ugins

Another plugin that runs a QuickAdd choice had to ask its user to type
the choice's name, then run it by that name, which breaks when the
choice is renamed or shares its name. The API now lets it use
QuickAdd's own picker and keep the choice's id:

- pickChoice opens the picker QuickAdd: Run opens, in a pick mode that
  resolves the Template, Capture or Macro the user picks and runs
  nothing. Typing searches nested choices whatever Search nested
  choices says. Escape rejects with QuickAdd's usual cancellation.
- getChoice looks a choice up by name or { id } and returns its id,
  name, type and folder path, or null.
- executeChoice also takes { id }. A name still runs the first match.
- importPackage imports a package the plugin ships, with no review
  modal. A choice whose id is already in the vault, and a file that
  already exists, are skipped. It throws before changing anything for
  an invalid package or one that runs code.
applyPackageImport decoded each file inside its write loop, so a
package whose second file was not valid base64 wrote the first file and
its folders before it failed. The modal, the CLI and the API all apply
imports through it. It now decodes every file while it plans the
writes, so an undecodable file stops the import before anything
changes.
…place

- main.ts and the API now find a choice through findChoiceWithPath, so a
  name or id resolves the same way in the API, URIs, commands and the
  CLI. getChoice walks the tree once.
- importPackageUnattended runs the trust gate itself and returns
  needsReview instead of importing, so no caller can skip it. The CLI
  passes acknowledge=true through.
- It asks whether a file exists at the validated path it will write, so
  a padded path cannot overwrite a file that importPackage must skip.
- pickChoice reads the picked choice from the live tree, and
  executeChoice names a non-object ref as a name in its error.
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 76bfa515-2f6d-4b93-b4c7-db4c0538df61
📥 Commits

Reviewing files that changed from the base of the PR and between 040300d and 4804745.

📒 Files selected for processing: 18
  • src/api/choiceApi.ts
  • src/api/importPackage.test.ts
  • src/api/pickChoice.test.ts
  • src/choiceExecutor.dateScope.test.ts
  • src/choiceExecutor.reentry.test.ts
  • src/cli/importPackageCli.ts
  • src/engine/TemplateChoiceEngine.collision.test.ts
  • src/gui/suggesters/choiceSuggester.ts
  • src/main.ts
  • src/quickAddApi.audit-api-prompts.test.ts
  • src/quickAddApi.choiceLookup.test.ts
  • src/quickAddApi.executeChoice.test.ts
  • src/quickAddApi.test.ts
  • src/quickAddApi.ts
  • src/services/packageImportService.ts
  • src/services/packageImportUnattended.ts
  • src/utils/choiceUtils.ts
  • tests/e2e/api-pick-choice.test.ts
💤 Files with no reviewable changes: 1
  • src/choiceExecutor.reentry.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

The change adds API methods for choice lookup, selection, execution, and package import. It adds a shared unattended package import flow for the API and CLI, and updates tests for these behaviors.

Changes: Choice and package import APIs

Layer / File(s) Summary
Choice lookup and API contracts
src/utils/choiceUtils.ts, src/main.ts, src/quickAddApi.ts, src/api/choiceApi.ts, src/quickAddApi*.test.ts, src/choiceExecutor*.test.ts, src/engine/TemplateChoiceEngine.collision.test.ts
Choice references resolve by ID or first matching name. Lookup returns a summary with its path, and executeChoice resolves from configured choices. Tests now provide choices through settings.choices; the error-utils mock uses a type-only namespace import.
Choice picker flow
src/gui/suggesters/choiceSuggester.ts, src/api/choiceApi.ts, src/api/pickChoice.test.ts, tests/e2e/api-pick-choice.test.ts
Pick mode returns a selected choice without executing it. Nested search is available in pick mode, and picker callbacks continue through nested levels. Tests cover selection, navigation, cancellation, and API behavior after a choice rename.
Unattended package import
src/services/packageImportUnattended.ts, src/services/packageImportService.ts, src/api/choiceApi.ts, src/api/importPackage.test.ts
The import flow previews packages, applies per-choice and per-asset decisions, and updates choices, commands, and settings after success. Asset content is decoded before the write phase. Tests cover skips, invalid packages and assets, review-required packages, and save completion.
CLI import integration
src/cli/importPackageCli.ts
The CLI delegates package imports to the unattended import service, passes default import modes and acknowledgement, and returns review details when needed.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant QuickAddApi
  participant ChoiceSuggester
  participant ChoiceTree
  Caller->>QuickAddApi: pickChoice(options)
  QuickAddApi->>ChoiceSuggester: Open picker with onPick callback
  ChoiceSuggester->>ChoiceTree: Search choices, including nested choices
  ChoiceTree-->>ChoiceSuggester: Matching choices
  ChoiceSuggester-->>QuickAddApi: Selected leaf choice
  QuickAddApi->>ChoiceTree: Resolve selected ID in live tree
  ChoiceTree-->>QuickAddApi: Choice and path
  QuickAddApi-->>Caller: Choice summary
Loading
sequenceDiagram
  participant Caller
  participant ChoiceAPI
  participant UnattendedImport
  participant PackageImportService
  participant PluginSettings
  Caller->>ChoiceAPI: importPackage(packageJson)
  ChoiceAPI->>UnattendedImport: Parse package and request import
  UnattendedImport->>PackageImportService: Preview, analyze, and apply decisions
  PackageImportService-->>UnattendedImport: Import result
  UnattendedImport->>PluginSettings: Update choices, sync commands, and save
  UnattendedImport-->>ChoiceAPI: Import result
  ChoiceAPI-->>Caller: Added and skipped choices and assets
Loading
Merge Risk: ⚪ Minimal · up to 48047

The reviewed changes have no identified issue that needs resolution before merge; normal checks remain appropriate.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 34.62% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 26 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 API changes: picking, looking up, executing by ID, and importing choices from other plugins.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

A rabbit taps the picker’s door
A nested choice appears once more
The paths are gathered, clear and neat
New package files avoid repeat
The settings save; the queue rolls on
The rabbit hops into the dawn

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

@chhoumann
chhoumann merged commit 49fbd50 into master Oct 18, 2026
16 of 18 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.

1 participant