Skip to content

fix: say so when a macro step cannot be run, instead of dropping it - #1623

Open
chhoumann wants to merge 3 commits into
masterfrom
chhoumann/1571-exhaustive-command-dispatch
Open

fix: say so when a macro step cannot be run, instead of dropping it#1623
chhoumann wants to merge 3 commits into
masterfrom
chhoumann/1571-exhaustive-command-dispatch

Conversation

@chhoumann

@chhoumann chhoumann commented Jul 27, 2026

Copy link
Copy Markdown
Owner

The problem

MacroChoiceEngine.executeCommands was a flat chain of if (isXCommand(command)) guards with no else. A command type it did not know was dropped on the floor - no error, no notice, no log - and the macro still reported success.

"Silently skipped" understates what the user sees. Live repro in an isolated vault, macro = WaitInfiniteAIAssistant(outputVariableName: "out")Capture with format AI said: {{VALUE:out}}:

Before After
before after

The skipped step never sets out, so the capture's {{VALUE:out}} falls through to ensureValueVariableResolvedpromptForVariable, and a modal titled "out" opens mid-macro asking the user to type the AI result by hand. Non-interactive runs abort with ChoiceAbortError. Nothing on screen said why.

InfiniteAIAssistant (#1571) is one instance of a class. The same hole swallows any unrecognised type from:

  • a hand-edited data.json,
  • a hand-authored package (QuickAddPackage.ts validates only that id/name/type are strings),
  • a data.json written by a newer QuickAdd and read by an older one - which our own downgrade recipe produces.

…including inside Conditional branches, which recurse through the same loop.

The change

A switch on command.type with the const exhaustiveCheck: never idiom this file already used in executeEditorCommand. Adding a CommandType without a handler is now a tsc error, so the hole cannot come back:

$ # with a 'Probe = "Probe"' member added to CommandType
src/engine/MacroChoiceEngine.ts(324,13): error TS2322: Type 'CommandType.Probe' is not assignable to type 'never'.

Skip and shout, not abort - matching executeObsidianCommand for a command whose plugin is gone. The step did nothing either way, and aborting would take the file-writing steps that did work with it.

Three shapes reach the default branch and they are deliberately not treated alike:

Shape Behaviour
A real type name QuickAdd does not know Quoted in the notice - it is what the user greps for in data.json
An entry with no usable type ({name: "x"}, a non-string type) Described, never quoted - the notice must not send anyone hunting for a command type called undefined or [object Object]
null / undefined Silent. A hole in the array is not an entry; the same shape is skipped without comment in packageImportService, and a red 15s notice per hole would bury the #1583 resilience work in noise

InfiniteAIAssistant gets a named case (the never check forces one) with an honest message. Verified absolutely: git log -S"InfiniteAIAssistant" --all -- src/engine/ is empty across all 202 tags, so "no released QuickAdd has ever been able to run one" is accurate, not rhetoric.

Also fixed: executeEditorCommand's own default threw, which aborted the entire macro. Same threat model (a newer QuickAdd adding an EditorCommandType), so it gets the same answer. Leaving it would have made PR-adjacent behaviour contradict itself: unknown CommandType skips politely, unknown EditorCommandType kills the run.

Deliberate non-changes

  • No data migration. Converting an InfiniteAIAssistant to a plain AIAssistant is not inert: with promptTemplate.enable: false it opens a template picker and fires a billable request (or throws under disableOnlineFeatures, the default). Dropping it deletes user data to fix a hazard nobody demonstrably has. A migration also runs once, while hand-editing and package import keep working afterwards - this fix covers the hazard permanently and generically.
  • quickadd:run still answers ok: true. The macro ran; one step was skipped. The envelope already carries verified: false with a comment explaining that a resolving execute() does not guarantee a file was written. Enumerating skipped commands in the envelope is a reasonable future addition, not this PR.
  • One notice per skipped command, not per type. The same cadence as every other skip in this file (executeObsidianCommand already stacks one per missing command), and a macro holding several is exactly when seeing each matters.

Evidence (isolated worktree vault, pnpm run obsidian:e2e)

Seeded macro with [null, {name: "Half a command"} /* no type */, EditorCommand("FoldEverything"), Wait], then quickadd:run:

{"ok":true,"command":"quickadd:run","choice":{"id":"edge1571","name":"Edge1571","type":"Macro"},"verified":false,"durationMs":662}

[ERROR] QuickAdd: Skipped 'Half a command' in the macro for 'Edge1571': the saved entry has no
command type, so QuickAdd cannot tell what it was meant to do. It is in
.obsidian/plugins/quickadd/data.json. The rest of the macro continues, so its result may be incomplete.

[ERROR] QuickAdd: Skipped 'Fold everything' in the macro for 'Edge1571': QuickAdd does not recognise
the editor command type 'FoldEverything'. It can come from a newer version of QuickAdd, an imported
package, or a hand-edited .obsidian/plugins/quickadd/data.json. The rest of the macro continues, so
its result may be incomplete.

The null entry produced no notice; the Wait after both skips still ran (662 ms).

Unknown CommandType, with a later capture step proving the macro carried on:

[ERROR] QuickAdd: Skipped 'Step from a newer QuickAdd' in the macro for 'Unknown1571': QuickAdd does
not recognise the command type 'SomeFutureCommand'. …

$ cat Unknown1571Out.md
the macro carried on

Tests

New src/engine/MacroChoiceEngine.unknownCommand.test.ts (10 cases): unknown type shouts and names all three origins; the next command still runs; InfiniteAIAssistant shouts; a shout from inside a conditional then branch; runSubset gets the same guard; a typeless/non-string type is never quoted; null/undefined stay silent; known types still run exactly once each.

tsc -noEmit clean, eslint clean, svelte-check 0 errors, full suite 4658 passed.


Part 1 of 2 for #1571. Part 2 removes the InfiniteAIAssistant type outright; this one is independently mergeable and is the part that generalises.

Summary by CodeRabbit

  • New Features
    • Improved support for user-script exports, including optional entry and settings, with safer initialization.
  • Bug Fixes
    • Unknown or unrunnable macro and editor commands are now reported clearly (with the originating step details) and skipped without stopping subsequent steps, including inside conditional branches and subsets.
    • More consistent messaging for editor command types and specific handling for AI assistant steps.
    • Invalid, missing, or empty command entries are safely ignored without confusing output.
  • Tests
    • Added coverage for unknown/unrunnable command scenarios and malformed inputs.

…ng it

`executeCommands` was a flat chain of `if (isXCommand(command))` guards with
no else, so a command type it did not know was skipped with no error, no
notice and no log - and the macro still reported success. The next step then
inherited the hole: a capture reading `{{VALUE:out}}` from the skipped step's
output variable falls through to `promptForVariable`, so the user gets a
modal titled "out" mid-macro asking them to type the missing value by hand,
and a non-interactive run aborts.

It is a switch now, with the `const exhaustiveCheck: never` idiom
`executeEditorCommand` already used, so adding a `CommandType` without a
handler is a tsc error rather than a silent hole. Unknown types are skipped
and shouted about rather than aborting the macro, matching what
`executeObsidianCommand` does for a command whose plugin is gone: the step
did nothing either way, and aborting would take the file-writing steps that
DID work with it.

Three shapes reach the new default branch, and they are not the same:
- a real type name QuickAdd does not know (a newer QuickAdd's data.json read
  by an older one - which our own downgrade recipe produces - a hand-edited
  data.json, or a hand-authored package) is quoted, because it is what the
  user greps for;
- an entry with no usable type is described as such, never quoted, so the
  notice cannot send anyone hunting for a command type called `undefined` or
  `[object Object]`;
- a null entry is a hole rather than an entry, and stays silent, like the
  same shape in package import.

`InfiniteAIAssistant` gets a named case saying it has never been runnable in
any released QuickAdd - it has no builder entry and no engine branch, so the
only way to hold one is a hand-edited data.json or a hand-authored package.

`executeEditorCommand`'s own default threw, which aborted the whole macro. It
has the same threat model, so it gets the same answer.
@coderabbitai

coderabbitai Bot commented Jul 27, 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 Plus

Run ID: df24a3c1-e6b1-4202-b3e0-61373862ccbc

📥 Commits

Reviewing files that changed from the base of the PR and between 462d0c9 and e0c022c.

📒 Files selected for processing (2)
  • src/engine/MacroChoiceEngine.ts
  • src/engine/MacroChoiceEngine.unknownCommand.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/engine/MacroChoiceEngine.ts
  • src/engine/MacroChoiceEngine.unknownCommand.test.ts

📝 Walkthrough

Walkthrough

MacroChoiceEngine now dispatches command types explicitly, reports unknown or unrunnable steps, skips invalid entries, and continues execution. User-script object exports support guarded settings extraction, with tests covering macro, editor-command, conditional, and runSubset behavior.

Changes

Macro command safety

Layer / File(s) Summary
Command dispatch and unrunnable reporting
src/engine/MacroChoiceEngine.ts
User-script exports are safely interpreted, command dispatch uses a type switch, invalid entries are skipped, and unknown macro or editor commands are reported while execution continues.
Unknown command behavior validation
src/engine/MacroChoiceEngine.unknownCommand.test.ts
Tests verify reporting, continuation, conditional handling, invalid entries, runSubset, editor commands, and known-command execution.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MacroChoiceEngine
  participant executeCommands
  participant executeEditorCommand
  participant reportUnrunnableCommand
  MacroChoiceEngine->>executeCommands: execute macro steps
  executeCommands->>reportUnrunnableCommand: report unknown macro command
  executeCommands->>executeEditorCommand: dispatch editor command
  executeEditorCommand->>reportUnrunnableCommand: report unknown editor command
  reportUnrunnableCommand-->>executeCommands: continue remaining steps
Loading

Possibly related issues

  • Issue 1571 — Covers explicit handling and reporting for silently skipped InfiniteAIAssistant command types.

Possibly related PRs

Suggested labels: released

Poem

I’m a bunny guarding each command,
No mystery step slips through the sand.
Unknown hops get logged with care,
Then onward goes the macro hare.
Nulls nap softly; known steps run bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 accurately summarizes the main change: reporting unrunnable macro steps instead of silently dropping them.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/1571-exhaustive-command-dispatch

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: e0c022c
Status: ✅  Deploy successful!
Preview URL: https://df3ce7bb.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1571-exhaustive-co.quickadd.pages.dev

View logs

@chhoumann
chhoumann marked this pull request as ready for review July 27, 2026 19:57
…st one

"The rest of the macro still ran" was logged BEFORE the rest ran, and when
the skipped step is the last (or only) one, there is no rest. What the tail
is actually for is telling the user the macro was not aborted, so what they
got is partial - which is true wherever the step sits.

Also pins the unnamed-command fallback. It existed to keep `'undefined'` out
of a notice, and nothing failed when it was removed.
…bout

A truthy non-object entry is the shape a shape-based guard would silently
re-swallow. That silence is the hole this change closes, so it gets a pin.
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