Skip to content

fix: the file-name preview stops promising names Obsidian will refuse - #1595

Merged
chhoumann merged 5 commits into
masterfrom
chhoumann/1578-preview-followups
Jul 27, 2026
Merged

fix: the file-name preview stops promising names Obsidian will refuse#1595
chhoumann merged 5 commits into
masterfrom
chhoumann/1578-preview-followups

Conversation

@chhoumann

@chhoumann chhoumann commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Closes #1578, closes #1579, closes #1580.

The third round of the preview-truth cluster, after #1582 (#1563/#1564). One
sentence: the file-name preview row shows the name that will be created, and
says so when Obsidian will refuse it.


The problems

#1578 - the preview showed names Obsidian refuses. Set a Template choice's
"File name format" to Bad: {{VALUE:title}} and the builder previewed
Bad: Example Title in ordinary styling. Running the choice created nothing.

$ pnpm run obsidian:e2e -- quickadd:run 'choice=Repro 1578' 'value-title=My Note' verify=true
{"ok":false,"error":"Choice execution failed; no file was created.", ...}
# Notice: QuickAdd: (ERROR) Could not create file with template at Bad: My Note.md:
#         File name cannot contain any of the following characters: \ / :

#1579 - the {{FIELD:}} placeholder repeated the filter syntax back at you.
{{FIELD:status|folder:Work}} previewed status|folder:Work_field_value. The
placeholder was built from the token's whole inner text, so the more precisely
you filtered, the less the preview looked like a value.

#1580 - fileNameDisplayFormatter.test.ts tested a mock of itself. It defined
a TestFileNameDisplayFormatter with a hand-written format() of a dozen regex
replaces and asserted that those regexes did what they said. Eleven green tests
that never imported the real class.

What ships

1. The test file is pinned against the real formatter (#1580)

Done first, because it protects the other two. One case per token against the
real class. It immediately caught two lies the mock had been hiding, filed as
#1587 ({{MVALUE}} previews literally while the run opens the math modal)
and #1588 ({{title}} previews as a name although formatFileName throws on
it), both pinned here as current behaviour with their issue number.

2. {{FIELD:}} names the field (#1579)

replaceFieldVarInString already parsed the specifier one line above the call,
so the parsed name is handed to suggestForField. The variable KEY stays keyed
on the whole specifier - two {{FIELD:status}} tokens with different filters are
different prompts, and a test pins that.

Also fixes the fallback beside it: getVariableValue was called with the bare
specifier instead of the FIELD:-prefixed key, so on the one path where a
suggester resolves undefined (a remote prompt provider can) it both missed the
stored value and cross-read the {{VALUE}} namespace - a {{VALUE:status}}
answer could be served to a {{FIELD:status}} token.

Declined: showing a real vault value the way {{FILE:}} does. FIELD
prompts at run time, so a concrete value asserts a pick that has not happened,
it changes as the filter narrows the candidate set, and it costs a vault scan per
keystroke. status_field_value is what the issue says the preview means to say.

3. An error diagnostic for a name Obsidian will refuse (#1578)

Same channel as the ./.. diagnostic from #1563, so the row flips to
"Unresolved:".

The character set is : and only :, measured against vault.create /
vault.createFolder on Obsidian 1.13.0 (macOS), one candidate per name:

char result
: throws Obsidian's own guard, for files and folder segments
\ / separators; QuickAdd creates the parent folder, so both are legal
* ? " < > | ^ [ ] #, tab create successfully
NUL, control chars already collapsed by the #1563 normalizer

So copying the stricter set from TemplateEngine.validateFolderSegment
(\ / : * ? " < > |) would have rejected names Obsidian makes without
complaint.

The check reads the finished name, not the format string. That is the only
place all the sources meet: a colon the author typed, one {{TIME}} produced (it
is HH:mm, and QuickAdd's own autocomplete offers it in this field), one a
{{GLOBAL_VAR:}} snippet or an included {{TEMPLATE:}} body carried in, and one
left behind by a token that never matched - {{TEMPLATE:Naming}} without the
extension is not a token, so the literal text goes to the vault. An earlier draft
scanned the format string with {{...}}-shaped spans masked; a mask is blind to
exactly that last case, which is the most likely {{TEMPLATE:}} typo.

Reading the finished name only works if the preview stops writing text that is
not part of the name, so two stand-ins were corrected first:

  • the VDATE (default: X) / (optional) hints are gone from the file-name
    preview. The run splices in the formatted date and nothing else, so
    2026-07-27 (default: tomorrow) was already a name that could not exist - and
    its colon would have been blamed on the author. The hints stay on the body
    preview and in the run's own prompt placeholder.
  • an inline {{VALUE:a,b}} option list previews the option, without the body
    preview's (N options) count.

Four guards keep it from crying wolf, each with a test:

  1. a pass that already reported an error says nothing more - all four
    [QuickAdd: ...] placeholders carry a colon and each already named its real
    problem;
  2. an unterminated {{ means the author is mid-token with the format suggester
    open;
  3. an unterminated ```js quickadd fence means they are mid-script - a
    script holding {a: 1} or "HH:mm" would otherwise turn the row red on every
    pause;
  4. stand-ins that echo a token's own argument (a {{VALUE:}} prompt header, a
    macro name, a field name) degrade to a neutral placeholder rather than
    inventing a character the run never produces. A stand-in is fiction either
    way; fiction that could not be a real file name is worse fiction.

Before / after

Bad: {{VALUE:title}} in a Template choice's "File name format":

before after
before after

{{DATE:YYYY-MM-DD}} {{TIME}} - the author typed no colon at all, and {{TIME}}
is offered by QuickAdd's own token autocomplete in this very field:

before after
before after

{{FIELD:status|folder:Work}} (#1579):

before after
before after

Tradeoffs and things deliberately not done

The run is unchanged. The issue asks whether it should refuse earlier, and it
should: createFileWithInput creates the target folder and createFileWithTemplate
formats the entire template body - real prompts, macros, and js quickadd fences
with their side effects - before Obsidian rejects the name. But a hard throw
would break a working setup, because : is legal on macOS/Linux at the
filesystem level: a note created outside Obsidian can have one, and capturing to
it works today. The guard has to be "reject only if the file does not already
exist". Filed as #1591 with that design, and the rule lives in
src/utils/generatedFilePath.ts so the run can share it without drift.

Capture targets. FileNameDisplayFormatter previews FILE NAMES, so it reports
the colon in property:status=done too - correctly, because the same class
previews a Template choice's file name, where that literal IS a path. Teaching a
file-name formatter capture semantics would be the wrong layer, so the SURFACE
gates it: CaptureTargetSetting renders no preview row for picker syntax. That
gate had no test; it has one now, including the case an adversarial review found -
a target written as {{GLOBAL_VAR:inbox}} that expands to property:type=draft
passed the raw-text gate and got a red error for a capture that runs fine.
FormatPreviewField now takes a hideWhen predicate over the resolved text.

No Windows portability warning. * ? " < > | fail on Windows at the
filesystem level but work everywhere else, so flagging them would fire on every
? in a title for the mac/linux majority - the per-keystroke noise #1558
removed. Platform.isWin would make it truthful per platform; not attempted from
an untestable platform.

"Unresolved:" is not the right label for a name that resolved and is merely
invalid. It is pre-existing (#1563's ./.. errors have the same shape) and the
fix is a copy decision across three preview surfaces, so it is filed as #1594
rather than folded in.

An existing file is exempt. A note created outside Obsidian on macOS/Linux
can carry a :, and a capture pointed at it appends without ever calling
vault.create. The diagnostic returns early when a file is already at that path
(tolerant of the missing extension, since a file-name format produces the name
and the engine appends .md).

Known limitation, documented in the code: a fence carried in by a
{{GLOBAL_VAR:}} snippet is stripped from the scan although the run would keep it
as literal text. Mapping spans back through the passes is not worth it for a
script inside a global variable inside a file name, and the failure is silence
rather than a wrong accusation.

Adjacent defects filed rather than folded in

#1587, #1588, #1589 (a {{VDATE:}} with no format previews the raw token while
the run defaults to YYYY-MM-DD, or YYYY-MM-DD HH:mm under |time - which is
another colon), #1590 (the one-page modal's file-name preview discards every
diagnostic and never sets the target folder), #1591, #1594.

Validation

Design reviewed by four adversarial lenses before any code was written; the
resolved-string approach here replaced my original literal-text design because
three of them independently showed it was blind to {{TIME}}. The shipped diff
then went through a second adversarial pass with per-finding verification - the
{{GLOBAL_VAR:}} capture-target false positive, the mid-script false positive,
the collapsed message variants and the wrong {{MATH:}} pin all come from it.

  • pnpm test 4349 passed, 37 skipped
  • pnpm lint, pnpm run check (0 errors), pnpm run build clean
  • Every screenshot and every measurement above taken in this worktree's own
    isolated Obsidian 1.13.0 vault via pnpm run obsidian:e2e
  • Mutation-checked: reverting the gate in CaptureTargetSetting.svelte,
    the hideWhen wiring, the FIELD stand-in and the {{RANDOM:}} pass each fail
    the tests that claim to cover them
  • Two P2 findings from the Codex reviewer on the opened PR are fixed in
    7d79bd2, each with a test and a live check: the existing-file exemption
    above, and hideWhen swallowing an unrelated error. The latter is why preview
    diagnostics now carry an optional kind, where "path" marks the problems
    that say the RESULT is not a usable path - the only ones a host that knows the
    field may not be a path at all should discard. [BUG] The preview row says "Unresolved:" for a name that resolved perfectly well #1594 wants the same split.

fileNameDisplayFormatter.test.ts defined its own TestFileNameDisplayFormatter -
a dozen hand-written regex replaces - and asserted that those regexes did what
they said. Eleven green tests that never imported FileNameDisplayFormatter and
could not fail for any change to it.

They had also drifted into asserting behaviour the plugin cannot produce:
{{TEMPLATE:daily-note}} was pinned to a fabricated
'[daily-note template content...]' placeholder that #1560 deleted and #1563
replaced with a real inert read.

Replaced with one case per token against the real class. Two of them pin
CURRENT behaviour that the mock claimed otherwise for, each with the issue it
is filed as: {{MATH:}} is left literal by the preview while the run prompts
for it (#1587), and {{title}} previews as a name although formatFileName
throws on it (#1588).
…1579)

Both preview formatters built the placeholder out of the token's whole inner
text, filters included, so the more precisely you filtered the less the preview
looked like a value: {{FIELD:status|folder:Work}} previewed
'status|folder:Work_field_value'. The field is status; at run time the token
resolves to one of that property's values.

replaceFieldVarInString already parsed the specifier one line above the call,
so the parsed field name is handed to suggestForField. The variable KEY stays
keyed on the whole specifier - two {{FIELD:status}} tokens with different
filters are different prompts.

Also fixes the fallback beside it: getVariableValue was called with the bare
specifier instead of the FIELD-prefixed key, so on the one path where a
suggester resolves undefined (a remote prompt provider can) it both missed the
value that had been stored and cross-read the {{VALUE}} namespace - a
{{VALUE:status}} answer could be served to a {{FIELD:status}} token.
…1578)

'Bad: {{VALUE:title}}' previewed 'Bad: Example Title' in the ordinary
'Preview:' styling, and running the choice created nothing - the Notice was
Obsidian's own: 'File name cannot contain any of the following characters:
\\ / :'. #1563 made this row mirror the run's name normalizer; a character
Obsidian refuses is the same class of truth and the last one missing.

Measured against vault.create/createFolder on Obsidian 1.13.0 (macOS), one
candidate character per name: ':' throws Obsidian's own guard for files AND
folder segments; '* ? " < > | ^ [ ] #' and tab all create successfully, so
copying the stricter set from TemplateEngine.validateFolderSegment would
reject names Obsidian makes without complaint; '\\' and '/' are separators
and QuickAdd creates the parent folder. The rule is ':' and only ':'.

The check reads the FINISHED name rather than the format string. That is the
only place all the sources meet: a colon the author typed, one {{TIME}}
produced (it is HH:mm, and the token autocomplete offers it in this field),
one a global snippet or an included template body carried in, and one left
behind by a token that never matched - {{TEMPLATE:Naming}} without the
extension is not a token, so the literal text goes to the vault, and a mask
over {{...}}-shaped spans would be blind to exactly that, the most likely
{{TEMPLATE:}} typo.

Reading the finished name only works if the preview stops writing text that
is not part of the name, so two stand-ins were corrected first:

- the VDATE '(default: X)' / '(optional)' hints are gone from the file-name
  preview. The run splices in the formatted date and nothing else, so
  '2026-07-27 (default: tomorrow)' was already a name that could not exist -
  and the colon in it would have been blamed on the author. The hints stay on
  the body preview and in the run's own prompt placeholder.
- an inline {{VALUE:a,b}} option list previews the option, without the body
  preview's ' (N options)' count.

Three guards keep it from crying wolf: a pass that already reported an error
says nothing more (all four [QuickAdd: ...] placeholders carry a colon and
each already named its real problem); an unterminated '{{' means the author is
mid-token with the format suggester open; and inline 'js quickadd' fences are
excluded, since the run replaces a fence with what the script RETURNS while
the preview must leave the source verbatim.

Stand-ins that echo a token's own argument - a {{VALUE:}} prompt header, a
macro name, a field name - degrade to a neutral placeholder when they would
otherwise invent one of these characters. A stand-in is fiction either way;
fiction that could not be a real file name is worse fiction.

The run is deliberately unchanged. Failing fast at the create sinks would
help - the folder is created and the template body formatted (prompts,
macros, script fences) before Obsidian rejects the name - but a hard throw
would also break appending to a colon-named file that already exists, which
is legal on macOS/Linux. Filed separately.

The formatter reports the colon for capture-target syntax like
'property:status=done' too, because it previews FILE NAMES and the same class
previews a Template choice's file name, where that literal IS a path.
CaptureTargetSetting renders no preview row for recognised picker syntax, and
that gate is now pinned by a component test instead of a comment.
Three lenses attacked the shipped diff and a verifier reproduced each finding.

- The capture target's picker-syntax gate read the RAW field, but the run
  resolves the target's format tokens BEFORE parsing it. So a target written as
  {{GLOBAL_VAR:inbox}} expanding to 'property:type=draft' passed the gate, was
  previewed as a path, and got a red 'cannot contain ":"' for a capture that
  runs perfectly well. FormatPreviewField takes a hideWhen predicate over the
  RESOLVED text, and CaptureTargetSetting asks the same parser again.

- The two message variants are one. Splitting on 'is the colon anywhere in the
  format string' sounds right and is not: every argument-bearing token carries
  a colon in its own syntax, so {{DATE:YYYY-MM-DD}} {{TIME}} - the shape where
  the hint is needed most - was told the author could see it, and only a format
  whose tokens take no argument at all reached the other variant. One sentence
  names both sources.

- A half-typed 'js quickadd' fence now suppresses the diagnostic the way a
  half-typed {{ does. Until the closing backticks exist there is no span to
  strip, so a script holding {a: 1} or "HH:mm" turned the row red on every
  pause. hasUnterminatedInlineScriptFence lives beside findInlineScriptSpans
  and shares its opener rules.

- The {{MATH:}} pin from the #1580 commit was itself the fiction #1580 exists
  to delete: {{MATH:...}} is not a token (MATH_VALUE_REGEX is /{{MVALUE}}/i),
  so preview and run agree on it. The case now pins {{MVALUE}}, which is the
  real divergence, and {{MATH:1+1}} is kept as plain text with the reason.
  Issue #1587 was corrected to match.

- The picker-preview component test asserted an absence that could not fail
  (diagnostics wait for 500ms of stillness). It now advances the clock and
  carries a positive control, plus the token-expansion case above.

- Added the {{TIME}}, {{FILE:}}, {{FILENAMECURRENT}} and {{GLOBAL_VAR:}} cases
  the rewritten test file's docstring claimed, and gave its formatter the
  target folder every real caller sets.

Accepted and documented rather than fixed: a fence carried in by a
{{GLOBAL_VAR:}} snippet is stripped from the scan although the run would keep
it as literal text - mapping spans back through the passes is not worth it for
a script inside a global variable inside a file name, and the failure is
silence rather than a wrong accusation.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The preview pipeline now classifies illegal filename characters, handles incomplete syntax, uses safer stand-ins, scopes FIELD placeholders to parsed names, exercises the real filename formatter, and hides picker-target previews.

Changes

Preview behavior

Layer / File(s) Summary
Filename validation and safe previews
src/utils/generatedFilePath.ts, src/formatters/previewDiagnostics.ts, src/formatters/formatter.ts, src/formatters/fileNameDisplayFormatter.ts, src/formatters/helpers/previewHelpers.ts, src/formatters/*test.ts
Illegal path characters, incomplete tokens, script fences, safe stand-ins, VDATE output, and diagnostic classifications are updated and tested.
FIELD placeholder scoping
src/formatters/formatter.ts, src/formatters/formatDisplayFormatter.ts, src/formatters/fileNameDisplayFormatter.ts, src/formatters/helpers/previewHelpers.ts, src/formatters/displayFormatters-1579-field-placeholder.test.ts
FIELD previews use parsed field names while variable keys retain complete filtered specifiers.
Real formatter regression coverage
src/formatters/fileNameDisplayFormatter.test.ts
Tests instantiate FileNameDisplayFormatter and cover resolved and unresolved token behavior.
Picker-target preview gating
src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte, src/gui/ChoiceBuilder/components/FormatPreviewField.svelte, src/gui/ChoiceBuilder/components/*1578-picker-preview.test.ts
Resolved picker syntax hides the path preview while ordinary paths and invalid-path diagnostics remain visible.

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

Possibly related issues

Possibly related PRs

Poem

Colons flee from filenames bright,
FIELDs keep filters out of sight.
Picker paths hide when syntax shows,
While safer previews softly glow.
Tests hop onward, row by row.

🚥 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
Linked Issues check ✅ Passed The changes satisfy [#1578], [#1579], and [#1580]: invalid-name diagnostics, FIELD preview parsing, and real formatter tests all align with the linked issues.
Out of Scope Changes check ✅ Passed The extra preview-hiding and helper changes stay within the same file-name preview work and do not look unrelated to the linked issues.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: file-name previews now avoid suggesting names Obsidian will reject.
✨ 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/1578-preview-followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7d79bd2
Status: ✅  Deploy successful!
Preview URL: https://b396b78f.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1578-preview-follo.quickadd.pages.dev

View logs

@chhoumann
chhoumann marked this pull request as ready for review July 27, 2026 11:04

@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: 8b51133d9c

ℹ️ 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/components/FormatPreviewField.svelte Outdated
Comment thread src/formatters/fileNameDisplayFormatter.ts Outdated

@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/formatters/fileNameDisplayFormatter.test.ts (1)

27-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared FileNameDisplayFormatter test scaffolding into a common helper module. Five spec files independently redefine near-identical App stubs, a makeFormatter() factory that constructs FileNameDisplayFormatter and calls setTargetFolderPath("Folder/Name"), and a preview() wrapper returning { text/out, diagnostics/problems } — one root cause (no shared test-support module for this formatter) repeated five times.

  • src/formatters/fileNameDisplayFormatter.test.ts#L27-L81: the most complete instance (full makeApp/plugin/makeFormatter/preview) — move this into a shared fileNameDisplayFormatter.test-helpers.ts (or similar) and import it here.
  • src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts#L61-L102: replace the local makeApp/makeFormatter/preview with the shared helper, parameterizing the small App-stub differences (e.g. active-file shape) if needed.
  • src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts#L22-L30: replace local makeFormatter/preview with the shared helper.
  • src/formatters/fileNameDisplayFormatter-1563-template.test.ts#L88-L90: replace local makeFormatter with the shared helper.
  • src/formatters/fileNameDisplayFormatter.audit-cleanup.test.ts#L44-L52: replace local mockApp/makeFormatter with the shared helper.
🤖 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/formatters/fileNameDisplayFormatter.test.ts` around lines 27 - 81,
Extract the shared App stub, plugin fixture, makeFormatter, and preview
scaffolding from fileNameDisplayFormatter.test.ts into a common
fileNameDisplayFormatter test-helpers module, parameterizing minor App
differences as needed. Update
src/formatters/fileNameDisplayFormatter.test.ts#L27-L81,
src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts#L61-L102,
src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts#L22-L30,
src/formatters/fileNameDisplayFormatter-1563-template.test.ts#L88-L90, and
src/formatters/fileNameDisplayFormatter.audit-cleanup.test.ts#L44-L52 to import
and use the shared helpers, including replacing audit-cleanup’s
mockApp/makeFormatter.
🤖 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/formatters/fileNameDisplayFormatter.test.ts`:
- Around line 27-81: Extract the shared App stub, plugin fixture, makeFormatter,
and preview scaffolding from fileNameDisplayFormatter.test.ts into a common
fileNameDisplayFormatter test-helpers module, parameterizing minor App
differences as needed. Update
src/formatters/fileNameDisplayFormatter.test.ts#L27-L81,
src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts#L61-L102,
src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts#L22-L30,
src/formatters/fileNameDisplayFormatter-1563-template.test.ts#L88-L90, and
src/formatters/fileNameDisplayFormatter.audit-cleanup.test.ts#L44-L52 to import
and use the shared helpers, including replacing audit-cleanup’s
mockApp/makeFormatter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c77bce6-ee4b-40f7-ba3f-9c122d33b37f

📥 Commits

Reviewing files that changed from the base of the PR and between 234a349 and 8b51133.

📒 Files selected for processing (14)
  • src/formatters/displayFormatters-1579-field-placeholder.test.ts
  • src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts
  • src/formatters/fileNameDisplayFormatter-1563-template.test.ts
  • src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts
  • src/formatters/fileNameDisplayFormatter.audit-cleanup.test.ts
  • src/formatters/fileNameDisplayFormatter.test.ts
  • src/formatters/fileNameDisplayFormatter.ts
  • src/formatters/formatDisplayFormatter.ts
  • src/formatters/formatter.ts
  • src/formatters/helpers/previewHelpers.ts
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting-1578-picker-preview.test.ts
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting.svelte
  • src/gui/ChoiceBuilder/components/FormatPreviewField.svelte
  • src/utils/generatedFilePath.ts

- An existing file is never created, so Obsidian is never asked to accept its
  name. ':' is legal on macOS/Linux at the filesystem level, so a note made
  outside Obsidian really can carry one, and a capture pointed at it appends
  (CaptureChoiceEngine takes the fileExists branch and never reaches
  vault.create) - the diagnostic was marking a supported configuration broken.
  The vault lookup is defensive because it runs outside format()'s try/catch.

- The capture target's hideWhen gate swallowed unrelated errors. A pass can
  fail and still leave text that parses as picker syntax:
  {{GLOBAL_VAR:inbox}}{{TEMPLATE:missing.md}} resolves to 'property:type=draft'
  followed by a not-found placeholder, and the run aborts on the missing
  template - hiding the row took the one message that explained it. Preview
  diagnostics now carry an optional kind, and 'path' marks the problems that
  say the RESULT is not a usable path (the normalizer's and this one). Those
  are the only ones a host that knows the field may not be a path at all is
  entitled to discard. #1594 wants the same split for the 'Unresolved:' label.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts (1)

92-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Retain coverage for unrelated diagnostics in the special-form loop.

The loop now checks only preview output, so unexpected non-path formatter errors for other capture-target forms would pass silently. Keep the explicit property:status=done assertion, but also assert that each case has no diagnostics or only kind: "path" diagnostics.

As per coding guidelines, bug fixes must add or adjust unit regression tests.

🤖 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/formatters/fileNameDisplayFormatter-1563-normalize.test.ts` around lines
92 - 118, Update the special-form test loop around the preview assertions to
validate diagnostics as well as output: for every capture-target case, require
that problems are absent or that every diagnostic has kind "path". Retain the
explicit property:status=done assertion unchanged, and add or adjust the unit
regression coverage without changing formatter behavior.

Source: Coding guidelines

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

Outside diff comments:
In `@src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts`:
- Around line 92-118: Update the special-form test loop around the preview
assertions to validate diagnostics as well as output: for every capture-target
case, require that problems are absent or that every diagnostic has kind "path".
Retain the explicit property:status=done assertion unchanged, and add or adjust
the unit regression coverage without changing formatter behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9742b01a-270c-467c-9f39-2b682301ecda

📥 Commits

Reviewing files that changed from the base of the PR and between 8b51133 and 7d79bd2.

📒 Files selected for processing (7)
  • src/formatters/fileNameDisplayFormatter-1563-normalize.test.ts
  • src/formatters/fileNameDisplayFormatter-1563-template.test.ts
  • src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts
  • src/formatters/fileNameDisplayFormatter.ts
  • src/formatters/previewDiagnostics.ts
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting-1578-picker-preview.test.ts
  • src/gui/ChoiceBuilder/components/FormatPreviewField.svelte
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/formatters/fileNameDisplayFormatter-1563-template.test.ts
  • src/gui/ChoiceBuilder/components/FormatPreviewField.svelte
  • src/gui/ChoiceBuilder/components/CaptureTargetSetting-1578-picker-preview.test.ts
  • src/formatters/fileNameDisplayFormatter-1578-illegal-chars.test.ts
  • src/formatters/fileNameDisplayFormatter.ts

@chhoumann
chhoumann merged commit 42f254f into master Jul 27, 2026
14 checks passed
@chhoumann
chhoumann deleted the chhoumann/1578-preview-followups branch July 27, 2026 14:44
chhoumann added a commit that referenced this pull request Jul 27, 2026
…red value

Two ways the preview disagreed with the run about the same token.

1. A format-less {{VDATE:due}} previewed as the raw token, while the run
supplies YYYY-MM-DD (or YYYY-MM-DD HH:mm under |time) and creates the note.
The bail-out narrows from "no name OR no format" to "no name", which is the
run's own rule.

That token's own text contains a colon, so the illegal-character diagnostic
added in #1595 was firing on it: a working {{VDATE:due}} showed a red "a file
or folder name cannot contain :" under a choice that creates 2026-07-27.md.
Rendering the date deletes the false accusation and, on {{VDATE:due|time}},
lets the true one appear - the run's datetime default really does put HH:mm in
the file name, and Obsidian really does refuse it.

2. Neither preview read an ANSWERED date. The one-page input form seeds the
user's real picks into the formatter before computing the preview, and VDATE
was the only seeded requirement type ignored - so the row showed today beside
the date just chosen. Both formatters now resolve a stored value through
renderStoredDateVariable, extracted from the run's own tail so there is one
implementation and not three. Answered-empty ("") stays empty; an unparseable
seeded string keeps the verbatim back-compat branch; only `undefined` falls
through to the example date.

Both previews also stop throwing on a half-typed |startof: unit, which blanked
the row and reddened it between two keystrokes (#1558's failure mode).

Closes #1589.
chhoumann added a commit that referenced this pull request Jul 27, 2026
Five things the review caught, each measured.

The one-page form previewed an untouched required {{VDATE:}} as answered-empty.
submit() deliberately OMITS a required blank date so the sequential date prompt
still fires, while updatePreviews copied this.result wholesale - so the row
showed a name the run would never create. Both now go through one
collectAnswers(), which is where the rule already lived (it needs
dateParseErrors, which only the modal has).

parseVDateOptionsForPreview swallowed the error as well as the throw. Master's
body preview reported `Unknown date unit "we"` for a permanent typo; this
branch rendered a finished date and said nothing, for a format the run aborts
on - while {{DATE:...|startof:we}} in the same pass still reported it, so the
row contradicted itself. The options and the error are now separate channels:
the text stays stable while the unit is half-typed, the complaint goes on the
diagnostics channel the builder already holds back for 500ms.

The example date now applies |startof:/|endof: too. #1595 left snap out on the
grounds that snapping only the file-name row would split it from the body row;
both rows do it now, so that reason is gone - and the alternative was an
asymmetry ten lines wide, because the answered branch beside it snaps and
{{DATE:...|startof:month}} in the same pass always has. Only a token that asks
for a snap touches moment, so a dateless preview keeps its old path.

existsInVault's bare-name branch is no longer TFile-only. That tightening
reddened a working capture target: captureTargetResolution resolves a bare
extension-less name to a folder SCOPE when a folder of that name exists with no
note beside it, and the picker then writes to a file inside it. The probe
mirrors that rule instead of approximating it, and the trailing-slash case it
was contradicting stays fixed.

Three of the new tests were vacuous, proved by mutation:
- |startof:w does not throw (w is a real alias for week), so neither
  parseVDateOptionsForPreview test entered the catch. Now |startof:we, plus a
  case pinning that a valid short alias stays quiet.
- The capture guard's test passed with the guard deleted. Its placement is what
  matters, so it now asserts the heading picker never opened - that fails when
  the guard moves to the sink.
- The qa-hidden assertion only checked the true half, which the constructor
  already satisfies.
Adds the missing coverage the review named: the body preview's {{MVALUE}} pass
(deleting it left the suite green), and the first-pass ordering in the one-page
modal.
chhoumann added a commit that referenced this pull request Jul 27, 2026
…red value

Two ways the preview disagreed with the run about the same token.

1. A format-less {{VDATE:due}} previewed as the raw token, while the run
supplies YYYY-MM-DD (or YYYY-MM-DD HH:mm under |time) and creates the note.
The bail-out narrows from "no name OR no format" to "no name", which is the
run's own rule.

That token's own text contains a colon, so the illegal-character diagnostic
added in #1595 was firing on it: a working {{VDATE:due}} showed a red "a file
or folder name cannot contain :" under a choice that creates 2026-07-27.md.
Rendering the date deletes the false accusation and, on {{VDATE:due|time}},
lets the true one appear - the run's datetime default really does put HH:mm in
the file name, and Obsidian really does refuse it.

2. Neither preview read an ANSWERED date. The one-page input form seeds the
user's real picks into the formatter before computing the preview, and VDATE
was the only seeded requirement type ignored - so the row showed today beside
the date just chosen. Both formatters now resolve a stored value through
renderStoredDateVariable, extracted from the run's own tail so there is one
implementation and not three. Answered-empty ("") stays empty; an unparseable
seeded string keeps the verbatim back-compat branch; only `undefined` falls
through to the example date.

Both previews also stop throwing on a half-typed |startof: unit, which blanked
the row and reddened it between two keystrokes (#1558's failure mode).

Closes #1589.
chhoumann added a commit that referenced this pull request Jul 27, 2026
Five things the review caught, each measured.

The one-page form previewed an untouched required {{VDATE:}} as answered-empty.
submit() deliberately OMITS a required blank date so the sequential date prompt
still fires, while updatePreviews copied this.result wholesale - so the row
showed a name the run would never create. Both now go through one
collectAnswers(), which is where the rule already lived (it needs
dateParseErrors, which only the modal has).

parseVDateOptionsForPreview swallowed the error as well as the throw. Master's
body preview reported `Unknown date unit "we"` for a permanent typo; this
branch rendered a finished date and said nothing, for a format the run aborts
on - while {{DATE:...|startof:we}} in the same pass still reported it, so the
row contradicted itself. The options and the error are now separate channels:
the text stays stable while the unit is half-typed, the complaint goes on the
diagnostics channel the builder already holds back for 500ms.

The example date now applies |startof:/|endof: too. #1595 left snap out on the
grounds that snapping only the file-name row would split it from the body row;
both rows do it now, so that reason is gone - and the alternative was an
asymmetry ten lines wide, because the answered branch beside it snaps and
{{DATE:...|startof:month}} in the same pass always has. Only a token that asks
for a snap touches moment, so a dateless preview keeps its old path.

existsInVault's bare-name branch is no longer TFile-only. That tightening
reddened a working capture target: captureTargetResolution resolves a bare
extension-less name to a folder SCOPE when a folder of that name exists with no
note beside it, and the picker then writes to a file inside it. The probe
mirrors that rule instead of approximating it, and the trailing-slash case it
was contradicting stays fixed.

Three of the new tests were vacuous, proved by mutation:
- |startof:w does not throw (w is a real alias for week), so neither
  parseVDateOptionsForPreview test entered the catch. Now |startof:we, plus a
  case pinning that a valid short alias stays quiet.
- The capture guard's test passed with the guard deleted. Its placement is what
  matters, so it now asserts the heading picker never opened - that fails when
  the guard moves to the sink.
- The qa-hidden assertion only checked the true half, which the constructor
  already satisfies.
Adds the missing coverage the review named: the body preview's {{MVALUE}} pass
(deleting it left the suite green), and the first-pass ordering in the one-page
modal.
chhoumann added a commit that referenced this pull request Jul 27, 2026
…run stops before the damage (#1618)

* fix(preview): resolve {{MVALUE}}, and stop discarding the collected answer

The file-name and format previews left {{MVALUE}} literal while the run opened
the math modal for it. Both display formatters already overrode
promptForMathValue with a "calculation_result" stand-in that nothing could
reach - a dead override is the tell that the pass was meant to be there.
Positioned where CompleteFormatter.format has it: after {{FILE:}}, before
{{RANDOM:}}.

Also closes the loop the preview exposed. RequirementCollector registers a
requirement keyed "mvalue" for this token, so the one-page input form asks for
a "Math expression" and the CLI prints missingFlags: ["value-mvalue=<value>"] -
and nothing read either answer, so the form asked and then the run opened the
modal on top of it, and passing the CLI's own recommended flag changed nothing.
replaceMathValueInString now reads the collected value first, the way {{VALUE}}
always has. READ, never write: the prompt's answer stays unstored, so
{{MVALUE}} {{MVALUE}} is still two questions.

The non-interactive abort also named "a {{MATH}} expression", a token QuickAdd
has never had (MATH_VALUE_REGEX is /{{MVALUE}}/i) - in the one message whose
job is to say which flag to pass.

Closes #1587. Closes #1607.

* fix(preview): give {{VDATE:}} the run's default format, and its answered value

Two ways the preview disagreed with the run about the same token.

1. A format-less {{VDATE:due}} previewed as the raw token, while the run
supplies YYYY-MM-DD (or YYYY-MM-DD HH:mm under |time) and creates the note.
The bail-out narrows from "no name OR no format" to "no name", which is the
run's own rule.

That token's own text contains a colon, so the illegal-character diagnostic
added in #1595 was firing on it: a working {{VDATE:due}} showed a red "a file
or folder name cannot contain :" under a choice that creates 2026-07-27.md.
Rendering the date deletes the false accusation and, on {{VDATE:due|time}},
lets the true one appear - the run's datetime default really does put HH:mm in
the file name, and Obsidian really does refuse it.

2. Neither preview read an ANSWERED date. The one-page input form seeds the
user's real picks into the formatter before computing the preview, and VDATE
was the only seeded requirement type ignored - so the row showed today beside
the date just chosen. Both formatters now resolve a stored value through
renderStoredDateVariable, extracted from the run's own tail so there is one
implementation and not three. Answered-empty ("") stays empty; an unparseable
seeded string keeps the verbatim back-compat branch; only `undefined` falls
through to the example date.

Both previews also stop throwing on a half-typed |startof: unit, which blanked
the row and reddened it between two keystrokes (#1558's failure mode).

Closes #1589.

* fix(preview): say so when a file-name format uses {{title}}

`{{title}} note` previewed as an ordinary name in ordinary styling, and the run
created nothing: formatFileName rejects the token outright, because the title
IS the file name. The preview now mirrors both of the run's checks - on the raw
format string, and on format()'s output, where an expanded {{GLOBAL_VAR:}}
snippet or a {{VALUE}} resolving to the literal text can smuggle one in.

The token keeps previewing literally rather than as an example title: it is
genuinely unresolved on screen, so the row's "Unresolved:" label is true, and
the new diagnostic is what says it will never resolve.

The output-side check needed one correction first, or it would have turned a
WORKING choice red: a {{title}} inside a spliced-in {{TEMPLATE:}} body is not a
failure. At run time that body goes through the child engine's
formatFileContent, which resolves the token to the stored title (or "") before
formatFileName ever sees the name. The preview now does the same, at the
include seam - not through the shared token resolver, which would have invented
"My Document Title" for an unstored variable.

`/\{\{title\}\}/i` also stopped being copy-pasted: six inlined copies in
CompleteFormatter and the new preview check all read TITLE_REGEX, so the rule
cannot drift between the surface that warns and the code that throws.

Closes #1588.

* fix(preview): stop labelling a resolved name "Unresolved:"

`Bad: {{VALUE:title}}` rendered as "Unresolved: Bad: Example Title". Every token
resolved; nothing was unresolved. The label sent the reader hunting for a broken
token when the problem was the finished name.

Two error classes shared one label, and the split already existed: #1582 added
`kind: "path"` for the capture target's hideWhen gate, defined as "the name came
out fine, the vault will just not accept it". Reusing that axis rather than
adding a parallel one keeps a single classification that cannot disagree with
itself. The row now reads Preview: / Won't be created: / Unresolved:, with
unresolved winning when both are present - a missing template beside an empty
path segment is fundamentally a failure to resolve.

The existence probe that suppresses the illegal-character error was shape-blind
in both directions, which put a wrong label on two real cases. Obsidian's path
map holds no trailing slash, so a capture target of `Meetings: 2026/` - naming
a FOLDER to pick inside, which exists - matched neither probe and went red;
while a file-name format of `Meetings: 2026` was excused by a folder of that
name although the run still cannot create `Meetings: 2026.md`. The probe now
requires a TFolder for the trailing-slash shape and a TFile otherwise.

FormatPreviewField.test.ts pins all three labels, including the both-classes
case; the illegal-character test pins all four probe shapes.

Closes #1594.

* fix(preview): give the one-page form its diagnostics and its target folder

The one-page input form previews the note's file name while you fill it in,
using the same FileNameDisplayFormatter the builder uses - and wired up two
fewer things, so the same format previewed differently in the two places.

Every diagnostic was thrown away. computePreview returned Record<string,string>
and the modal rendered only those strings, so a name Obsidian will refuse was
presented in ordinary styling with nothing said. This is the surface where the
message is worth the most: the user's REAL answers are seeded into the
formatter, so the row shows the exact name that is about to fail. It is now an
ordered list of rows carrying label, text and diagnostics, rendered with the
builder's own .qa-preview-issue styling and its three-state label vocabulary.

setTargetFolderPath was never called, so `Notes/{{FOLDER}}/x` previewed
`Notes//x` plus an empty-segment error nobody could see. It now resolves
against the choice's folder when the run has already decided it - a single
configured folder, no chooser - and falls back to the builder's neutral
placeholder otherwise. Deliberately NOT the raw configured folder in every
case: the run formats it first (formatFolderPath) while setTargetFolderPath
does not, so a folder of `Journal/{{DATE:YYYY-MM}}` would splice the literal
token into the name and, since every argument-bearing token carries a colon,
raise the illegal-character error against a choice that works. The builder's
row takes the same folder, so the two surfaces agree.

Three things found while wiring it up:

- The row's key was the raw record key, so it read `fileName:`.
- The first pass ran before renderField populated the value map, flashing a
  stand-in over prefilled answers.
- updatePreviews had no generation guard; the 150ms debounce orders the STARTS
  of these passes, not their completions, and one can read up to 25 templates.
  Text and problems now commit under one monotonic token, as in the builder.

The empty tinted box reading "Preview" is gone too: it was rendered for every
Capture, Macro and Multi choice, and every Template using the default
note-title prompt, none of which produce a row.

Closes #1590.

* fix: refuse an impossible file name before running the template body

A Template choice whose name resolves to something Obsidian refuses did not
fail when the name was computed. It failed at vault.create - by which point
QuickAdd had already created the target folder and formatted the ENTIRE
template body: real {{VALUE}} prompts, macros, and inline `js quickadd` fences
with whatever side effects those scripts have. The user answered the whole
prompt chain, their scripts ran, and then the choice died.

Measured in this worktree's isolated vault (Obsidian 1.13.0), a Template choice
named `Bad: {{VALUE:title}}` whose template contains an inline script:

  before: ok:false "Choice execution failed; no file was created."
          folderExists: true, inlineScriptRuns: 1
  after:  ok:false "Cannot create \"Repro1591Folder/Bad: My Note.md\": a file
          or folder name cannot contain \":\". ..."
          folderExists: false, inlineScriptRuns: 0

The rule is "reject only if the file does not already exist": ":" is legal on
macOS/Linux at the filesystem level, so a note made outside Obsidian really can
carry one, and appending to it works today because nothing ever asks Obsidian
to accept the name. Each guard therefore sits on a branch where non-existence
is already established, so it needs no probe of its own:

- TemplateEngine.createFileWithTemplate, first statement. Its two production
  callers are TemplateChoiceEngine's `else` branch of vault.adapter.exists and
  its freshly incremented collision name.
- CaptureChoiceEngine, gated character-for-character on the dispatch condition
  that routes to onCreateFileIfItDoesntExist - placed above the heading picker,
  so the user is not asked to choose a heading for a note that cannot be
  created - and re-asserted at that sink so the invariant is local to it.
- The "move note to match choice settings?" offer, which creates the target
  folder and then fails at renameFile. It declines quietly: the offer is an
  optional convenience, and the choice's own preview explains the problem.

Deliberately NOT inside normalizeGeneratedFilePath: CaptureChoiceEngine calls
that as an existence PROBE inside try/catch, where a new throw would silently
answer "does not exist".

The character set and the {{TIME}} hint come from generatedFilePath.ts, which
the preview also reads, so the row that warns and the code that stops cannot
drift. Only the tense differs.

Closes #1591.

* fix(preview): fold in the adversarial review of this branch

Five things the review caught, each measured.

The one-page form previewed an untouched required {{VDATE:}} as answered-empty.
submit() deliberately OMITS a required blank date so the sequential date prompt
still fires, while updatePreviews copied this.result wholesale - so the row
showed a name the run would never create. Both now go through one
collectAnswers(), which is where the rule already lived (it needs
dateParseErrors, which only the modal has).

parseVDateOptionsForPreview swallowed the error as well as the throw. Master's
body preview reported `Unknown date unit "we"` for a permanent typo; this
branch rendered a finished date and said nothing, for a format the run aborts
on - while {{DATE:...|startof:we}} in the same pass still reported it, so the
row contradicted itself. The options and the error are now separate channels:
the text stays stable while the unit is half-typed, the complaint goes on the
diagnostics channel the builder already holds back for 500ms.

The example date now applies |startof:/|endof: too. #1595 left snap out on the
grounds that snapping only the file-name row would split it from the body row;
both rows do it now, so that reason is gone - and the alternative was an
asymmetry ten lines wide, because the answered branch beside it snaps and
{{DATE:...|startof:month}} in the same pass always has. Only a token that asks
for a snap touches moment, so a dateless preview keeps its old path.

existsInVault's bare-name branch is no longer TFile-only. That tightening
reddened a working capture target: captureTargetResolution resolves a bare
extension-less name to a folder SCOPE when a folder of that name exists with no
note beside it, and the picker then writes to a file inside it. The probe
mirrors that rule instead of approximating it, and the trailing-slash case it
was contradicting stays fixed.

Three of the new tests were vacuous, proved by mutation:
- |startof:w does not throw (w is a real alias for week), so neither
  parseVDateOptionsForPreview test entered the catch. Now |startof:we, plus a
  case pinning that a valid short alias stays quiet.
- The capture guard's test passed with the guard deleted. Its placement is what
  matters, so it now asserts the heading picker never opened - that fails when
  the guard moves to the sink.
- The qa-hidden assertion only checked the true half, which the constructor
  already satisfies.
Adds the missing coverage the review named: the body preview's {{MVALUE}} pass
(deleting it left the suite green), and the first-pass ordering in the one-page
modal.

* test(preview): anchor the seeded VDATE instant to local time

The two "renders an ANSWERED date" cases seeded @Date:2026-08-15T00:00:00.000Z
and asserted "2026-08-15". The stored value is an INSTANT and moment formats it
in local time, so anywhere west of UTC it renders as the 14th - green in CET,
red on the UTC runners. Built from a local Date instead, which round-trips in
every zone (verified UTC-8 through UTC+14). The sibling illegal-chars test file
already carried this warning in its header.

* fix(preview): treat an inline-script folder as unknown, and pin two guards

Codex spotted a real hole in likelyTargetFolderPath: it refuses a configured
folder containing `{{`, but an inline `js quickadd` fence contains none, and
the folder validator lets backticks through (INVALID_FOLDER_CHARS_REGEX is
\ / : * ? " < > |). formatFolderPath EXECUTES that fence - it is format()'s
first pass - so the previews would have spliced the raw script source into
{{FOLDER}}, which is exactly what an inert preview must never do (#1558), and
its punctuation would then have been read as illegal characters. It falls back
to the neutral placeholder now, with the rule taken from INLINE_JAVASCRIPT_REGEX
rather than approximated.

Two coverage gaps CodeRabbit named, both of which were vacuous when written and
are mutation-verified now:

- The move offer's guard had no test at all. Driving it needs the INTERACTIVE
  path (a `templatePath` argument makes the run non-interactive and skips
  reconciliation entirely, so the first attempt passed with the guard deleted).
  Both halves are pinned: an impossible target asks nothing and renames nothing,
  a legal one still offers the move.
- The capture guard's test now asserts the capture's own {{VALUE}} prompt never
  opened, which is the whole point of #1591 - the user must not answer a prompt
  chain for a note that cannot be created.

* test(preview): pin that a slow preview pass is stale, never mixed

CodeRabbit asked whether the previous value's diagnostics can label the new one
while an async pass is in flight past the 500ms idle gate. They cannot, and the
regression test they asked for is what shows why: `preview` and `diagnostics`
commit together under `previewToken`, so the row always holds one pass's text
under that same pass's label and complaint. It can lag; it cannot mix. A stale
pass landing after a newer one is discarded, which the second case pins.

The comment records why the obvious fix is a downgrade: clearing `diagnostics`
on a value change would leave the previous, known-bad name on screen under a
plain "Preview:" label for the length of the pass - the exact assertion this row
exists to withdraw.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant