@@ -42,6 +42,7 @@ export class WorkspaceProvider
42
42
private timeout : NodeJS . Timeout | undefined ;
43
43
private fetching = false ;
44
44
private visible = false ;
45
+ private searchFilter = "" ;
45
46
46
47
constructor (
47
48
private readonly getWorkspacesQuery : WorkspaceQuery ,
@@ -52,6 +53,15 @@ export class WorkspaceProvider
52
53
// No initialization.
53
54
}
54
55
56
+ setSearchFilter ( filter : string ) {
57
+ this . searchFilter = filter ;
58
+ this . refresh ( undefined ) ;
59
+ }
60
+
61
+ getSearchFilter ( ) : string {
62
+ return this . searchFilter ;
63
+ }
64
+
55
65
// fetchAndRefresh fetches new workspaces, re-renders the entire tree, then
56
66
// keeps refreshing (if a timer length was provided) as long as the user is
57
67
// still logged in and no errors were encountered fetching workspaces.
@@ -300,7 +310,90 @@ export class WorkspaceProvider
300
310
301
311
return Promise . resolve ( [ ] ) ;
302
312
}
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 ;
304
397
}
305
398
}
306
399
0 commit comments