Skip to content

fix: append link respects caret when focused in a frontmatter property (#768)#1340

Open
Ivikhostrup wants to merge 5 commits into
chhoumann:masterfrom
Ivikhostrup:fix/768-append-link-in-property-fields
Open

fix: append link respects caret when focused in a frontmatter property (#768)#1340
Ivikhostrup wants to merge 5 commits into
chhoumann:masterfrom
Ivikhostrup:fix/768-append-link-in-property-fields

Conversation

@Ivikhostrup

@Ivikhostrup Ivikhostrup commented Jun 13, 2026

Copy link
Copy Markdown

Problem

Fixes #768. When Append link to current file runs while the caret is in a
frontmatter property field (Obsidian's Properties widget), the link is
inserted at the first body line (right after the YAML) instead of at the
cursor. It works fine when the caret is in the document body.

Steps to reproduce (on master)

Manual, in a live vault:

  1. Settings → QuickAdd: create (or edit) a Template or Capture choice and enable
    Append link to current file.
  2. Open any note that has frontmatter, so the Properties panel renders in
    Live Preview, e.g.:
    ---
    title: My Note
    ---
    some existing text
  3. Click into a property value field (e.g. title) so the caret is inside
    the Properties widget — not the note body.
  4. Run the QuickAdd choice.

Expected: the link is appended at the caret (in the property).
Actual: the link is inserted on the first body line, right after the ---:

---
title: My Note
---
[[Created Note]]      <-- inserted here instead of at the caret
some existing text

Automated (no Obsidian needed) — the new test doubles as a repro:

pnpm install
pnpm exec vitest run src/utilityObsidian.appendLink-768.test.ts

On master the insertLinkWithPlacement (#768 property-field routing) case
fails (the body editor's replaceSelection is called instead of the focused
property); with this PR all 7 cases pass.

Root cause

The whole append-link path reads the cursor from the markdown body editor:

insertFileLinkToActiveView (src/utilityObsidian.ts) →
insertLinkWithPlacement, which uses editor.listSelections() /
editor.getCursor().

In Live Preview the Properties panel is a separate set of input elements, not
the CodeMirror document. When a property is focused, the body editor isn't, so
getCursor() returns a stale body caret (the first line after the frontmatter).
There was no check for this case, so the link landed in the body.

Fix

insertLinkWithPlacement now detects when focus is inside the Properties widget
(the focused DOM element under .metadata-property /
.metadata-properties-container) and inserts the link at that element's
caret
, dispatching an input event so Obsidian persists it to the
frontmatter. It falls back to the existing body-editor behavior when no property
field is focused or the element type is unsupported. Placement modes
(replaceSelection, etc.) only describe body insertion, so they don't apply in
the property case.

Two small helpers (getFocusedEditablePropertyEl, insertTextIntoEditableEl)
are added and exported via __test for unit coverage.

Validation

  • pnpm exec vitest run src/utilityObsidian.appendLink-768.test.ts7 passed
    (focus detection, editable-element insertion, and property-vs-body routing).
  • Regression check on related suites
    (utilityObsidian, linkPlacement, engine/, formatter-linkcurrent) →
    345 passed, 2 skipped, no regressions.
  • eslint on the changed files → clean; tsc -noEmit -skipLibCheck → no src/ errors.
  • Also reproduced/confirmed at runtime by driving the real
    insertFileLinkToActiveView over a frontmatter document with a jsdom
    Properties widget: pre-fix the link lands at the first body line; post-fix it
    lands in the focused property and the body editor is left untouched.

Note: automated tests + a jsdom-level runtime repro are green, but I have not
yet clicked through this in a live Obsidian vault. A manual check (the steps
above) would be good before merge.

Notes for reviewers

  • main.js / styles.css are gitignored in this repo, so only source is included.
  • The contenteditable branch of insertTextIntoEditableEl covers Obsidian's
    rich text/list property inputs; the <input>/<textarea> branch covers the
    simpler ones.

Summary by CodeRabbit

  • Bug Fixes

    • Choice and capture link insertion now targets the active frontmatter Properties widget when the caret is in a property value field, correctly merging/appending links (including list, empty/missing, and embedded behavior).
    • Avoids inserting links when focus is in the property key field, in typed/number-like inputs, outside a markdown view, or when no property is focused.
  • Tests

    • Added DOM-mounting and assertions covering property focus detection and link placement/combining, including numeric values preserved as strings.

… fields

When "Append link to current file" runs while the caret is inside a
frontmatter property (Obsidian's Properties widget), the markdown body
editor is not focused, so editor.getCursor()/listSelections() report a
stale body caret. The link was inserted at the first body line instead
of at the user's cursor.

insertLinkWithPlacement now detects when focus is in a property field
(via the focused DOM element inside .metadata-property /
.metadata-properties-container) and inserts the link at that element's
caret, firing an input event so Obsidian persists it to the frontmatter.
It falls back to the existing body-editor behavior when no property field
is focused or the element type is unsupported.

Adds src/utilityObsidian.appendLink-768.test.ts covering the focus
detection helper, the editable-element insertion helper, and the
property-vs-body routing in insertLinkWithPlacement.

Co-Authored-By: Claude <noreply@anthropic.com>
@Ivikhostrup Ivikhostrup requested a review from chhoumann as a code owner June 13, 2026 22:53
@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: 260871e2-f01e-4486-babc-43d96fbcc36e

📥 Commits

Reviewing files that changed from the base of the PR and between 80502c9 and a8d5c5e.

📒 Files selected for processing (5)
  • src/choiceExecutor.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/utilityObsidian.appendLink-768.test.ts
  • src/utilityObsidian.ts
💤 Files with no reviewable changes (1)
  • src/engine/TemplateChoiceEngine.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/engine/CaptureChoiceEngine.ts

📝 Walkthrough

Walkthrough

When a QuickAdd choice runs, the code now captures the currently focused frontmatter property field (file and property key) before any prompts or suggesters appear. If a property was focused, capture and template choices route their link insertion through a persistent frontmatter-aware path that merges the link into the correct frontmatter property using processFrontMatter, rather than inserting into the document body. Core utilities detect focused editable property value fields, persist links to matching frontmatter keys, and handle list/scalar/embed behaviors.

Changes

Frontmatter Property Link Insertion

Layer / File(s) Summary
Core frontmatter property utilities
src/utilityObsidian.ts, src/utilityObsidian.appendLink-768.test.ts
Adds FrontmatterPropertyTarget interface, getFocusedPropertyTarget to locate the focused editable value field in the correct Markdown view, and appendLinkToFrontmatterProperty to persistently merge links into frontmatter. Tests cover field focus detection, typed field exclusion, null cases, and link appending across list, scalar, missing, numeric string preservation, and embed scenarios.
Focus capture in choice executor
src/IChoiceExecutor.ts, src/choiceExecutor.ts
IChoiceExecutor adds optional focusedProperty field. ChoiceExecutor captures the focused property state once per execution chain via getFocusedPropertyTarget(this.app) when executionDepth === 0, then clears it when the outermost execution unwinds in the finally block.
Capture choice engine integration
src/engine/CaptureChoiceEngine.ts
insertCaptureLink becomes async and conditionally routes to appendLinkToFrontmatterProperty when choiceExecutor.focusedProperty is present; otherwise uses existing canvas/editor and body insertion logic. Both call sites (run() and handleCanvasTextCapture()) now await the insertion.
Template choice engine integration
src/engine/TemplateChoiceEngine.ts
Conditionally inserts the generated file link: if choiceExecutor.focusedProperty exists, calls appendLinkToFrontmatterProperty; otherwise falls back to insertFileLinkToActiveView.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

released

Poem

🐇 When fingers find a property field,
We catch that focus before it's sealed.
Links now land right where carets stay—
No more stray hops to body's bay!
Frontmatter bound, with tests that spring!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 describes the primary fix: making the append link functionality respect the caret position when focused in a frontmatter property field.
Linked Issues check ✅ Passed The PR fully addresses issue #768 by implementing frontmatter property detection, link insertion via processFrontMatter, list/scalar property handling, and typed input fallback logic.
Out of Scope Changes check ✅ Passed All changes directly support frontmatter property link insertion within scope of issue #768; no unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/768-append-link-in-property-fields

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.

@Ivikhostrup Ivikhostrup changed the title Fix #768: append link respects caret in frontmatter property fields fix: append link respects caret when focused in a frontmatter property (#768) Jun 13, 2026
Funnel the focus-detection + insertion behind a single intention-revealing
tryInsertIntoFocusedProperty(text) call, name the Properties-widget selector
and the editable-element check, and use descriptive variable names.

@chhoumann chhoumann left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for tackling this, @Ivikhostrup — the diagnosis in #768 is exactly right (a focused property is a separate input, not the CodeMirror caret), and the naming refactor in dda12e5b reads well. I want to call out what's solid before the problems: the body-insertion path is left completely untouched (purely additive, multi-cursor + every placement mode byte-for-byte identical) so there's zero regression to the existing feature, the early-return is positioned correctly, and there's no injection vector (text nodes / element.value, never innerHTML).

I took this into a live Obsidian vault — the part you flagged as not-yet-verified — and that's unfortunately where the property helper runs into trouble. Requesting changes; details are inline, but the headlines:

🔴 Blockers (reproduced live — data loss)

  • Number / Date / Datetime properties get wiped. They render as native <input type="number"> / type="date". Slicing [[Note]] into them makes the browser reject the value, so element.value becomes "", insertTextAtCaret returns true (suppressing the body fallback), and Obsidian persists the emptied value on blur. Reproduced: count: 5 → count: (empty), date: 2026-06-14 → (empty).
  • Caret in the property KEY field corrupts the key name (count[[Note]]: 5) — .metadata-property-key-input matches the same .metadata-property selector as the value.

🟠 Majors

  • Default text properties are contenteditable .metadata-input-longtext, not <input>. So the branch the tests cover (<input type="text">) isn't the one real text properties use, and the contenteditable branch is untested. The good news: that branch does persist for longtext — but only on blur, and the helper never blurs (so it can sit uncommitted / be discarded on re-render).
  • List / tags (.multi-select-input) silently swallow the text — they commit pills on Enter, not on a synthetic input event — so it's a no-op that still returns true and discards the link.
  • Focus timing: append-link runs after the choice. Any choice with a prompt/suggester/modal (or launched from the command palette) moves focus off the property before insertion → it falls back to the stale body caret → the original #768. The focused target isn't captured before the UI opens. Worth verifying with the built PR across capture-with-prompt / template-with-VALUE-suggester / command-palette / split panes.
  • textContent fallback flattens an already-linked property's child nodes and drops the existing link.

Suggested direction

The brittle part is using Obsidian's private Properties DOM to both detect the caret and persist the value. Consider (1) capturing the focused property target before the choice opens any UI, and (2) persisting via app.fileManager.processFrontMatter(...) keyed to the resolved property + the target TFile, using the DOM only to read which property/caret. That sidesteps the typed-input wipe, the blur dependency, pop-out windows, and list/tags in one move.

At minimum the two blockers need fixing, plus a live-vault e2e (text, longtext, number, date, list/tags, key field) before merge. Happy to review a follow-up — the core idea is right and worth getting in. 🙏

(Reviewed at head dda12e5b. Findings verified by driving a real Obsidian dev vault, not just jsdom.)

Comment thread src/utilityObsidian.ts Outdated
const endOffset = element.selectionEnd ?? element.value.length;
const before = element.value.slice(0, startOffset);
const after = element.value.slice(endOffset);
element.value = before + text + after;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocker (data loss): this <input> branch wipes existing data for non-text inputs. Obsidian renders Number/Date/Datetime properties as native typed inputs (<input type="number">, type="date", type="datetime-local"). element.value = before + text + after makes the browser reject the non-numeric string, so element.value becomes "" and Obsidian persists null on blur — the original value is destroyed (reproduced live: count: 5 → count: empty, date: 2026-06-14 → empty). Because insertTextAtCaret then returns true, the body fallback is suppressed too. Please restrict this branch to text-like inputs (e.g. only when element.type is text/search/empty) and return false otherwise so it falls back to the body editor.

Comment thread src/utilityObsidian.ts Outdated
if (typeof document === "undefined") return false;
const focused = document.activeElement;
if (!(focused instanceof HTMLElement)) return false;
if (!focused.closest(PROPERTY_WIDGET_SELECTOR)) return false;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocker (key corruption): a .metadata-property row contains both .metadata-property-key-input (the property NAME) and the value editor, and both are editable elements inside .metadata-property. If the caret is in the key field (e.g. mid-naming a new property), the link is injected into the property name and persisted as a corrupted key on blur (count[[Note]]: 5). Please scope detection to the value side — require focused.closest(".metadata-property-value") and/or bail when focused.closest(".metadata-property-key").

Minor on PROPERTY_WIDGET_SELECTOR (line 563): the real container class is .metadata-properties (not .metadata-properties-container), so that half of the selector never matches — detection currently survives only via .metadata-property.

Comment thread src/utilityObsidian.ts Outdated
return true;
}

if (element.isContentEditable) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Two real cases land in this contenteditable branch:

  1. Default text properties render as a contenteditable .metadata-input-longtext div (not the <input> the tests mount), so this is the real text path. It does persist to frontmatter — but only on blur, and insertTextAtCaret never blurs, so the change can sit uncommitted and be discarded if Obsidian re-renders the widget first.
  2. List / tags render as .multi-select-input (also contenteditable) and commit pills on Enter, not on a synthetic input event. So this inserts the text, fires input, returns true (suppressing the body fallback), but the text never reaches frontmatter and is discarded on blur (reproduced live: tags stayed [a, b]).

Please detect .multi-select-container/.multi-select-input and return false (so the body fallback runs) rather than reporting false success; and consider explicitly committing (blur) after a longtext insert.

Comment thread src/utilityObsidian.ts Outdated
selection.removeAllRanges();
selection.addRange(range);
} else {
element.textContent = `${element.textContent ?? ""}${text}`;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Setting element.textContent = existing + text on a contenteditable removes ALL child nodes (replacing them with one flat text node) and appends at the end, ignoring the caret. For an already-linked longtext property (rendered as .metadata-link child nodes whose textContent is only the display text) this silently drops the original link. This branch is reachable when focus is restored after a modal closes without an in-element selection. Please return false here so the caller falls back to the body editor instead of doing a destructive append.

Comment thread src/utilityObsidian.ts Outdated
*/
function tryInsertIntoFocusedProperty(text: string): boolean {
if (typeof document === "undefined") return false;
const focused = document.activeElement;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Use the popout-aware document, not the global one. This codebase's convention is getOwnerDocument/activeDocument (see activeWindow.ts, suggest.ts, fileSuggester.ts). With global document.activeElement, a property focused in a popped-out window is never detected and #768 silently falls back to body insertion in popouts. The active view is in scope at the insertLinkWithPlacement call site, so you can thread view.containerEl.ownerDocument.activeElement in.

Also cross-realm: a popout input is not instanceof the main realm's HTMLInputElement (see isEditableElement, lines 566-572), so even after fixing the document it would be rejected — prefer duck-typing (element.matches("input,textarea") / "selectionStart" in element).

Comment thread src/utilityObsidian.ts Outdated
// #768: a focused frontmatter property field leaves the body editor's caret
// stale; insert into the property at its caret instead. Placement modes only
// apply to body insertion.
if (tryInsertIntoFocusedProperty(text)) return;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Two concerns at this routing point:

  1. Focus timing: insertFileLinkToActiveView runs after the Template/Capture choice. Any flow that opens a prompt/suggester/modal — or is launched from the command palette — can move focus off the property before this runs, so tryInsertIntoFocusedProperty returns false and we fall back to the stale body caret (the original [BUG] Append link to current file in properties #768). Capturing the focused property target before QuickAdd opens UI (and threading it through) would make this robust.
  2. Wrong-file write: there's no check that the focused property belongs to the active view. With split panes / multiple MarkdownViews, document.activeElement could be a property in a different leaf than getActiveViewOfType(MarkdownView) returns, so the link is written into the wrong note's frontmatter. Consider asserting view.containerEl.contains(focused) before inserting.

Both need a live-vault check with the built PR (capture-with-prompt, template-with-VALUE-suggester, command-palette, split panes).

}

/** Mounts an Obsidian-like Properties widget with one focused text property. */
function mountFocusedTextProperty(initialValue = ""): HTMLInputElement {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This suite only mounts <input type="text">, which is the one shape that trivially persists and isn't how a default text property renders (default text = contenteditable .metadata-input-longtext). So the contenteditable Range branch, the textContent fallback, the typed-input wipe (number/date), and the key-field case are all untested, and the 7 green tests give false confidence for the most common real property shapes. Note: jsdom doesn't implement isContentEditable, so a contenteditable test must force it (e.g. Object.defineProperty(el, "isContentEditable", { value: true })). Also, the input-event assertion only proves a locally-attached listener fired — it can't prove Obsidian persists — so it's worth a real dev-vault e2e gating merge rather than relying on these unit tests.

@Ivikhostrup Ivikhostrup marked this pull request as draft June 13, 2026 23:30
@Ivikhostrup

Copy link
Copy Markdown
Author

Marking this draft — the fix is verified non-functional in real Obsidian. Posting the verification so the approach can be corrected rather than silently shipped.

Verified against a live Obsidian 1.9.14 instance, inspected over its remote-debugging (CDP) port in an isolated throwaway vault.

Mechanism — confirmed ✅

With the caret in a frontmatter property, editor.getCursor() returns {line: 4} — the first body line after the YAML. The body editor's reported caret is stale, and insertLinkWithPlacement faithfully inserts there. That part of the diagnosis holds.

Why this PR's remedy does not work ❌

The real property value editor is a div.metadata-input-longtext (contenteditable), managed by Obsidian's MetadataEditor. Mutating it from outside its own input pipeline does not persist to the file:

Insertion approach Persists to file?
DOM text-node insert + new Event('input') (this PR) ❌ no change
document.execCommand('insertText', …) ❌ no change
app.fileManager.processFrontMatter(file, fm => …) ✅ persists

insertTextAtCaret runs, but Obsidian re-renders the property from its model and discards the DOM change — the link is never written. The unit test passed only because it drove a plain <input> (which Obsidian doesn't use here) with the same assumption baked into the fixture, so it never exercised the real path.

Correct direction

  • .metadata-property is a valid selector (confirmed via closest()); .metadata-properties-container does not exist in the DOM and should be dropped.
  • The focused property row exposes data-property-key (and .metadata-property-key-input), so the target property is identifiable.
  • Persist via processFrontMatter on that key instead of touching the DOM.

Open design questions before reworking

  1. processFrontMatter appends to the property value; it can't insert at the in-value caret. "Append to the focused property" is the persistable reading of "at the cursor position."
  2. String vs. list property handling differ (append to string vs. push a list item).
  3. processFrontMatter re-serializes the YAML (e.g. trims tags: tags:).

Happy to rework around processFrontMatter if that direction looks right.

AI-assisted verification (Claude). DOM facts gathered via CDP inspection of a live vault, not inferred.

…cessFrontMatter

Addresses @chhoumann's review. The DOM-mutation approach was non-functional
(synthetic input events are discarded) and unsafe (wiped number/date inputs,
corrupted the key field). Replaced with the reviewer's suggested architecture:

- ChoiceExecutor.execute() captures the focused frontmatter property
  (file + key) BEFORE any prompt/suggester opens, fixing the focus-timing race
  where the caret had already left the property by insertion time.
- getFocusedPropertyTarget(): popout-aware (per-view ownerDocument), scoped to
  .metadata-property-value (never the key field), binds to the owning view's
  file (split-pane safe), and skips typed number/date/checkbox inputs.
- appendLinkToFrontmatterProperty(): persists via app.fileManager
  .processFrontMatter — pushes to list properties, appends to scalars, sets
  empty ones. Never wipes (the previous data-loss blockers).
- insertLinkWithPlacement() restored to its original body-only behavior; the
  brittle DOM-insertion helpers are removed.

Engines route to the property when one was captured, else body insertion.

Verified live in a real Obsidian vault (CDP): the built plugin loads, and the
full command flow appends the link into the focused text property
(text_prop: hello [[Captured]]) instead of the body, with sibling properties
untouched. Unit tests cover detection (value/key/typed/none/split) and the
processFrontMatter append (list/scalar/empty/number/embed).

Co-Authored-By: Claude <noreply@anthropic.com>
@Ivikhostrup

Copy link
Copy Markdown
Author

Thanks for the incredibly thorough live review, @chhoumann — you found the data-loss cases and the focus-timing race I'd missed, and the direction you laid out was right. I've reworked it along exactly those lines.

What changed

The brittle "DOM detect + DOM mutate" approach is gone. Now:

  1. Capture before UI. ChoiceExecutor.execute() records the focused property { file, key } before any prompt/suggester/preflight runs (right next to the existing originLeaf capture), so the caret leaving the property no longer matters.
  2. getFocusedPropertyTarget() — popout-aware (per-view ownerDocument via getOwnerDocument), scoped to .metadata-property-value, binds to the owning view's file (split-pane safe), duck-typed (no cross-realm instanceof on the element).
  3. appendLinkToFrontmatterProperty() — persists via app.fileManager.processFrontMatter: pushes to list properties, appends to scalars, sets empties. Never mutates the DOM, so no blur dependency and nothing to discard.
  4. insertLinkWithPlacement is back to its original body-only form; the DOM-insertion helpers are deleted.

Point-by-point

Your finding Resolution
🔴 Number/Date wiped Detection skips input[type=number/date/datetime-local/time/month/checkbox] → body fallback. And processFrontMatter never wipes regardless.
🔴 Key-field corruption Detection requires .closest(".metadata-property-value"); the key input lives in .metadata-property-key → excluded.
🟠 longtext is the real text path / blur-only No longer DOM-mutating; processFrontMatter persists without a blur.
🟠 list/tags no-op List properties now get a new array item via processFrontMatter.
🟠 Focus-timing race Captured at execute() before any UI.
🟠 textContent drops existing links That code path is removed entirely.
Popout / cross-realm Per-view ownerDocument; element checks are duck-typed selectors, not instanceof.
Wrong-file write (split panes) Binds to the leaf whose containerEl.contains(focused).
Tests gave false confidence New tests don't touch isContentEditable; they drive detection via selectors and the append via a processFrontMatter mock (list/scalar/empty/number/embed/key-field/typed).

Live verification (real Obsidian vault, via CDP)

Built the plugin, installed it in an isolated vault, and ran the full command flow with the caret in a text property:

before: text_prop: hello   (caret in .metadata-input-longtext)
run choice "E2E Capture" (appendLink on) →
after:  text_prop: hello [[Captured]]     ← link in the property, not the body
        num_prop: 5                        ← untouched

Also confirmed live: the property value editor is .metadata-input-longtext (contenteditable), number/date are typed <input>s and the key is .metadata-property-key-input — i.e. the selectors above match the real DOM.

Honest about coverage

  • The text / no-prompt path is verified end-to-end live. The prompt/suggester path relies on the capture happening before runOnePagePreflight (by construction, mirroring originLeaf) — I didn't drive a prompt+focus through automation.
  • list/tags append and popout are covered by unit tests + the confirmed DOM structure, but I couldn't get programmatic/synthetic focus to land in multi-select / typed inputs in a headless instance, so those specific real-focus paths aren't live-verified. A manual pass on list/tags + popout + command-palette before merge would be worth it.
  • Design choice: number/date/checkbox properties fall back to body insertion (appending a link there is nonsensical / would coerce the type). Text and list are handled in place. Happy to change that if you'd rather they no-op.

Marking ready for another look. 🙏

AI-assisted (Claude). Live checks via CDP against a real dev vault.

@Ivikhostrup Ivikhostrup marked this pull request as ready for review June 15, 2026 06:46

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@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.

🧹 Nitpick comments (1)
src/utilityObsidian.appendLink-768.test.ts (1)

92-101: ⚡ Quick win

Expand typed-input exclusion coverage to all excluded types.

This test currently validates only type="number", while production filtering also excludes date/datetime/time/month/checkbox. Parameterizing this case will better guard the selector contract.

Suggested test refactor
-it("returns null for typed inputs (number/date) so they fall back to body", () => {
-	const file = makeFile("Note.md");
-	const { app } = mountViewWithProperty({
-		key: "count",
-		focus: "value",
-		file,
-		valueType: "number",
-	});
-	expect(getFocusedPropertyTarget(app)).toBeNull();
-});
+it.each(["number", "date", "datetime-local", "time", "month", "checkbox"])(
+	"returns null for typed input '%s' so it falls back to body",
+	(valueType) => {
+		const file = makeFile("Note.md");
+		const { app } = mountViewWithProperty({
+			key: "count",
+			focus: "value",
+			file,
+			valueType,
+		});
+		expect(getFocusedPropertyTarget(app)).toBeNull();
+	},
+);
🤖 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/utilityObsidian.appendLink-768.test.ts` around lines 92 - 101, The test
case `"returns null for typed inputs (number/date) so they fall back to body"`
currently only validates the "number" valueType, but the production code also
excludes date, datetime, time, month, and checkbox types. Parameterize this test
using a test iteration pattern to cover all excluded types (number, date,
datetime, time, month, checkbox) instead of testing only one type. This ensures
the getFocusedPropertyTarget selector contract is properly guarded for all the
types that should return null and fall back to the body.
🤖 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.

Nitpick comments:
In `@src/utilityObsidian.appendLink-768.test.ts`:
- Around line 92-101: The test case `"returns null for typed inputs
(number/date) so they fall back to body"` currently only validates the "number"
valueType, but the production code also excludes date, datetime, time, month,
and checkbox types. Parameterize this test using a test iteration pattern to
cover all excluded types (number, date, datetime, time, month, checkbox) instead
of testing only one type. This ensures the getFocusedPropertyTarget selector
contract is properly guarded for all the types that should return null and fall
back to the body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27baf57c-0af9-45b0-9ce4-1f16efce512c

📥 Commits

Reviewing files that changed from the base of the PR and between dda12e5 and 80502c9.

📒 Files selected for processing (6)
  • src/IChoiceExecutor.ts
  • src/choiceExecutor.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/utilityObsidian.appendLink-768.test.ts
  • src/utilityObsidian.ts

…ed path

Follow-up to the chhoumann#768 rework, fixing issues from review:

- ChoiceExecutor: replace the latching `focusCaptured` flag with an
  `executionDepth` counter. The flag never reset, so a reused executor
  (e.g. quickAddApi.executeChoice) leaked a stale property target into the
  next top-level call. Now the focused property is captured at depth 0→1 and
  cleared in `finally` when the chain unwinds — nested Multi/Macro keep the
  original caret; independent calls each capture fresh.
- appendLinkToFrontmatterProperty: drop the embed branch and unused
  linkOptions param — an embed (![[…]]) in a YAML value isn't rendered, so it
  was dead/incidental coupling. Property links are always plain links.
- getFocusedPropertyTarget: pin the Obsidian-internals comment to the verified
  version; trim the overstated doc comment.
- Tests updated accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
@Ivikhostrup

Copy link
Copy Markdown
Author

Did a self-review pass and closed the live-verification gaps I'd flagged. Two commits on top (a8d5c5e).

Review fixes

  • Correctness: the focus-capture used a latching flag that never reset, so a reused ChoiceExecutor (e.g. quickAddApi.executeChoice called twice) could leak a stale property target into the next top-level call. Replaced with an executionDepth counter — captured at depth 0→1, cleared in finally when the chain unwinds. Nested Multi/Macro keep the original caret; independent calls capture fresh.
  • Nit: dropped the embed branch + unused linkOptions param in appendLinkToFrontmatterProperty — an embed (![[…]]) in a YAML value isn't rendered, so it was dead coupling. Property links are always plain links now.
  • Pinned the Obsidian-DOM-internals comment to the verified version; trimmed an overstated comment.

Live verification (real Obsidian vault, built plugin, driven via CDP)

Ran the full command flow end-to-end for the paths you asked about:

Path Result
Caret in a text property → run a choice with a {{VALUE}} prompt text_prop: hello [[Captured2]] — link lands in the property even though the prompt opened and stole focus mid-execution (capture-before-UI holds)
Split panes: Other.md active, caret in Host.md's property (non-active leaf) ✅ link written to Host ([[Captured]]), Other untouched — binds to the file owning the focused property, not the active file
Focus moved off the property before the command ✅ property untouched, falls back to body — no corruption
Caret in property KEY field / number / date ✅ excluded → body fallback (no key corruption, no typed-input wipe)

What I couldn't fully automate (being upfront)

  • list/tags real-focus: programmatic/synthetic focus won't land in .multi-select / typed inputs in a headless instance (Obsidian's widget redirects focus to a button), so the list detection→array-push path is covered by the confirmed DOM structure + unit tests, not a live click. Worth a manual click-test on a list property.
  • Command-palette invocation still falls back to body (the palette blurs the property before the choice runs — capturing before QuickAdd's UI can't help once Obsidian's palette has taken focus). This is no worse than today's behavior and non-destructive; hotkey-bound commands work.
  • The body-fallback path itself is unchanged original code (insertLinkWithPlacement reverted byte-for-byte).

CI is green locally (2004 tests, build, lint, svelte-check). Ready for another look.

AI-assisted (Claude). Live checks via CDP against a real dev vault.

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.

[BUG] Append link to current file in properties

3 participants