11using GitRekt ;
2+ using System . Security . Cryptography ;
23using System . Text ;
34
45Console . OutputEncoding = Encoding . UTF8 ;
3738 var hasAiVerdictFilter = cliArguments . AiValidationVerdictFilter is not null ;
3839 var useAiAgent = cliArguments . AiValidation ? . UseAgent == true ;
3940 var aiValidationCache = new Dictionary < string , AiValidationCacheValue > ( StringComparer . OrdinalIgnoreCase ) ;
41+ var totalDisplayedResults = 0 ;
4042
4143 foreach ( var ( query , queryIndex ) in cliArguments . Queries . Select ( ( query , index ) => ( query , index ) ) )
4244 {
4547 var validatedResultIndex = 0 ;
4648 var hasSearchResults = false ;
4749 var hasDisplayedResults = false ;
50+ var duplicateSignatures = new HashSet < string > ( StringComparer . Ordinal ) ;
4851
4952 if ( isMultiQuery )
5053 {
6467
6568 while ( resultIndex < searchPage . Items . Count )
6669 {
70+ if ( IsDuplicateResult ( searchPage . Items [ resultIndex ] , duplicateSignatures ) )
71+ {
72+ resultIndex ++ ;
73+ continue ;
74+ }
75+
6776 var chunkValidationOutcomes = await ValidateNextSearchResultChunkAsync (
6877 aiValidationClient ,
6978 query ,
7079 cliArguments . UseAdvancedQuery ,
7180 searchPage ,
7281 resultIndex ,
7382 validatedResultIndex ,
83+ duplicateSignatures ,
7484 aiValidationCache ) ;
7585 validatedResultIndex = chunkValidationOutcomes . ValidatedResultIndex ;
7686
8898 }
8999
90100 displayedResultIndex ++ ;
101+ totalDisplayedResults ++ ;
91102 hasDisplayedResults = true ;
92103 var header = hasAiVerdictFilter
93104 ? $ "Result { validationOutcome ? . ValidatedIndex ?? displayedResultIndex } /{ searchPage . AvailableCount } "
@@ -123,6 +134,8 @@ await WriteSearchResultAsync(
123134 }
124135 }
125136
137+ WriteCompletionSummary ( totalDisplayedResults , aiValidationClient ? . TokenUsage , outputWriter , statusLine ) ;
138+
126139 return 0 ;
127140}
128141catch ( Exception ex )
@@ -150,13 +163,44 @@ await WriteSearchResultAsync(
150163 return new StreamWriter ( fullPath , append : false ) ;
151164}
152165
166+ static void WriteCompletionSummary ( int displayedResultCount , AiTokenUsage ? tokenUsage , TextWriter ? outputWriter , ConsoleStatusLine statusLine )
167+ {
168+ var resultText = displayedResultCount == 1
169+ ? "1 result"
170+ : $ "{ displayedResultCount . ToString ( System . Globalization . CultureInfo . InvariantCulture ) } results";
171+
172+ if ( tokenUsage is null )
173+ {
174+ WriteColoredLine ( $ "Done. Displayed { resultText } .", ConsoleColor . Cyan , outputWriter , statusLine ) ;
175+ return ;
176+ }
177+
178+ if ( ! tokenUsage . HasUsage )
179+ {
180+ WriteColoredLine ( $ "Done. Displayed { resultText } . AI token usage was not reported by the provider.", ConsoleColor . Cyan , outputWriter , statusLine ) ;
181+ return ;
182+ }
183+
184+ WriteColoredLine (
185+ $ "Done. Displayed { resultText } . AI tokens: { FormatTokenCount ( tokenUsage . TotalTokens ) } total ({ FormatTokenCount ( tokenUsage . InputTokens ) } input, { FormatTokenCount ( tokenUsage . OutputTokens ) } output).",
186+ ConsoleColor . Cyan ,
187+ outputWriter ,
188+ statusLine ) ;
189+ }
190+
191+ static string FormatTokenCount ( long tokenCount )
192+ {
193+ return tokenCount . ToString ( "N0" , System . Globalization . CultureInfo . InvariantCulture ) ;
194+ }
195+
153196static async Task < AiValidationPageOutcomes > ValidateNextSearchResultChunkAsync (
154197 IAiValidationClient ? aiValidationClient ,
155198 string query ,
156199 bool useAdvancedQuery ,
157200 GithubCodeSearchPage searchPage ,
158201 int startIndex ,
159202 int validatedResultIndex ,
203+ HashSet < string > duplicateSignatures ,
160204 Dictionary < string , AiValidationCacheValue > aiValidationCache )
161205{
162206 const int MaxStreamingRepositoryBatchSize = 8 ;
@@ -199,6 +243,7 @@ [new AiValidationOutcome(cacheValue.Validation, cacheValue.Error, validatedResul
199243 searchPage ,
200244 startIndex ,
201245 MaxStreamingRepositoryBatchSize ,
246+ duplicateSignatures ,
202247 aiValidationCache ) ;
203248
204249 if ( pendingResults . Count > 1 )
@@ -260,6 +305,7 @@ static List<PendingAiValidationResult> CollectStreamingRepositoryBatch(
260305 GithubCodeSearchPage searchPage ,
261306 int startIndex ,
262307 int maxBatchSize ,
308+ HashSet < string > duplicateSignatures ,
263309 Dictionary < string , AiValidationCacheValue > aiValidationCache )
264310{
265311 var firstResult = searchPage . Items [ startIndex ] ;
@@ -281,6 +327,11 @@ static List<PendingAiValidationResult> CollectStreamingRepositoryBatch(
281327 break ;
282328 }
283329
330+ if ( index != startIndex && IsDuplicateResult ( result , duplicateSignatures ) )
331+ {
332+ break ;
333+ }
334+
284335 var cacheKey = CreateAiValidationCacheKey ( query , useAdvancedQuery , result ) ;
285336
286337 if ( aiValidationCache . ContainsKey ( cacheKey ) )
@@ -412,6 +463,34 @@ static string NormalizeSnippet(string snippet)
412463 return string . Join ( ' ' , snippet . Split ( ( char [ ] ? ) null , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ) ;
413464}
414465
466+ static string CreateResultDuplicateSignature ( GithubCodeSearchResult result )
467+ {
468+ var snippetSignature = CreateSnippetSignature ( result ) ;
469+
470+ if ( string . IsNullOrWhiteSpace ( snippetSignature ) )
471+ {
472+ return string . Empty ;
473+ }
474+
475+ var signatureBytes = Encoding . UTF8 . GetBytes ( string . Join ( '\n ' , result . Repository . FullName , snippetSignature ) ) ;
476+ var signatureHash = SHA256 . HashData ( signatureBytes ) ;
477+ return Convert . ToHexString ( signatureHash . AsSpan ( 0 , 16 ) ) ;
478+ }
479+
480+ static bool IsDuplicateResult ( GithubCodeSearchResult result , HashSet < string > duplicateSignatures )
481+ {
482+ const int maxTrackedSignatures = 2048 ;
483+
484+ if ( duplicateSignatures . Count >= maxTrackedSignatures )
485+ {
486+ return false ;
487+ }
488+
489+ var signature = CreateResultDuplicateSignature ( result ) ;
490+
491+ return ! string . IsNullOrWhiteSpace ( signature ) && ! duplicateSignatures . Add ( signature ) ;
492+ }
493+
415494static void WriteLine ( string value , TextWriter ? outputWriter , ConsoleStatusLine statusLine )
416495{
417496 statusLine . Clear ( ) ;
@@ -485,7 +564,10 @@ static async Task WriteAdditionalLeadsAsync(GithubClient client, IReadOnlyList<A
485564 } ;
486565
487566 WriteColoredLine ( $ " • { verdictLabel } : { url } ", GetAiValidationColor ( item . Verdict ) , outputWriter , statusLine ) ;
488- WriteLabeledLine ( " Why" , item . Reason , outputWriter , statusLine , ConsoleColor . Gray ) ;
567+ if ( HasMeaningfulSensitiveItemReason ( item . Reason ) )
568+ {
569+ WriteLabeledLine ( " Why" , item . Reason , outputWriter , statusLine , ConsoleColor . Gray ) ;
570+ }
489571
490572 if ( ! string . IsNullOrWhiteSpace ( item . Snippet ) )
491573 {
@@ -494,6 +576,13 @@ static async Task WriteAdditionalLeadsAsync(GithubClient client, IReadOnlyList<A
494576 }
495577}
496578
579+ static bool HasMeaningfulSensitiveItemReason ( string ? reason )
580+ {
581+ return ! string . IsNullOrWhiteSpace ( reason )
582+ && ! string . Equals ( reason . Trim ( ) , "Agent marked this item as sensitive." , StringComparison . OrdinalIgnoreCase )
583+ && ! reason . StartsWith ( "No item-specific reason was returned" , StringComparison . OrdinalIgnoreCase ) ;
584+ }
585+
497586static void WriteSectionLine ( string title , TextWriter ? outputWriter , ConsoleStatusLine statusLine , ConsoleColor color )
498587{
499588 WriteColoredLine ( title , color , outputWriter , statusLine ) ;
@@ -602,8 +691,12 @@ static string CreateRepositoryFileUrl(GithubCodeSearchRepository repository, str
602691 var repositoryUrl = ! string . IsNullOrWhiteSpace ( repository . HtmlUrl )
603692 ? repository . HtmlUrl
604693 : $ "https://github.com/{ repository . FullName } ";
694+ var branch = ! string . IsNullOrWhiteSpace ( repository . DefaultBranch )
695+ ? repository . DefaultBranch
696+ : "main" ;
697+ var encodedBranch = string . Join ( '/' , branch . Split ( '/' , StringSplitOptions . RemoveEmptyEntries ) . Select ( Uri . EscapeDataString ) ) ;
605698 var encodedPath = string . Join ( '/' , path . Split ( '/' , StringSplitOptions . RemoveEmptyEntries ) . Select ( Uri . EscapeDataString ) ) ;
606- return AppendLineAnchor ( $ "{ repositoryUrl . TrimEnd ( '/' ) } /blob/HEAD /{ encodedPath } ", lineNumber ) ;
699+ return AppendLineAnchor ( $ "{ repositoryUrl . TrimEnd ( '/' ) } /blob/{ encodedBranch } /{ encodedPath } ", lineNumber ) ;
607700}
608701
609702static string AppendLineAnchor ( string url , int ? lineNumber )
0 commit comments