@@ -22,9 +22,11 @@ function isMentionPrefixAt(text: string, atIndex: number): boolean {
2222}
2323
2424/**
25- * Scans `text` for known integration names and rewrites each bare
26- * occurrence to `@CanonicalName` (case-normalized). Names already
27- * preceded by `@` are surfaced as contexts but not double-prefixed.
25+ * Scans `text` for explicit `@`-prefixed integration mentions and
26+ * canonicalizes their casing (`@slack` → `@Slack`). Bare integration names
27+ * are left untouched — plain words like `Monday` or `Notion` in prose must
28+ * never be rewritten or chipped (the scunthorpe problem); only a deliberate
29+ * `@` opt-in gets mention treatment.
2830 */
2931function convertText ( text : string ) : { text : string ; contexts : IntegrationContext [ ] } {
3032 const { regex, byName } = getIntegrationMatcher ( )
@@ -42,23 +44,17 @@ function convertText(text: string): { text: string; contexts: IntegrationContext
4244 while ( ( match = regex . exec ( text ) ) !== null ) {
4345 const info = byName . get ( match [ 0 ] . toLowerCase ( ) )
4446 if ( ! info ) continue
45- const atIndex = match . index - 1
46- const hasAtChar = atIndex >= 0 && text [ atIndex ] === '@'
47- // Email-like prefix (`foo@slack`): the `@` is not a token-starting
48- // prefix. Skip entirely so we neither rewrite nor surface a context —
49- // the downstream sync effect would strip it anyway and we'd just be
50- // doing wasted work that flickers a chip for one render.
51- if ( hasAtChar && ! isMentionPrefixAt ( text , atIndex ) ) continue
47+ // Only explicit, token-starting `@` mentions count. Bare names and
48+ // email-like prefixes (`foo@slack`) are skipped entirely.
49+ if ( ! isMentionPrefixAt ( text , match . index - 1 ) ) continue
5250 if ( ! seen . has ( info . name ) ) {
5351 seen . add ( info . name )
5452 contexts . push ( { kind : 'integration' , blockType : info . blockType , label : info . name } )
5553 }
56- const isPrefixed = hasAtChar
5754 // Skip rewrite when the match is already in canonical `@Slack` form;
58- // otherwise rewrite (either insert `@` or canonicalize the casing).
59- if ( isPrefixed && match [ 0 ] === info . name ) continue
60- const replacement = isPrefixed ? info . name : `@${ info . name } `
61- result += text . slice ( lastEnd , match . index ) + replacement
55+ // otherwise canonicalize the casing.
56+ if ( match [ 0 ] === info . name ) continue
57+ result += text . slice ( lastEnd , match . index ) + info . name
6258 lastEnd = match . index + match [ 0 ] . length
6359 mutated = true
6460 }
@@ -78,15 +74,17 @@ interface ProcessChangeArgs {
7874}
7975
8076/**
81- * Auto-converts organic integration names (`Slack`, `AWS Textract`,
82- * `Gmail`, any casing) into proper `@CanonicalName` mentions so they
83- * render with the exact same chip UI as resource mentions — preserving
84- * caret alignment, undo grouping, and copy/paste round-tripping.
77+ * Canonicalizes explicit `@`-prefixed integration mentions (`@slack`,
78+ * `@aws textract`, any casing) into `@CanonicalName` form so they render
79+ * with the exact same chip UI as resource mentions — preserving caret
80+ * alignment, undo grouping, and copy/paste round-tripping. Bare integration
81+ * names in prose are deliberately left untouched: mention treatment is
82+ * strictly opt-in via `@`.
8583 *
8684 * Two entry points share one dedup helper:
8785 * - `processChange`: keystroke fast-path. Detects a typed word-boundary
88- * char that just completed an integration name and rewrites the
89- * textarea in a single `setRangeText` edit (joined with the typed
86+ * char that just completed an `@`-prefixed integration name and rewrites
87+ * the textarea in a single `setRangeText` edit (joined with the typed
9088 * boundary char in native undo history).
9189 * - `applyToText`: bulk path for paste, template insertion, draft
9290 * restore, speech-to-text, and any other programmatic value source.
@@ -139,22 +137,18 @@ export function useIntegrationAutoMention({ setSelectedContexts }: UseIntegratio
139137 const info = byName . get ( completed . name . toLowerCase ( ) )
140138 if ( ! info ) return nextValue
141139
142- const atIndex = completed . start - 1
143- const hasAtChar = atIndex >= 0 && nextValue [ atIndex ] === '@'
144- // Email-like prefix (`foo@slack`): bail out — neither rewrite nor
145- // record a context. The `@` is not a token-starting prefix.
146- if ( hasAtChar && ! isMentionPrefixAt ( nextValue , atIndex ) ) return nextValue
147- const hasAtPrefix = hasAtChar
140+ // Only explicit, token-starting `@` mentions count. Bare names and
141+ // email-like prefixes (`foo@slack`) are left untouched — a plain word
142+ // like `Monday` in prose must never be rewritten or chipped.
143+ if ( ! isMentionPrefixAt ( nextValue , completed . start - 1 ) ) return nextValue
148144 // If the user already typed canonical `@Slack`, nothing to rewrite —
149- // just record the context. Otherwise either insert `@` (bare name)
150- // or canonicalize the casing (e.g. `@slack` → `@Slack` so the
151- // mention pipeline's case-sensitive token match finds it).
152- if ( ! hasAtPrefix || completed . name !== info . name ) {
145+ // just record the context. Otherwise canonicalize the casing
146+ // (e.g. `@slack` → `@Slack` so the mention pipeline's case-sensitive
147+ // token match finds it).
148+ if ( completed . name !== info . name ) {
153149 const caret = textarea . selectionStart ?? diffIndex + 1
154- const replacement = hasAtPrefix ? info . name : `@${ info . name } `
155- const caretShift = hasAtPrefix ? 0 : 1
156- textarea . setRangeText ( replacement , completed . start , completed . end , 'preserve' )
157- textarea . setSelectionRange ( caret + caretShift , caret + caretShift )
150+ textarea . setRangeText ( info . name , completed . start , completed . end , 'preserve' )
151+ textarea . setSelectionRange ( caret , caret )
158152 }
159153
160154 mergeContexts ( [ { kind : 'integration' , blockType : info . blockType , label : info . name } ] )
0 commit comments