diff --git a/packages/core/src/followup/suggestionGenerator.test.ts b/packages/core/src/followup/suggestionGenerator.test.ts index c5e3e2e84c..ebd176ac1f 100644 --- a/packages/core/src/followup/suggestionGenerator.test.ts +++ b/packages/core/src/followup/suggestionGenerator.test.ts @@ -214,6 +214,38 @@ describe('shouldFilterSuggestion', () => { it('filters multiple sentences', () => { expect(shouldFilterSuggestion('Run the tests. Then commit.')).toBe(true); + expect(shouldFilterSuggestion('Hello! How are you?')).toBe(true); + expect(shouldFilterSuggestion('Do this. Then do that.')).toBe(true); + // Abbreviation skipped, then real sentence boundary detected + expect(shouldFilterSuggestion('Check Dr. Smith. Then commit.')).toBe(true); + // Non-word char before punctuation — still detected as sentence boundary + expect(shouldFilterSuggestion('Run (see docs). Then deploy')).toBe(true); + }); + + it('does not filter abbreviations as multiple sentences', () => { + // Issue #6077 — "vs." followed by a capitalized word should pass. + expect( + shouldFilterSuggestion( + "Let's start with the Weeds vs. Wildflowers audit.", + ), + ).toBe(false); + expect(shouldFilterSuggestion('Weeds vs. Wildflowers audit')).toBe(false); + // Common honorifics and abbreviations + expect(shouldFilterSuggestion('Check Dr. Smith notes')).toBe(false); + expect(shouldFilterSuggestion('Ask Mr. Jones for help')).toBe(false); + expect(shouldFilterSuggestion('Talk to Ms. Patel next')).toBe(false); + expect(shouldFilterSuggestion('See Prof. Lee today')).toBe(false); + expect(shouldFilterSuggestion('Visit St. Petersburg office')).toBe(false); + expect(shouldFilterSuggestion('Check etc. Tasks remaining')).toBe(false); + expect(shouldFilterSuggestion('Review options etc. Then commit')).toBe( + false, + ); + // Latin shorthands with an internal period + expect(shouldFilterSuggestion('Use e.g. Docker to build')).toBe(false); + expect(shouldFilterSuggestion('Use i.e. Docker to build')).toBe(false); + // Capitalized variants still recognized as abbreviations + expect(shouldFilterSuggestion('Use E.g. Docker to build')).toBe(false); + expect(shouldFilterSuggestion('Use I.e. Docker to build')).toBe(false); }); it('filters formatting', () => { diff --git a/packages/core/src/followup/suggestionGenerator.ts b/packages/core/src/followup/suggestionGenerator.ts index 7276dbf1b1..3824bce8c2 100644 --- a/packages/core/src/followup/suggestionGenerator.ts +++ b/packages/core/src/followup/suggestionGenerator.ts @@ -239,6 +239,39 @@ const ALLOWED_SINGLE_WORDS = new Set([ 'no', ]); +const KNOWN_ABBREVIATIONS = new Set([ + 'Mr', + 'Mrs', + 'Dr', + 'Ms', + 'Prof', + 'Sr', + 'Jr', + 'St', + 'vs', + 'etc', +]); + +const SENTENCE_BOUNDARY_RE = /[.!?]\s+[A-Z]/g; + +function hasSentenceBoundary(suggestion: string): boolean { + for (const m of suggestion.matchAll(SENTENCE_BOUNDARY_RE)) { + const i = m.index!; + const before = suggestion.slice(0, i); + const wordMatch = before.match(/(\w+)$/); + if (!wordMatch) return true; + const word = wordMatch[1]; + if (KNOWN_ABBREVIATIONS.has(word)) continue; + if ( + (word === 'g' && /e\.g$/i.test(before)) || + (word === 'e' && /i\.e$/i.test(before)) + ) + continue; + return true; + } + return false; +} + /** * Returns the filter reason if the suggestion should be suppressed, or null if it passes. */ @@ -299,7 +332,7 @@ export function getFilterReason(suggestion: string): string | null { if (suggestion.length > 30) return 'too_many_words'; } if (suggestion.length >= 100) return 'too_long'; - if (/[.!?]\s+[A-Z]/.test(suggestion)) return 'multiple_sentences'; + if (hasSentenceBoundary(suggestion)) return 'multiple_sentences'; if (/[\n*]|\*\*/.test(suggestion)) return 'has_formatting'; if (