You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2e111f6
Browse filesBrowse the repository at this point in the historyBrowse files
fix(search): answer a Note match on the canvas card (#6901)
* fix(search): answer a Note match on the canvas card
A workflow search match inside a Note counted towards the result total and
then highlighted nowhere: the editor panel renders nothing for a Note, and
`clearCurrentBlock` — which the panel calls to refuse one — also cleared the
shared `activeSearchTarget`, destroying the very target the card was about to
paint. Searching a 15k-character note reported "1 of 6" and moved nothing.
The card's read view is now the surface that answers:
- A rehype plugin marks every rendered occurrence; the current one is picked by
an ordinal counted with the same scan the indexer uses, and travels by context
so cycling matches does not re-parse the document.
- `<Streamdown>` is keyed on the query. Its memo comparator ignores
`rehypePlugins`/`components`, so a plugin change alone cannot re-render it —
marks appeared only when something else remounted the card, and then outlived
the query that produced them.
- The canvas selects, centres and expands the Note, because a compact card
resets its scroll region to the top and cannot hold a position deep in its
own body. Scrolling to the mark is `scrollTop` arithmetic, never
`scrollIntoView`, which would drag ReactFlow's transformed viewport off-frame.
- Title matches mark the name too.
`activeSearchTarget` is re-published with a fresh identity on most of the search
panel's renders, so subscribers take primitives. Holding the object in
`WorkflowContent` — the panel's own ancestor — closed an unbounded update loop.
Separately, the serializer escaped every underscore, writing
`SB\_ACTION\_ROUTER\_SECRET` into the document. CommonMark's intraword rule
means that backslash carries no meaning, and search matches the stored markdown,
so it made anything with an underscore unfindable in a note that plainly showed
it. Dropped outside code regions, where the serializer emits verbatim and a
backslash is the author's own character.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(search): honour fences and folded whitespace in Note highlighting
Two review findings, both real.
The intraword-underscore cleanup guarded code with a pattern that recognised
only the shortest delimiter forms — a bare ``` pair and a single-backtick span.
A ````-fenced block, a tilde fence, or a ``multi-backtick`` span ended the
region early and handed the rest of the author's code to the rewrite, turning
`a\_b` into `a_b` inside their code sample. Fenced blocks are now walked a line
at a time, tracking the opening delimiter exactly the way stripEmptyListItemLines
already does (three or more, closed only by a run at least as long), and the
inline branch matches a backtick RUN closed by one of equal length.
The note scanner claimed to be the same scan as the indexer's `findTextRanges`
but did not fold whitespace, which staging added since this branch was written.
The indexer folds every `\s` to a space, so a phrase matches across a soft line
break — which `remark-breaks` renders as a `<br>`, splitting the phrase over two
text nodes that a per-node scan could never see. The hit counted in the panel
and highlighted nowhere, the exact bug this branch exists to fix.
The plugin now scans runs of continuously-readable text rather than single
nodes, so a match spanning an inline boundary (a soft break, a bold word) is
wrapped as several marks sharing one ordinal. Runs end at any non-inline
element, so two paragraphs are never joined into a phrase the reader cannot see.
`foldSearchWhitespace` moved to `@sim/utils/string`: the canvas card renders
from a package, which cannot import from `apps/*`, and two copies of that rule
silently disagreeing is precisely what produced the second finding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(markdown): close a fence only on a bare delimiter run
A closing fence carries nothing but its delimiter run; a line that merely
starts with one is content. The guard matched the prefix alone, so an interior
line like ` ````example ` inside a same-length fence ended the block, and every
cleanup below then processed the author's remaining code as prose — dropping
the backslashes from their `a\_b`.
Both fence walks in this file shared that flaw, so both now go through one
`closesFence`, which requires the run to be followed by nothing but whitespace.
Strictly more conservative: a fence stays open longer, so more content is left
verbatim.
Scope, stated plainly: the serializer always opens a block with one more
delimiter than the longest run inside it, so its own output cannot reach this
shape today, and `postProcessSerializedMarkdown` only ever sees serializer
output. This is a correctness fix that removes an unstated coupling to that
choice, not a live corruption path. The tests therefore exercise
`postProcessSerializedMarkdown` directly — a round-trip test of the same input
would pass either way, which is exactly the vacuous check worth avoiding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(markdown): skip quoted fences, and stop joining runs across inline tags
Two review findings, both real, both the same shape: a rule that looked at the
rendered form and forgot what the source actually says.
QUOTED FENCES. The fence walk only recognised a bare delimiter run, but the
serializer writes a fence inside a blockquote or a `[!NOTE]` callout with a `>`
on every line. Code state was therefore never entered there and the block's
interior was cleaned up as prose: `> x = a\_b` round-tripped to `> x = a_b`,
losing the author's backslash. Unlike the fence-length cases this one is
reachable today — verified against the real serializer before and after. Both
fence walks now unquote the line first.
INLINE JOINS. Runs concatenated the visible text of every inline tag, so
`a<strong>b</strong>c` read as `abc` — a hit that cannot exist in the markdown
the indexer scans, where `**` sits between the words. That is worse than a
spurious mark: `occurrenceIndex` counts SOURCE occurrences, so a fabricated hit
earlier in the document steals the current ordinal and paints the mark on text
the search never matched. Only `<br>` continues a run now, because it alone
stands for a character the source really has (a `\n`, folded to a space).
Everything else stands for syntax the render drops.
Nothing real is lost: a match spanning `a**b**c` would have to contain the
asterisks to exist at all, and a match wholly inside an element is still found —
the element simply starts its own run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(search): match a Note body as it renders, instead of rewriting the file
Replaces the serializer change with one that writes nothing.
The editor backslash-escapes every markdown-significant character in prose, so a
Note the reader sees as `{{TE_SERET}}` is stored as `{{TE\_SERET}}` and search —
which matches the stored value — could not find it. The previous approach undid
that escape in `postProcessSerializedMarkdown`, which meant re-deriving markdown
structure from the serialized string with regexes so it knew what was code. That
is a losing game: three review rounds, each finding another construct it did not
model (longer fences, then delimiter-prefixed lines, then quoted fences), and
each miss REWROTE somebody's code. `markdown-fidelity.ts` is back to staging,
byte for byte.
The escape is now undone on the matching side only. A field declares
`searchTextFormat: 'markdown'` (the Note body is the only one), and the indexer
matches it against `projectEscapedMarkdownForSearch(value)` — a total,
structure-free function that returns the rendered text plus an index back into
the source. Ranges stay in source coordinates, so replace still rewrites the
whole `\_` and never strands a backslash.
The asymmetry is the whole point: a matcher that de-escapes something a fence
would have kept literal changes only which text highlights, and no caller writes
it back. A rewriter making the identical mistake corrupts the file. So there is
nothing here that needs to know about fences at all.
Two consequences worth having: existing notes are searchable immediately rather
than after their next edit, and no stored byte changes, so no document the
editor has ever written can be affected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mode?: 'basic'|'advanced'|'both'|'trigger'|'trigger-advanced'// Default is 'both' if not specified. 'trigger' means only shown in trigger mode. 'trigger-advanced' is the advanced side of a trigger field — either a canonical pair member or a standalone field shown under the block-level advanced toggle
271
271
canonicalParamId?: string
272
+
/**
273
+
* Declares that the stored value is markdown, so workflow search matches it
274
+
* against the text it RENDERS as rather than its source.
275
+
*
276
+
* The rich-text editor backslash-escapes every markdown-significant character
277
+
* in prose, so a Note body the reader sees as `SB_ACTION` is stored as
278
+
* `SB\_ACTION` and would otherwise be unfindable by what is on screen. Ranges
279
+
* stay in source coordinates, so replace still rewrites the escaped span.
280
+
*
281
+
* Omit for every ordinary field: a code or plain-text value is searched as
282
+
* stored, where a backslash is the author's own character.
283
+
*/
284
+
searchTextFormat?: 'markdown'
272
285
/** Controls parameter visibility in agent/tool-input context */
0 commit comments