@@ -42,6 +42,7 @@ export class WorkspaceProvider
4242 private timeout : NodeJS . Timeout | undefined ;
4343 private fetching = false ;
4444 private visible = false ;
45+ private searchFilter = "" ;
4546
4647 constructor (
4748 private readonly getWorkspacesQuery : WorkspaceQuery ,
@@ -52,6 +53,15 @@ export class WorkspaceProvider
5253 // No initialization.
5354 }
5455
56+ setSearchFilter ( filter : string ) {
57+ this . searchFilter = filter ;
58+ this . refresh ( undefined ) ;
59+ }
60+
61+ getSearchFilter ( ) : string {
62+ return this . searchFilter ;
63+ }
64+
5565 // fetchAndRefresh fetches new workspaces, re-renders the entire tree, then
5666 // keeps refreshing (if a timer length was provided) as long as the user is
5767 // still logged in and no errors were encountered fetching workspaces.
@@ -300,7 +310,90 @@ export class WorkspaceProvider
300310
301311 return Promise . resolve ( [ ] ) ;
302312 }
303- return Promise . resolve ( this . workspaces || [ ] ) ;
313+
314+ // Filter workspaces based on search term
315+ let filteredWorkspaces = this . workspaces || [ ] ;
316+ const trimmedFilter = this . searchFilter . trim ( ) ;
317+ if ( trimmedFilter ) {
318+ const searchTerm = trimmedFilter . toLowerCase ( ) ;
319+ filteredWorkspaces = filteredWorkspaces . filter ( ( workspace ) =>
320+ this . matchesSearchTerm ( workspace , searchTerm ) ,
321+ ) ;
322+ }
323+
324+ return Promise . resolve ( filteredWorkspaces ) ;
325+ }
326+
327+ /**
328+ * Check if a workspace matches the given search term using smart search logic.
329+ * Prioritizes exact word matches over substring matches.
330+ */
331+ private matchesSearchTerm (
332+ workspace : WorkspaceTreeItem ,
333+ searchTerm : string ,
334+ ) : boolean {
335+ const workspaceName = workspace . workspace . name . toLowerCase ( ) ;
336+ const ownerName = workspace . workspace . owner_name . toLowerCase ( ) ;
337+ const templateName = (
338+ workspace . workspace . template_display_name ||
339+ workspace . workspace . template_name ||
340+ ""
341+ ) . toLowerCase ( ) ;
342+ const status = workspace . workspace . latest_build . status . toLowerCase ( ) ;
343+
344+ // Check if any agent names match the search term
345+ const agents = extractAgents ( workspace . workspace . latest_build . resources ) ;
346+ const agentNames = agents . map ( ( agent ) => agent . name . toLowerCase ( ) ) ;
347+ const hasMatchingAgent = agentNames . some ( ( agentName ) =>
348+ agentName . includes ( searchTerm ) ,
349+ ) ;
350+
351+ // Check if any agent metadata contains the search term
352+ const hasMatchingMetadata = agents . some ( ( agent ) => {
353+ const watcher = this . agentWatchers [ agent . id ] ;
354+ if ( watcher ?. metadata ) {
355+ return watcher . metadata . some ( ( metadata ) => {
356+ const metadataStr = JSON . stringify ( metadata ) . toLowerCase ( ) ;
357+ return metadataStr . includes ( searchTerm ) ;
358+ } ) ;
359+ }
360+ return false ;
361+ } ) ;
362+
363+ // Smart search: Try exact word match first, then fall back to substring
364+ const searchWords = searchTerm
365+ . split ( / \s + / )
366+ . filter ( ( word ) => word . length > 0 ) ;
367+ const allText = [
368+ workspaceName ,
369+ ownerName ,
370+ templateName ,
371+ status ,
372+ ...agentNames ,
373+ ] . join ( " " ) ;
374+
375+ // Check for exact word matches (higher priority)
376+ const hasExactWordMatch =
377+ searchWords . length > 0 &&
378+ searchWords . some ( ( word ) => {
379+ // Escape special regex characters to prevent injection
380+ const escapedWord = word . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
381+ const wordBoundaryRegex = new RegExp ( `\\b${ escapedWord } \\b` , "i" ) ;
382+ return wordBoundaryRegex . test ( allText ) ;
383+ } ) ;
384+
385+ // Check for substring matches (lower priority) - only if no exact word match
386+ const hasSubstringMatch =
387+ ! hasExactWordMatch &&
388+ ( workspaceName . includes ( searchTerm ) ||
389+ ownerName . includes ( searchTerm ) ||
390+ templateName . includes ( searchTerm ) ||
391+ status . includes ( searchTerm ) ||
392+ hasMatchingAgent ||
393+ hasMatchingMetadata ) ;
394+
395+ // Return true if either exact word match or substring match
396+ return hasExactWordMatch || hasSubstringMatch ;
304397 }
305398}
306399
0 commit comments