Skip to content

feat(capture): capture to an arbitrary frontmatter property (#466)#1363

Merged
chhoumann merged 3 commits into
masterfrom
chhoumann/466-capture-to-property
Jun 15, 2026
Merged

feat(capture): capture to an arbitrary frontmatter property (#466)#1363
chhoumann merged 3 commits into
masterfrom
chhoumann/466-capture-to-property

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Closes #466.

What

Adds a property: Capture target that pre-filters the note picker by an arbitrary frontmatter field, mirroring the existing #tag target.

Capture to value Result
property:type=draft picker limited to notes whose frontmatter type equals draft
property:type presence mode — notes that have a type field (any value)
property:type=draft|folder:Notes|exclude-tag:done + the shared {{FILE}}/{{FIELD}} pipe filters (folder/tag/exclude-folder/exclude-tag/exclude-file)
property:status={{VALUE}} format tokens in the value resolve at run time
  • Value matching is case-insensitive (key and value), trimmed; list-valued properties (type: [draft, idea]) match if any element equals the value.
  • Matches YAML frontmatter only (not inline Dataview fields).

Why this syntax

A bare {{type: draft}} would clash with the {{FIELD:type}} format grammar (the original report ran into exactly this). property: is an unambiguous prefix that mirrors the # tag sigil, survives the engine's formatFileName pass, and can't collide with a real vault path. | is reserved for the pipe-filter grammar, reusing the same FieldSuggestionParser/EnhancedFieldSuggestionFileFilter that {{FILE:}} uses.

How

Mirrors the tag target across the three resolution sites plus the builder, with a single shared classifier so the engine and the one-page preflight can never disagree:

  • src/utils/propertyTarget.ts — pure parsePropertyTarget, the single source of truth used by both engine and preflight.
  • src/utils/vaultQueries.tsgetMarkdownFilesWithProperty + pure frontmatterValueMatches (guards null before String() coercion; uses String(raw) so false/0 match; array any-match; case-insensitive key lookup).
  • src/engine/CaptureChoiceEngine.tsproperty branch in resolveCaptureTarget (placed before the .base/folder/extension checks so a value containing .md///.base can't misroute) + selectFileWithProperty; an empty field name aborts with a clear message.
  • src/preflight/collectChoiceRequirements.ts — populates the one-page/CLI target dropdown for property targets, skipped when the value is tokenized (mirrors the tokenized-file-path escape).
  • CaptureTargetSetting.svelte — shows a neutral "filters notes by frontmatter property" hint instead of a misleading fake-path preview.
  • Docs: docs/docs/Choices/CaptureChoice.md.

Verification

  • Unit: 35 new tests (parser, value matcher, query, preflight); full suite green.
  • Real Obsidian e2e (isolated worktree vault): the live suggester listed exactly the expected matches — including Type: Draft (case-insensitive key + value), an array member, and a sub-folder note; |folder: intersected correctly; presence mode listed all typed notes; property: aborted cleanly with no junk file; a full capture wrote below the preserved YAML frontmatter.

Release / migration impact

New, additive capture-target syntax. No migration. Existing Capture to values are unaffected (nothing else starts with property:).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added property:field=value capture targets (frontmatter-based) and property:field presence-mode selection.
    • The capture destination picker now matches frontmatter fields/values case-insensitively with trimmed input, supports array-valued fields (any entry match), and allows picker filters via |... syntax.
    • Updated “Capture to” UI/help for property targets; property targets no longer trigger canvas-target UI.
    • Note creation from the picker does not automatically reuse the property filter.
  • Documentation

    • Expanded the capture targeting guide with full syntax, matching rules (including equality vs presence), and filter composition examples.

)

Mirror the existing #tag capture target with a frontmatter-property filter:

  property:type=draft   capture into a note whose `type` frontmatter is draft
  property:type         presence mode (note has the field, any value)
  property:type=draft|folder:Notes|exclude-tag:done
                        + the shared {{FILE}}/{{FIELD}} pipe filters

- new pure parsePropertyTarget (single classifier shared by engine + preflight)
- getMarkdownFilesWithProperty + null/false/0-safe frontmatterValueMatches,
  case-insensitive key+value, array any-match, reuses
  EnhancedFieldSuggestionFileFilter for the pipe filters
- resolveCaptureTarget property branch (before the .base/folder checks) +
  selectFileWithProperty mirroring selectFileWithTag; empty-field aborts cleanly
- preflight one-page dropdown for property targets, skipped when the value is
  tokenized (mirrors the tokenized-file-path escape)
- builder shows a neutral "filters notes by frontmatter property" hint instead
  of a misleading fake-path preview
- docs + tests (parser, value matcher, query, preflight)

Closes #466
@coderabbitai

coderabbitai Bot commented Jun 15, 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: 6af8a8f0-ea00-4ef0-84bd-c7307486d570

📥 Commits

Reviewing files that changed from the base of the PR and between efa22c9 and 86b9a14.

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

📝 Walkthrough

Walkthrough

Adds property:<field>[=<value>] as a new capture target syntax. A new parser module classifies and parses these targets; new vault query helpers scan frontmatter with presence and equality modes. The capture engine resolves property targets to a file picker, preflight collection populates candidates, the settings UI shows a property hint, and documentation describes the full syntax.

Changes

property: capture target feature

Layer / File(s) Summary
PropertyTarget interface and parser
src/utils/propertyTarget.ts, src/utils/propertyTarget.test.ts
Introduces PropertyTarget interface, isPropertyTarget, and parsePropertyTarget with case-insensitive prefix detection, first-= field/value splitting, pipe-filter parsing, and presence mode when no value is given.
Vault query helpers and barrel export
src/utils/vaultQueries.ts, src/utils/vaultQueries.test.ts, src/utilityObsidian.ts
Adds frontmatterValueMatches (null-safe, array-aware, coercion matching) and getMarkdownFilesWithProperty (presence and equality vault scan with optional folder/tag filtering). Re-exports the new function from the utility barrel.
CaptureChoiceEngine: property resolution and picker
src/engine/CaptureChoiceEngine.ts, src/engine/CaptureChoiceEngine.selection.test.ts
Extends resolveCaptureTarget to recognize property: strings (aborting if no field), updates the target type union, adds selectFileWithProperty with ordered candidates and basename/path existence checks, and refactors selectFileFromSet for shared picker mechanics. Tests verify correct parsing, .md value handling, pipe-filter parsing, and validation of required field names.
Preflight requirements: property target detection
src/preflight/collectChoiceRequirements.ts, src/preflight/collectChoiceRequirements.test.ts
Parses property: targets in collectForCaptureChoice (skipping template-format strings), gates the dropdown preflight on property targets, and populates the candidate file list via getMarkdownFilesWithProperty. Tests cover dropdown forcing, presence-mode queries, filter propagation, and malformed target handling.
Settings UI hint and documentation
src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte, docs/docs/Choices/CaptureChoice.md
Derives isProperty/propertyHint in the settings component to swap FormatPreviewField for a property hint and updates the description text. Adds the property: routing rule and a full "Capturing to a property" subsection with syntax, matching rules, filter composition, and constraints to the docs.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CaptureChoiceEngine
  participant parsePropertyTarget
  participant getMarkdownFilesWithProperty
  participant InputSuggester

  User->>CaptureChoiceEngine: trigger capture (captureTo = "property:status=active")
  CaptureChoiceEngine->>parsePropertyTarget: "property:status=active"
  parsePropertyTarget-->>CaptureChoiceEngine: {kind:"property", field:"status", value:"active", filter:{}}
  CaptureChoiceEngine->>getMarkdownFilesWithProperty: app, "status", "active", {}
  getMarkdownFilesWithProperty-->>CaptureChoiceEngine: TFile[] (matching frontmatter)
  CaptureChoiceEngine->>InputSuggester: ordered candidates list
  InputSuggester-->>CaptureChoiceEngine: selected TFile path
  CaptureChoiceEngine-->>User: capture written to selected file
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • chhoumann/quickadd#1129: Directly modifies src/preflight/collectChoiceRequirements.ts — the same file this PR extends with property: target handling.
  • chhoumann/quickadd#1098: Both PRs modify src/engine/CaptureChoiceEngine.ts around capture-target resolution, adding new target kinds and dispatch routing.

Poem

🐇 A hop through the frontmatter fields,
Where property:status=active now yields
A picker of notes, filtered just right —
No | in values, but filters take flight!
The rabbit approves this YAML-fueled delight. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 and concisely describes the main feature: adding support for capturing to arbitrary frontmatter properties. It is specific, action-oriented, and accurately reflects the primary change across all modified files.
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.

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

✨ 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/466-capture-to-property

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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 15, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 86b9a14
Status: ✅  Deploy successful!
Preview URL: https://169220dc.quickadd.pages.dev
Branch Preview URL: https://chhoumann-466-capture-to-pro.quickadd.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3bc258737d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/engine/CaptureChoiceEngine.ts Outdated
Comment on lines +917 to +919
valueExists: (v) =>
existingMatched.has(v) ||
existingMatched.has(v.replace(/\.md$/i, "")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check custom property targets against the whole vault

When create-file is enabled for a property:type=... target, typing the path or basename of an existing note that does not currently match the property is not found in existingMatched, so the suggester offers it as “Create new note”. Selecting that custom row returns the path, and the later fileExists branch writes to the existing nonmatching file via onFileExists, bypassing the property filter and not creating anything. The existence check should consider the whole vault or reject existing non-matches, not just filesWithProperty.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in efa22c9. The tag/property pickers now share selectFileFromSet, whose valueExists is vault-aware: typing an existing note that doesn't match the filter no longer shows a "Create new note" row and isn't offered, so it can't be captured into. Verified e2e — typing an existing type: note note yields no rows; a genuinely new name still offers create.

- fix Codex P2: extract shared selectFileFromSet helper for the tag/property
  pickers and make the "Create new note" existence check vault-aware, so typing
  an existing non-matching note no longer mislabels as "create" nor captures
  into that file (it isn't offered) — also removes the selectFileWithTag clone
- docs: the pipe-filter grammar is {{FIELD}} (which honors |folder:), not {{FILE}}
- docs + code comment: value matching is always case-insensitive; only
  folder/tag/exclude-* pipes are honored (others are inert)
- builder: exclude property targets from canvas detection so property:...canvas
  never offers a meaningless canvas-node picker
- tests: engine-level resolveCaptureTarget property cases (incl. .md-bearing
  value not misrouting, pipe filters, empty-field abort)
@chhoumann

Copy link
Copy Markdown
Owner Author

Review round (commit efa22c9) — addressed an ultracode multi-lens review + the Codex P2:

  • Codex P2 (create→existing-note): tag/property pickers now share selectFileFromSet with a vault-aware valueExists, so typing an existing non-matching note no longer mislabels as "Create new note" nor captures into it. (Also removes the selectFileWithTag duplication.)
  • Docs: the pipe-filter grammar is {{FIELD}} (which honors |folder:), not {{FILE}} — fixed the cross-reference.
  • Docs + code comment: value matching is always case-insensitive; only folder/tag/exclude-* pipes are honored (other FIELD options are inert).
  • Builder: property:…canvas no longer also renders the canvas-node picker.
  • Tests: engine-level resolveCaptureTarget property cases (incl. a .md-bearing value not misrouting, pipe filters, empty-field abort).

Full suite green (2325 passing), build + lint + svelte-check clean; e2e re-verified in the worktree vault.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efa22c9441

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/engine/CaptureChoiceEngine.ts Outdated
Comment on lines +897 to +900
valueExists: (value) =>
existingTagged.has(value) ||
existingTagged.has(value.replace(/\.md$/i, "")),
existingInSet.has(value) ||
existingInSet.has(value.replace(/\.md$/i, "")) ||
this.vaultFileExists(value),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress create for nested existing basenames

When the tag/property picker has create-if-missing enabled and the user types the basename of an existing non-matching note in a folder, this valueExists check still returns false because vaultFileExists() only probes getAbstractFileByPath(value[.md/.canvas]), e.g. Foo/Foo.md, and never scans vault basenames such as Archive/Foo.md. In that scenario the modal still offers Create new note: Foo, mislabeling an existing note and creating a duplicate root note instead of suppressing the create row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 86b9a14: replaced vaultFileExists with captureTargetAlreadyExists, which also checks a vault-wide basename set (built once per picker, O(1) per keystroke). Verified e2e — typing the basename of a non-matching subfolder note now shows no rows; a genuinely new name still offers create.

@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

🤖 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 `@src/engine/CaptureChoiceEngine.ts`:
- Around line 851-860: The vaultFileExists method only checks for direct path
matches and does not search for existing files by basename in other directories,
causing false negatives for notes that exist outside the root folder. Extend
vaultFileExists to also iterate through all files in the vault and check if any
file's basename (without extension) matches the provided value, in addition to
the existing direct path checks. This will ensure that valueExists correctly
identifies existing notes regardless of their folder location and prevent false
"creatable" labels that could lead to accidental duplicate note creation.
🪄 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: 2bd82b1c-0f73-4203-b193-ab4674f2dadb

📥 Commits

Reviewing files that changed from the base of the PR and between 3bc2587 and efa22c9.

📒 Files selected for processing (5)
  • docs/docs/Choices/CaptureChoice.md
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte
  • src/utils/propertyTarget.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/docs/Choices/CaptureChoice.md
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte
  • src/utils/propertyTarget.ts

Comment thread src/engine/CaptureChoiceEngine.ts Outdated
…er (#466)

Follow-up to the vault-aware picker fix: vaultFileExists only probed exact paths,
so typing the bare basename of an existing note that lives in a subfolder still
offered "Create new note" (mislabeling it / risking a duplicate). Replace it with
captureTargetAlreadyExists, which checks exact path candidates AND a vault-wide
basename set (built once per picker, O(1) per keystroke). Flagged by Codex (P2)
and CodeRabbit (major) on the previous commit.
@chhoumann

Copy link
Copy Markdown
Owner Author

Pushed 86b9a14 addressing the basename-suppression follow-up from Codex (P2) and CodeRabbit (major). Full suite green (2325), lint clean, e2e re-verified.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 86b9a14236

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chhoumann

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 86b9a14236

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chhoumann chhoumann merged commit 507f9d4 into master Jun 15, 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] Capture To arbitrary frontmatter fields.

1 participant