@@ -11,9 +11,9 @@ import {
1111 type AgentCliFlags ,
1212 type AgentCliRuntime ,
1313 agentCliFail ,
14- agentCliOk ,
1514} from '@/lib/mothership/agent-cli/types'
1615import { buildIntegrationToolSchemas } from '@/lib/mothership/chat/payload'
16+ import { workspaceFileTextFormat } from '@/lib/workspace-files/text-format'
1717import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection'
1818import { OPAQUE_RESOLVED_SECRET_REPLACEMENT } from '@/executor/utils/resolved-secret-matcher'
1919import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
@@ -61,6 +61,7 @@ const PLATFORM_SCOPES: ReadonlySet<Scope> = new Set<Scope>(['blocks', 'tools'])
6161
6262interface GrepRuntime extends AgentCliRuntime {
6363 reportIncomplete ( message : string ) : void
64+ reportNonText ( ) : void
6465}
6566
6667/**
@@ -370,22 +371,44 @@ const FETCHERS: Record<
370371 credentials : async ( _runtime , item ) => render ( 'credentials' , item . id , item . label , item . raw ) ,
371372}
372373
373- function concurrencyFor ( scope : Scope ) : number {
374- return scope === 'files' ? FILE_READ_CONCURRENCY : FETCH_CONCURRENCY
374+ /**
375+ * A listed file the agent's text read has no extractor for (fonts, media, archives, images), by
376+ * the same gate that read applies. A listing without a name cannot say, so that file is read.
377+ */
378+ function lacksText ( file : unknown ) : boolean {
379+ if ( ! isRecordLike ( file ) ) return false
380+ const name = str ( file . name )
381+ return name !== undefined && ! workspaceFileTextFormat ( { name, type : str ( file . type ) ?? '' } )
375382}
376383
377384async function fetchAll (
378385 runtime : GrepRuntime ,
379386 scope : Scope ,
380387 entries : IndexEntry [ ]
381388) : Promise < GrepMaterialized [ ] > {
382- if ( scope === 'files' && entries . length > MAX_GREP_FILES ) {
389+ if ( scope !== 'files' ) {
390+ return mapConcurrent ( entries , FETCH_CONCURRENCY , ( item ) => FETCHERS [ scope ] ( runtime , item ) )
391+ }
392+ // Skipped files keep their identity, so `--in` naming one is not an unknown selector.
393+ const skipped : GrepMaterialized [ ] = [ ]
394+ const readable : IndexEntry [ ] = [ ]
395+ for ( const item of entries ) {
396+ if ( lacksText ( item . raw ) ) {
397+ runtime . reportNonText ( )
398+ skipped . push ( { scope, id : item . id , label : item . label , text : null } )
399+ } else readable . push ( item )
400+ }
401+ if ( readable . length > MAX_GREP_FILES ) {
383402 runtime . reportIncomplete (
384- `files: searched the first ${ MAX_GREP_FILES } of ${ entries . length } selected files; narrow with --in files/<name-or-id>.`
403+ `files: searched the first ${ MAX_GREP_FILES } of ${ readable . length } selected files; narrow with --in files/<name-or-id>.`
385404 )
386405 }
387- const selected = scope === 'files' ? entries . slice ( 0 , MAX_GREP_FILES ) : entries
388- return mapConcurrent ( selected , concurrencyFor ( scope ) , ( item ) => FETCHERS [ scope ] ( runtime , item ) )
406+ const read = await mapConcurrent (
407+ readable . slice ( 0 , MAX_GREP_FILES ) ,
408+ FILE_READ_CONCURRENCY ,
409+ ( item ) => FETCHERS . files ( runtime , item )
410+ )
411+ return [ ...read , ...skipped ]
389412}
390413
391414/** A whole world, for a search with no `--in`: every resource the index lists. */
@@ -566,10 +589,15 @@ export interface GrepSource {
566589 registry ?: AgentCliRuntime [ 'resolvedSecretTraceRegistry' ]
567590 /** The only scopes this corpus holds, and the refusal for a search that names another. */
568591 scopeLimit ?: { scopes : readonly Scope [ ] ; refusal : string }
592+ /**
593+ * `reportNonText` counts a file skipped unread because it has no text to extract: a note,
594+ * not an incomplete search. The skipped file is still returned with null text.
595+ */
569596 materialize (
570597 scopes : Scope [ ] ,
571598 nameFilter : string | undefined ,
572- reportIncomplete : ( message : string ) => void
599+ reportIncomplete : ( message : string ) => void ,
600+ reportNonText : ( ) => void
573601 ) : Promise < GrepMaterialized [ ] >
574602}
575603
@@ -624,15 +652,24 @@ export async function executeGrep(positionals: string[], flags: AgentCliFlags, s
624652 issueCount ++
625653 if ( issues . length < 10 ) issues . push ( clip ( message ) )
626654 }
627- const finish = ( stdout : string ) =>
628- issueCount === 0
629- ? agentCliOk ( stdout )
630- : {
631- exitCode : 1 ,
632- stdout,
633- stderr : `Search incomplete (${ issueCount } issue${ issueCount === 1 ? '' : 's' } ); matches and counts cover only the content read.\n${ issues . join ( '\n' ) } ${ issueCount > issues . length ? `\n${ issueCount - issues . length } further issues omitted.` : '' } ` ,
634- }
635- const candidates = await source . materialize ( searched , nameFilter , reportIncomplete )
655+ let nonTextCount = 0
656+ const reportNonText = ( ) => {
657+ nonTextCount ++
658+ }
659+ const finish = ( stdout : string ) => {
660+ const skipped = nonTextCount
661+ ? `Skipped ${ nonTextCount } file${ nonTextCount === 1 ? '' : 's' } with no searchable text (images, media, archives, and other binary formats).`
662+ : ''
663+ const incomplete =
664+ issueCount &&
665+ `Search incomplete (${ issueCount } issue${ issueCount === 1 ? '' : 's' } ); matches and counts cover only the content read.\n${ issues . join ( '\n' ) } ${ issueCount > issues . length ? `\n${ issueCount - issues . length } further issues omitted.` : '' } `
666+ return {
667+ exitCode : issueCount ? 1 : 0 ,
668+ stdout,
669+ stderr : [ incomplete , skipped ] . filter ( Boolean ) . join ( '\n' ) ,
670+ }
671+ }
672+ const candidates = await source . materialize ( searched , nameFilter , reportIncomplete , reportNonText )
636673 source . signal ?. throwIfAborted ( )
637674 /**
638675 * A resource nothing in the searched worlds answers to is a wrong selector, not a
@@ -721,8 +758,8 @@ export const universalGrepCommand: AgentCliEngine = {
721758 executeGrep ( positionals , flags , {
722759 signal : runtime . signal ,
723760 registry : runtime . resolvedSecretTraceRegistry ,
724- async materialize ( scopes , nameFilter , reportIncomplete ) {
725- const bounded = gatedRuntime ( { ...runtime , reportIncomplete } )
761+ async materialize ( scopes , nameFilter , reportIncomplete , reportNonText ) {
762+ const bounded = gatedRuntime ( { ...runtime , reportIncomplete, reportNonText } )
726763 return nameFilter
727764 ? materializeWithin ( bounded , scopes , nameFilter )
728765 : ( await settleAll ( scopes . map ( ( scope ) => materializeScope ( bounded , scope ) ) ) ) . flat ( )
0 commit comments