Skip to content

fix(gui): preserve choice-builder edit position by converting the ChoiceBuilder trio to Svelte (#1130)#1330

Merged
chhoumann merged 8 commits into
masterfrom
chhoumann/1130-modal-refresh-edit-pos
Jun 13, 2026
Merged

fix(gui): preserve choice-builder edit position by converting the ChoiceBuilder trio to Svelte (#1130)#1330
chhoumann merged 8 commits into
masterfrom
chhoumann/1130-modal-refresh-edit-pos

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Fixes #1130.

Problem

The Capture/Template choice-builder modals fully refreshed (contentEl.empty(); display()) on every toggle/dropdown change, resetting scroll position and dropping input focus/caret. Reported still-broken in 2.12.0.

A prior attempt (#1132#1136, an xstate reload machine that snapshotted/restored scroll+focus) was reverted in #1137 — we didn't want that architecture. The Svelte 5 migration (#1248) shipped the mountComponent seam but left these imperative new Setting() builders as backlog.

Approach

Convert the ChoiceBuilder trio (choiceBuilder.ts base + captureChoiceBuilder.ts + templateChoiceBuilder.ts) from imperative Obsidian Setting builders into Svelte 5 runes components mounted through the existing mountComponent seam. Conditional rows become reactive {#if} blocks, so reload() is eliminated — the modal never tears down, so scroll and caret are preserved by construction. No xstate, no scroll-restore band-aid, no new deps.

This is the first concrete slice of the imperative-GUI → Svelte migration; the shared primitives are reusable for the remaining modals.

What changed (5 commits, each independently green)

  1. Shared primitivesSettingItem/Toggle/Dropdown emit Obsidian-exact .setting-item / .checkbox-container / .dropdown markup (zero new CSS, native theming); ValidatedInput (port of validatedInput.ts incl. the validateToken staleness guard); FormatPreviewField (un-debounced, parity); AppendLinkSetting (collapses the duplicated capture/template logic), FileOpeningSetting, OnePageOverrideSetting, OpenFileSetting, ChoiceNameHeader; a use: suggester action.
  2. TemplateChoiceBuilderTemplateChoiceForm.svelte.
  3. CaptureChoiceBuilderCaptureChoiceForm.svelte + section components (CaptureTargetSetting + keyed CanvasNodePicker, WritePositionSetting, InsertAfter/BeforeFields); pure canvas helpers extracted verbatim to canvasNodes.ts.
  4. Base reduced to a thin Svelte mount shell (removes reload() + all add* helpers + the dead addFileSearchInputToSetting).
  5. Review fixes (see below).

Persistence boundary (subtle, load-bearing)

A Svelte $state proxy does not write through to the original object (proxy.js), so ChoiceBuilder.onClose() resolves snapshot(getResultChoice()), and converted builders override getResultChoice() to return the form's proxy (this.formProps.choice) — not this.choice. Snapshotting the original would silently drop every edit. Callers already spread the result, so resolving a plain snapshot is safe.

Adversarial review (Codex) found + fixed two real issues

  • HIGH — newly created choices are class instances (new CaptureChoice() from createChoice()), and $state() does not deeply proxy class instances (only Object/Array prototypes). So in the add-new flow, nested toggles wouldn't reveal conditional rows (existing choices load as plain JSON, which is why it hid). Fix: structuredClone the choice in the form-props factories. Regression test uses a real CaptureChoice instance and fails without the clone.
  • MEDIUMInsertBeforeFields didn't default createIfNotFound/createIfNotFoundLocation (parity gap vs InsertAfterFields), risking a persisted undefined that throws in the capture formatter. Fixed by defaulting at init.

Verification

  • Gates: tsc, eslint, svelte-check 0/0, vitest 1835 pass (incl. new component + persistence + class-instance regression tests), production esbuild build clean.
  • Dev-vault e2e (real Obsidian):
    • Editing an existing Capture choice: scrolled to bottom (scrollTop 556), focused the format textarea with caret mid-string, changed the write-position dropdown (previously reload()'d) → scrollTop stayed 556, focus + caret intact, the dependent section appeared reactively, no errors.
    • Toggle → close → edit persisted to data.json (proves the proxy-snapshot path).
    • Add-new flow: New choice → Capture → fresh builder; toggling "Create file if it doesn't exist" and write-position "After line…" revealed their conditional rows reactively (confirms the class-instance fix).
    • Both builders mount with correct structure/theming and no console errors.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Pick specific canvas nodes as capture targets with capturability checks and helpful messages.
    • Redesigned reactive configuration forms with validated inputs, live format/filename previews, suggesters, and improved conditional sections.
    • New capture controls: write-position selector, insert-before/after fine‑tuning, append-link options, folder/file chooser, one-page override, and file-opening settings (location, split, view, focus).
  • Tests
    • Added unit tests covering form reactivity, validation, and conditional rendering.

…1130)

First slice of the imperative-GUI -> Svelte migration that fixes #1130
(builder modals fully refresh on button clicks, losing scroll/caret).

Adds reactive replacements for the Obsidian Setting builders so conditional
rows live in {#if} blocks instead of forcing reload():
- SettingItem/Toggle/Dropdown: Obsidian-exact .setting-item / .checkbox-container
  / .dropdown markup (zero new CSS, native theming)
- ChoiceNameHeader, OnePageOverrideSetting, OpenFileSetting, FileOpeningSetting,
  AppendLinkSetting (collapses the duplicated capture/template logic)
- ValidatedInput: Svelte port of validatedInput.ts (validateToken staleness
  guard, required/disabled, aria-live hint, suggester use: action)
- FormatPreviewField: un-debounced preview (parity), token-guarded
- suggesterAction.ts use: action; *FormProps factories carrying the
  snapshot-the-proxy persistence contract

Additive only (nothing wired yet). Gates green: tsc, eslint, svelte-check 0/0,
vitest (+14 new component tests).
Replace templateChoiceBuilder's imperative display()/reload() with a mounted
TemplateChoiceForm.svelte: every conditional row (folder sub-tree, append-link
cascade, file-exists mode, file-opening) is now a reactive {#if}, so toggling a
control updates in place instead of tearing down the modal — no lost scroll/caret.

Persistence-blocker fix: a  proxy does not write through to the original
object, so ChoiceBuilder.onClose now resolves snapshot(getResultChoice()), and
converted builders override getResultChoice() to return the form's proxy
(this.formProps.choice). Still-imperative CaptureChoiceBuilder keeps the default
(mutates this.choice in place) and is unaffected.

Tests: TemplateChoiceForm reactive-reveal-without-remount + the persistence
read-back that fails if the original (not the proxy) is snapshotted. Gates green:
tsc, eslint, svelte-check 0/0, vitest 1831 pass.
Replace captureChoiceBuilder's imperative display()/reload() (~13 reload sites)
with a mounted CaptureChoiceForm.svelte composed of section components:
CaptureTargetSetting (+ keyed CanvasNodePicker), WritePositionSetting (precedence
ladder as $derived, mutual-exclusivity zeroing, insert-after/before fields,
canvas-compat notice), InsertAfter/InsertBeforeFields, plus the shared primitives.
Pure canvas helpers extracted verbatim to canvasNodes.ts behind an app seam.

All conditional rows are reactive {#if} over the choice proxy, so toggling a
control updates in place — no modal teardown, no lost scroll/caret. Up-front
normalizeChoice mirrors the imperative render-time defaults (guarded to avoid
persisted-shape drift). Extends the vitest obsidian stub with vault.getFiles/
getAbstractFileByPath.

Tests: write-position mutual-exclusivity + reveal-without-remount, capture-to-
active-file gating, proxy persistence. Gates green: tsc, eslint, svelte-check 0/0,
vitest 1834 pass.
Both subclasses now mount a Svelte form, so delete the imperative base helpers
(reload, addCenteredChoiceNameHeader, addOpenFileSetting, addFileOpeningSetting,
addOnePageOverrideSetting) and the dead addFileSearchInputToSetting, plus their
now-unused imports (Setting, setIcon, GenericTextSuggester, normalizeFileOpening,
promptRenameChoice). The base keeps only the modal lifecycle, the svelteElements
registry, and the snapshot-on-close persistence boundary.

This removes the last reload() in the ChoiceBuilder trio — completing the #1130
fix: the choice builder modals no longer tear down/rebuild on button clicks, so
scroll position and input caret are preserved. Gates green: tsc, eslint,
svelte-check 0/0, vitest 1834 pass, production build clean.
…t insert-before (#1130)

Adversarial review (codex) found two real issues the initial e2e missed (it tested
existing = plain-JSON choices, not freshly-created ones):

- HIGH: createChoice() returns class instances (new CaptureChoice/TemplateChoice),
  and Svelte's proxy() returns non-Object-prototype values UNCHANGED
  (proxy.js:48), so $state(initial) did not deeply proxy a new choice — nested
  toggles wouldn't re-render conditional {#if} rows in the add-new flow. Fix:
  structuredClone the choice in the form-props factories to a plain object, which
  $state proxies deeply (also unifies the snapshot-on-close persistence path).
  Regression test uses a real CaptureChoice instance and fails without the clone.

- MEDIUM: InsertBeforeFields didn't default createIfNotFound/createIfNotFoundLocation
  (InsertAfterFields did), so a legacy/imported insert-before choice could persist
  undefined and throw 'Unknown createIfNotFoundLocation' in the capture formatter
  (and crash on bind:checked={undefined}). Default them synchronously at init.

Gates green: tsc, eslint, svelte-check 0/0, vitest 1835 (+1 regression).
@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: 82d43f67-9e2f-4c95-a601-f339e44ddd8f

📥 Commits

Reviewing files that changed from the base of the PR and between 056b6d5 and 841c7f6.

📒 Files selected for processing (4)
  • src/gui/ChoiceBuilder/components/CanvasNodePicker.svelte
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.svelte
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.test.ts
  • src/gui/components/SettingItem.svelte
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.test.ts
  • src/gui/components/SettingItem.svelte
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.svelte
  • src/gui/ChoiceBuilder/components/CanvasNodePicker.svelte

📝 Walkthrough

Walkthrough

Migrates ChoiceBuilder modals from imperative Obsidian Setting construction to Svelte 5 reactive forms. Adds shared UI primitives (Toggle, Dropdown, SettingItem, ValidatedInput, suggester action), mid-level setting components, canvas node utilities/picker, cloned $state form props for reactive editing, and snapshot-based result resolution; includes tests for behavior and persistence.

Changes

Svelte-Based Choice Builder Refactor

Layer / File(s) Summary
Modal host & snapshot contract
src/gui/ChoiceBuilder/choiceBuilder.ts
Base modal now defines an abstract display mount point and resolves the modal result via snapshot(this.getResultChoice()) on close.
Form props & persistence boundary
src/gui/ChoiceBuilder/captureChoiceFormProps.svelte.ts, src/gui/ChoiceBuilder/templateChoiceFormProps.svelte.ts, src/gui/ChoiceBuilder/templateChoicePersistence.test.ts, src/gui/ChoiceBuilder/templateChoicePersistence.test.ts
Factories deep-clone incoming choice objects and wrap them in Svelte $state so forms can mutate a reactive cloned choice; tests verify snapshot-safe persistence vs plain-object inputs.
Low-level UI component library
src/gui/components/Toggle.svelte, src/gui/components/Dropdown.svelte, src/gui/components/SettingItem.svelte, src/gui/ChoiceBuilder/components/ValidatedInput.svelte, src/gui/ChoiceBuilder/components/suggesterAction.ts, tests
Reusable Svelte primitives: toggle, dropdown, Obsidian-style setting row, validated input with async validation and staleness guards, plus a suggester action to attach Obsidian suggesters.
Suggester action
src/gui/ChoiceBuilder/components/suggesterAction.ts
Action that normalizes suggester factories, attaches suggesters to inputs, and tears them down on unmount.
Mid-level setting components
src/gui/ChoiceBuilder/components/ChoiceNameHeader.svelte, FormatPreviewField.svelte, OpenFileSetting.svelte, FileOpeningSetting.svelte, OnePageOverrideSetting.svelte, AppendLinkSetting.svelte, tests
Components implementing rename header, live format preview, open-file toggle, file-opening configuration, one-page override, and append-link UI with corresponding tests.
Write position and insert controls
src/gui/ChoiceBuilder/components/WritePositionSetting.svelte, InsertBeforeFields.svelte, InsertAfterFields.svelte
Write-position dropdown resets mutually-exclusive placement flags and conditionally renders insert-before/insert-after controls; includes canvas-placement notices and create-if-not-found behaviors.
Canvas node selection & capture-target
src/gui/ChoiceBuilder/canvasNodes.ts, src/gui/ChoiceBuilder/components/CanvasNodePicker.svelte, src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte
Utilities to normalize/resolve .canvas files, parse nodes into picker options, detect active canvas selection; CanvasNodePicker and CaptureTargetSetting integrate to allow selecting capturable canvas nodes.
Capture choice form and tests
src/gui/ChoiceBuilder/CaptureChoiceForm.svelte, src/gui/ChoiceBuilder/CaptureChoiceForm.test.ts, src/gui/ChoiceBuilder/captureChoiceFormProps.svelte.ts, src/gui/ChoiceBuilder/captureChoiceBuilder.ts
Reactive CaptureChoiceForm bound to a cloned $state choice with template suggestions, format suggesters, selection/templater toggles, nested open-file controls; builder mounts form and returns edited snapshot; tests assert reactive UI switching and proxied-state persistence.
Template choice form and tests
src/gui/ChoiceBuilder/TemplateChoiceForm.svelte, src/gui/ChoiceBuilder/TemplateChoiceForm.test.ts, src/gui/ChoiceBuilder/templateChoiceFormProps.svelte.ts, src/gui/ChoiceBuilder/templateChoiceBuilder.ts
TemplateChoiceForm supports template-path validation, filename-format preview, folder chooser with ExclusiveSuggester, behavior/mode selection, open-file wiring; builders mount the form and normalize defaults; tests verify conditional reactive rendering and persistence boundaries.
Builder wiring & normalization
src/gui/ChoiceBuilder/captureChoiceBuilder.ts, src/gui/ChoiceBuilder/templateChoiceBuilder.ts
Builders normalize choice defaults (fileOpening, create-if-not-found, insert defaults), create form props, mount Svelte components, and produce edited choice snapshots via getResultChoice().
Test infrastructure updates
tests/obsidian-stub.ts, multiple *.test.ts files
Extended Obsidian test stub (vault.getFiles, getAbstractFileByPath) and numerous Vitest suites covering components and form behaviors.

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • chhoumann/quickadd#1055: Related to the per-capture "use editor selection as capture value" control wired in CaptureChoiceForm.
  • chhoumann/quickadd#1325: Related changes to template file discovery used by template-path suggestions in the forms.
  • chhoumann/quickadd#1124: Related canvas-target semantics and write-position handling used by the Canvas picker and capture flow.

Suggested labels

released

Poem

🐰 "I hopped through forms of Svelte light,
Replaced the old settings' jolt and fright,
Nodes and templates, previewed with care,
Snapshot safe — no jump, no tear,
A gentle hop, the modal's right."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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 describes the main change: converting ChoiceBuilder components from imperative Obsidian Settings to Svelte while preserving edit position.
Linked Issues check ✅ Passed The PR directly addresses #1130 by converting the choice builders to Svelte 5 components with reactive conditional rendering, eliminating full modal teardowns that caused scroll position loss.
Out of Scope Changes check ✅ Passed All changes are within scope: Svelte component extraction, base class refactoring, shared UI primitives, and canvas node selection—all directly supporting the conversion to fix #1130.

✏️ 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/1130-modal-refresh-edit-pos

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

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6febe11
Status: ✅  Deploy successful!
Preview URL: https://57c92b4f.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1130-modal-refresh.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: d47c659113

ℹ️ 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/gui/ChoiceBuilder/CaptureChoiceForm.svelte
…l-refresh-edit-pos

# Conflicts:
#	src/gui/ChoiceBuilder/captureChoiceBuilder.ts
#	src/gui/ChoiceBuilder/templateChoiceBuilder.ts

@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: 2

🧹 Nitpick comments (4)
src/gui/ChoiceBuilder/components/ValidatedInput.test.ts (2)

15-16: 💤 Low value

Double tick() calls may be unnecessary.

Lines 15-16, 23-24, and 58-59 each call await tick() twice in succession. Typically a single tick() flushes Svelte's pending updates. Unless there's a known multi-tick requirement in this component, consider simplifying to one tick() per wait point.

Also applies to: 23-24, 58-59

🤖 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/gui/ChoiceBuilder/components/ValidatedInput.test.ts` around lines 15 -
16, The test in src/gui/ChoiceBuilder/components/ValidatedInput.test.ts is
calling await tick() twice at multiple wait points (in the blocks around the
first/second and later waits); remove the duplicate calls so each wait point
uses a single await tick() (e.g., change occurrences where there are two
consecutive await tick() calls to a single await tick()), keeping test semantics
the same and running the assertions after the single tick.

47-62: ⚡ Quick win

Staleness guard test could verify intermediate results are dropped.

The test fires two input events ("bad" then "good") and asserts the final hint is empty, but it doesn't verify that the intermediate "Invalid value" message from the "bad" validation was never displayed. Consider adding an assertion after the first input event to confirm the stale result doesn't appear.

🤖 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/gui/ChoiceBuilder/components/ValidatedInput.test.ts` around lines 47 -
62, Update the "keeps only the latest async validation result (staleness guard)"
test in ValidatedInput.test.ts to assert that the intermediate stale validation
result is never shown: after calling fireEvent.input(input) with "bad" (and
awaiting the minimal ticks/promises needed for the async validator to run), read
the hint (container.querySelector(".qa-field-hint")) and assert it does not
equal "Invalid value" (or is empty) before proceeding to set "good" and
asserting the final state; this ensures the validator/ValidatedInput component's
staleness guard drops the intermediate result.
src/gui/ChoiceBuilder/components/FormatPreviewField.svelte (1)

28-31: 💤 Low value

Clarify comment about $derived behavior.

The comment states the formatter is "computed once," but $derived is a reactive primitive that re-runs when dependencies change. While the props (app/plugin/formatterKind) are stable in practice for this component's lifetime, the phrasing "once" might mislead future readers. Consider rephrasing to "app/plugin/kind are stable for this field's lifetime, so $derived computes the formatter exactly once in practice."

🤖 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/gui/ChoiceBuilder/components/FormatPreviewField.svelte` around lines 28 -
31, Update the in-code comment near the $derived usage in
FormatPreviewField.svelte to avoid implying $derived never re-runs: change the
phrasing to state that "app/plugin/formatterKind are stable for this field's
lifetime, so $derived computes the formatter exactly once in practice" (or
similar) and keep the rest of the explanation about why referencing props in a
reactive context is correct; the fix targets the comment surrounding the
$derived declaration (no code changes required).
src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte (1)

38-45: ⚖️ Poor tradeoff

Deduplication pattern is correct but may scale poorly for very large vaults.

Lines 38-45 spread all folder/markdown/canvas paths and format syntax into a new Set, then convert to an array. This works correctly but creates intermediate arrays that could be large in vaults with thousands of files. If performance becomes an issue, consider building the Set iteratively. For typical vault sizes, this is fine.

🤖 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/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte` around lines 38
- 45, The current return builds a Set by spreading folderPaths, markdownPaths,
canvasPaths and FILE_NAME_FORMAT_SYNTAX into an array then de-duplicating, which
allocates large intermediate arrays; instead, create the Set once and add each
source iteratively (e.g., const result = new Set(); for (const p of folderPaths)
result.add(p); for (const p of markdownPaths) result.add(p); for (const p of
canvasPaths) result.add(p); for (const s of FILE_NAME_FORMAT_SYNTAX)
result.add(s); return Array.from(result);) so you avoid the big temporary
combined array while preserving the same deduplication behavior for folderPaths,
markdownPaths, canvasPaths and FILE_NAME_FORMAT_SYNTAX.
🤖 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/gui/ChoiceBuilder/components/CanvasNodePicker.svelte`:
- Around line 95-100: The message shown when selectedOption is missing is
misleading; in CanvasNodePicker update the handling around selectedOption (the
nodeOptions.find(...) that compares option.id to activeSelectionNodeId) to show
an accurate Notice when phase === "ready" and the activeSelectionNodeId is not
present (e.g., "The selected node was not found in the loaded canvas.") instead
of "still loading"; keep the existing loading message only when phase !==
"ready" or when nodeOptions are truly empty, and ensure you reference
selectedOption, nodeOptions, activeSelectionNodeId and phase in the conditional
logic so the correct message is displayed in each case.

In `@src/gui/components/SettingItem.svelte`:
- Around line 37-38: The component currently renders both {`@render` control?.()}
and {`@render` children?.()} which can duplicate content; change the markup so
only one is rendered—preferably render {`@render` control?.()} when control is
provided and otherwise render {`@render` children?.()} (i.e., use a conditional
like control ? {`@render` control?.()} : {`@render` children?.()}) and update the
component comment/docs to state that control takes precedence if both are
supplied.

---

Nitpick comments:
In `@src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte`:
- Around line 38-45: The current return builds a Set by spreading folderPaths,
markdownPaths, canvasPaths and FILE_NAME_FORMAT_SYNTAX into an array then
de-duplicating, which allocates large intermediate arrays; instead, create the
Set once and add each source iteratively (e.g., const result = new Set(); for
(const p of folderPaths) result.add(p); for (const p of markdownPaths)
result.add(p); for (const p of canvasPaths) result.add(p); for (const s of
FILE_NAME_FORMAT_SYNTAX) result.add(s); return Array.from(result);) so you avoid
the big temporary combined array while preserving the same deduplication
behavior for folderPaths, markdownPaths, canvasPaths and
FILE_NAME_FORMAT_SYNTAX.

In `@src/gui/ChoiceBuilder/components/FormatPreviewField.svelte`:
- Around line 28-31: Update the in-code comment near the $derived usage in
FormatPreviewField.svelte to avoid implying $derived never re-runs: change the
phrasing to state that "app/plugin/formatterKind are stable for this field's
lifetime, so $derived computes the formatter exactly once in practice" (or
similar) and keep the rest of the explanation about why referencing props in a
reactive context is correct; the fix targets the comment surrounding the
$derived declaration (no code changes required).

In `@src/gui/ChoiceBuilder/components/ValidatedInput.test.ts`:
- Around line 15-16: The test in
src/gui/ChoiceBuilder/components/ValidatedInput.test.ts is calling await tick()
twice at multiple wait points (in the blocks around the first/second and later
waits); remove the duplicate calls so each wait point uses a single await tick()
(e.g., change occurrences where there are two consecutive await tick() calls to
a single await tick()), keeping test semantics the same and running the
assertions after the single tick.
- Around line 47-62: Update the "keeps only the latest async validation result
(staleness guard)" test in ValidatedInput.test.ts to assert that the
intermediate stale validation result is never shown: after calling
fireEvent.input(input) with "bad" (and awaiting the minimal ticks/promises
needed for the async validator to run), read the hint
(container.querySelector(".qa-field-hint")) and assert it does not equal
"Invalid value" (or is empty) before proceeding to set "good" and asserting the
final state; this ensures the validator/ValidatedInput component's staleness
guard drops the intermediate result.
🪄 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: 8f15251c-22a2-49d0-b5aa-bdc0a008f6e1

📥 Commits

Reviewing files that changed from the base of the PR and between 2b93029 and d47c659.

📒 Files selected for processing (33)
  • src/gui/ChoiceBuilder/CaptureChoiceForm.svelte
  • src/gui/ChoiceBuilder/CaptureChoiceForm.test.ts
  • src/gui/ChoiceBuilder/TemplateChoiceForm.svelte
  • src/gui/ChoiceBuilder/TemplateChoiceForm.test.ts
  • src/gui/ChoiceBuilder/canvasNodes.ts
  • src/gui/ChoiceBuilder/captureChoiceBuilder.ts
  • src/gui/ChoiceBuilder/captureChoiceFormProps.svelte.ts
  • src/gui/ChoiceBuilder/choiceBuilder.ts
  • src/gui/ChoiceBuilder/components/AppendLinkSetting.svelte
  • src/gui/ChoiceBuilder/components/AppendLinkSetting.test.ts
  • src/gui/ChoiceBuilder/components/CanvasNodePicker.svelte
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte
  • src/gui/ChoiceBuilder/components/ChoiceNameHeader.svelte
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.svelte
  • src/gui/ChoiceBuilder/components/FileOpeningSetting.test.ts
  • src/gui/ChoiceBuilder/components/FormatPreviewField.svelte
  • src/gui/ChoiceBuilder/components/InsertAfterFields.svelte
  • src/gui/ChoiceBuilder/components/InsertBeforeFields.svelte
  • src/gui/ChoiceBuilder/components/OnePageOverrideSetting.svelte
  • src/gui/ChoiceBuilder/components/OpenFileSetting.svelte
  • src/gui/ChoiceBuilder/components/ValidatedInput.svelte
  • src/gui/ChoiceBuilder/components/ValidatedInput.test.ts
  • src/gui/ChoiceBuilder/components/WritePositionSetting.svelte
  • src/gui/ChoiceBuilder/components/suggesterAction.ts
  • src/gui/ChoiceBuilder/templateChoiceBuilder.ts
  • src/gui/ChoiceBuilder/templateChoiceFormProps.svelte.ts
  • src/gui/ChoiceBuilder/templateChoicePersistence.test.ts
  • src/gui/components/Dropdown.svelte
  • src/gui/components/SettingItem.svelte
  • src/gui/components/SettingItem.test.ts
  • src/gui/components/Toggle.svelte
  • src/gui/components/Toggle.test.ts
  • tests/obsidian-stub.ts

Comment thread src/gui/ChoiceBuilder/components/CanvasNodePicker.svelte
Comment thread src/gui/components/SettingItem.svelte Outdated
- Normalize fileOpening at FileOpeningSetting init (Codex P2): an imported/legacy
  choice with openFile:false and no fileOpening object threw on toggling Open
  (FileOpeningSetting dereferenced .location/.mode). Now normalized at the point
  the imperative addFileOpeningSetting did. Regression test added.
- SettingItem renders (control ?? children) instead of both (CodeRabbit) — avoids
  duplicate content if both slots are ever passed.
- CanvasNodePicker shows an accurate 'selected node not found' message once nodes
  are loaded instead of 'still loading' (CodeRabbit).

Gates green: tsc, eslint, svelte-check 0/0, vitest.
@chhoumann

Copy link
Copy Markdown
Owner Author

Addressed review feedback in 841c7f6:

  • Codex P2 (file-opening normalize): FileOpeningSetting now normalizes fileOpening at init, so an imported/legacy choice with openFile:false and no fileOpening object no longer throws when Open is toggled on (same point the imperative addFileOpeningSetting normalized). Regression test added. 👍
  • CodeRabbit (SettingItem): renders (control ?? children) instead of both slots.
  • CodeRabbit (CanvasNodePicker): shows an accurate "selected node was not found" message once nodes are loaded, instead of "still loading".

Gates green: tsc, eslint, svelte-check 0/0, vitest 1884.

@chhoumann chhoumann merged commit 2681dc3 into master Jun 13, 2026
9 checks passed
@chhoumann chhoumann deleted the chhoumann/1130-modal-refresh-edit-pos branch June 13, 2026 19:37
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] UX Improvement: Modal completely refreshes on button clicks, causing loss of edit position

1 participant