From bab6c21c4c9fc85e23d7bae834f9e8ca357b9e4b Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Mon, 28 Jul 2025 21:15:12 -0700 Subject: [PATCH 1/6] feat: Add search filter to sidebar --- package.json | 27 +++++++++++ src/commands.ts | 47 +++++++++++++++++++ src/extension.ts | 17 +++++++ src/workspacesProvider.ts | 95 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 185 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b8cfbad..5dd63dc0 100644 --- a/package.json +++ b/package.json @@ -212,6 +212,18 @@ "title": "Coder: Open App Status", "icon": "$(robot)", "when": "coder.authenticated" + }, + { + "command": "coder.searchWorkspaces", + "title": "Coder: Search Workspaces", + "icon": "$(search)", + "when": "coder.authenticated && coder.isOwner" + }, + { + "command": "coder.clearWorkspaceSearch", + "title": "Coder: Clear Workspace Search", + "icon": "$(clear-all)", + "when": "coder.authenticated && coder.isOwner" } ], "menus": { @@ -239,6 +251,21 @@ "command": "coder.refreshWorkspaces", "when": "coder.authenticated && view == myWorkspaces", "group": "navigation" + }, + { + "command": "coder.searchWorkspaces", + "when": "coder.authenticated && coder.isOwner && view == allWorkspaces", + "group": "navigation" + }, + { + "command": "coder.refreshWorkspaces", + "when": "coder.authenticated && coder.isOwner && view == allWorkspaces", + "group": "navigation" + }, + { + "command": "coder.clearWorkspaceSearch", + "when": "coder.authenticated && coder.isOwner && view == allWorkspaces", + "group": "navigation" } ], "view/item/context": [ diff --git a/src/commands.ts b/src/commands.ts index b40ea56e..f6b229cd 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -684,6 +684,53 @@ export class Commands { }); } + /** + * Search/filter workspaces in the All Workspaces view. + * This method will be called from the view title menu. + */ + public async searchWorkspaces(): Promise { + const quickPick = vscode.window.createQuickPick(); + quickPick.placeholder = + "Type to search workspaces by name, owner, template, status, or agent"; + quickPick.title = "Search All Workspaces"; + quickPick.value = ""; + + // Get current search filter to show in the input + const currentFilter = (await vscode.commands.executeCommand( + "coder.getWorkspaceSearchFilter", + )) as string; + if (currentFilter) { + quickPick.value = currentFilter; + } + + quickPick.ignoreFocusOut = true; // Keep open when clicking elsewhere + quickPick.canSelectMany = false; // Don't show selection list + + quickPick.onDidChangeValue((value) => { + // Update the search filter in real-time as user types + vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", value); + }); + + quickPick.onDidAccept(() => { + // When user presses Enter, close the search + quickPick.hide(); + }); + + quickPick.onDidHide(() => { + // Don't clear the search when closed - keep the filter active + quickPick.dispose(); + }); + + quickPick.show(); + } + + /** + * Clear the workspace search filter. + */ + public clearWorkspaceSearch(): void { + vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", ""); + } + /** * Return agents from the workspace. * diff --git a/src/extension.ts b/src/extension.ts index f38fa0cd..ad42186d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -290,6 +290,23 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { "coder.viewLogs", commands.viewLogs.bind(commands), ); + vscode.commands.registerCommand( + "coder.searchWorkspaces", + commands.searchWorkspaces.bind(commands), + ); + vscode.commands.registerCommand( + "coder.setWorkspaceSearchFilter", + (searchTerm: string) => { + allWorkspacesProvider.setSearchFilter(searchTerm); + }, + ); + vscode.commands.registerCommand("coder.getWorkspaceSearchFilter", () => { + return allWorkspacesProvider.getSearchFilter(); + }); + vscode.commands.registerCommand( + "coder.clearWorkspaceSearch", + commands.clearWorkspaceSearch.bind(commands), + ); // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 278ee492..df078d12 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -42,6 +42,7 @@ export class WorkspaceProvider private timeout: NodeJS.Timeout | undefined; private fetching = false; private visible = false; + private searchFilter = ""; constructor( private readonly getWorkspacesQuery: WorkspaceQuery, @@ -52,6 +53,15 @@ export class WorkspaceProvider // No initialization. } + setSearchFilter(filter: string) { + this.searchFilter = filter; + this.refresh(undefined); + } + + getSearchFilter(): string { + return this.searchFilter; + } + // fetchAndRefresh fetches new workspaces, re-renders the entire tree, then // keeps refreshing (if a timer length was provided) as long as the user is // still logged in and no errors were encountered fetching workspaces. @@ -300,7 +310,90 @@ export class WorkspaceProvider return Promise.resolve([]); } - return Promise.resolve(this.workspaces || []); + + // Filter workspaces based on search term + let filteredWorkspaces = this.workspaces || []; + const trimmedFilter = this.searchFilter.trim(); + if (trimmedFilter) { + const searchTerm = trimmedFilter.toLowerCase(); + filteredWorkspaces = filteredWorkspaces.filter((workspace) => + this.matchesSearchTerm(workspace, searchTerm), + ); + } + + return Promise.resolve(filteredWorkspaces); + } + + /** + * Check if a workspace matches the given search term using smart search logic. + * Prioritizes exact word matches over substring matches. + */ + private matchesSearchTerm( + workspace: WorkspaceTreeItem, + searchTerm: string, + ): boolean { + const workspaceName = workspace.workspace.name.toLowerCase(); + const ownerName = workspace.workspace.owner_name.toLowerCase(); + const templateName = ( + workspace.workspace.template_display_name || + workspace.workspace.template_name || + "" + ).toLowerCase(); + const status = workspace.workspace.latest_build.status.toLowerCase(); + + // Check if any agent names match the search term + const agents = extractAgents(workspace.workspace.latest_build.resources); + const agentNames = agents.map((agent) => agent.name.toLowerCase()); + const hasMatchingAgent = agentNames.some((agentName) => + agentName.includes(searchTerm), + ); + + // Check if any agent metadata contains the search term + const hasMatchingMetadata = agents.some((agent) => { + const watcher = this.agentWatchers[agent.id]; + if (watcher?.metadata) { + return watcher.metadata.some((metadata) => { + const metadataStr = JSON.stringify(metadata).toLowerCase(); + return metadataStr.includes(searchTerm); + }); + } + return false; + }); + + // Smart search: Try exact word match first, then fall back to substring + const searchWords = searchTerm + .split(/\s+/) + .filter((word) => word.length > 0); + const allText = [ + workspaceName, + ownerName, + templateName, + status, + ...agentNames, + ].join(" "); + + // Check for exact word matches (higher priority) + const hasExactWordMatch = + searchWords.length > 0 && + searchWords.some((word) => { + // Escape special regex characters to prevent injection + const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const wordBoundaryRegex = new RegExp(`\\b${escapedWord}\\b`, "i"); + return wordBoundaryRegex.test(allText); + }); + + // Check for substring matches (lower priority) - only if no exact word match + const hasSubstringMatch = + !hasExactWordMatch && + (workspaceName.includes(searchTerm) || + ownerName.includes(searchTerm) || + templateName.includes(searchTerm) || + status.includes(searchTerm) || + hasMatchingAgent || + hasMatchingMetadata); + + // Return true if either exact word match or substring match + return hasExactWordMatch || hasSubstringMatch; } } From 3e340bfef4f5d22b1431ede606fd625180304f86 Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Tue, 29 Jul 2025 15:38:30 -0700 Subject: [PATCH 2/6] feat: optimize workspace search with performance improvements - Pre-compile regex patterns to avoid repeated compilation - Cache stringified metadata to reduce JSON serialization overhead - Extract input field processing into reusable helper method - Add input validation to prevent performance issues from long search terms - Add comprehensive error handling for edge cases and malformed data --- src/workspacesProvider.ts | 182 +++++++++++++++++++++++++++++--------- 1 file changed, 139 insertions(+), 43 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index df078d12..3e1a9911 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -43,6 +43,7 @@ export class WorkspaceProvider private fetching = false; private visible = false; private searchFilter = ""; + private metadataCache: Record = {}; constructor( private readonly getWorkspacesQuery: WorkspaceQuery, @@ -54,6 +55,10 @@ export class WorkspaceProvider } setSearchFilter(filter: string) { + // Validate search term length to prevent performance issues + if (filter.length > 200) { + filter = filter.substring(0, 200); + } this.searchFilter = filter; this.refresh(undefined); } @@ -73,6 +78,9 @@ export class WorkspaceProvider } this.fetching = true; + // Clear metadata cache when refreshing to ensure data consistency + this.clearMetadataCache(); + // It is possible we called fetchAndRefresh() manually (through the button // for example), in which case we might still have a pending refresh that // needs to be cleared. @@ -325,76 +333,164 @@ export class WorkspaceProvider } /** - * Check if a workspace matches the given search term using smart search logic. - * Prioritizes exact word matches over substring matches. + * Extract and normalize searchable text fields from a workspace. + * This helper method reduces code duplication between exact word and substring matching. */ - private matchesSearchTerm( - workspace: WorkspaceTreeItem, - searchTerm: string, - ): boolean { - const workspaceName = workspace.workspace.name.toLowerCase(); - const ownerName = workspace.workspace.owner_name.toLowerCase(); + private extractSearchableFields(workspace: WorkspaceTreeItem): { + workspaceName: string; + ownerName: string; + templateName: string; + status: string; + agentNames: string[]; + agentMetadataText: string; + } { + // Handle null/undefined workspace data safely + const workspaceName = (workspace.workspace.name || "").toLowerCase(); + const ownerName = (workspace.workspace.owner_name || "").toLowerCase(); const templateName = ( workspace.workspace.template_display_name || workspace.workspace.template_name || "" ).toLowerCase(); - const status = workspace.workspace.latest_build.status.toLowerCase(); + const status = ( + workspace.workspace.latest_build?.status || "" + ).toLowerCase(); - // Check if any agent names match the search term - const agents = extractAgents(workspace.workspace.latest_build.resources); - const agentNames = agents.map((agent) => agent.name.toLowerCase()); - const hasMatchingAgent = agentNames.some((agentName) => - agentName.includes(searchTerm), + // Extract agent names with null safety + const agents = extractAgents( + workspace.workspace.latest_build?.resources || [], ); + const agentNames = agents + .map((agent) => (agent.name || "").toLowerCase()) + .filter((name) => name.length > 0); - // Check if any agent metadata contains the search term - const hasMatchingMetadata = agents.some((agent) => { - const watcher = this.agentWatchers[agent.id]; - if (watcher?.metadata) { - return watcher.metadata.some((metadata) => { - const metadataStr = JSON.stringify(metadata).toLowerCase(); - return metadataStr.includes(searchTerm); - }); - } - return false; - }); + // Extract and cache agent metadata with error handling + let agentMetadataText = ""; + const metadataCacheKey = agents.map((agent) => agent.id).join(","); - // Smart search: Try exact word match first, then fall back to substring - const searchWords = searchTerm - .split(/\s+/) - .filter((word) => word.length > 0); - const allText = [ + if (this.metadataCache[metadataCacheKey]) { + agentMetadataText = this.metadataCache[metadataCacheKey]; + } else { + const metadataStrings: string[] = []; + agents.forEach((agent) => { + const watcher = this.agentWatchers[agent.id]; + if (watcher?.metadata) { + watcher.metadata.forEach((metadata) => { + try { + metadataStrings.push(JSON.stringify(metadata).toLowerCase()); + } catch (error) { + // Handle JSON serialization errors gracefully + this.storage.output.warn( + `Failed to serialize metadata for agent ${agent.id}: ${error}`, + ); + } + }); + } + }); + agentMetadataText = metadataStrings.join(" "); + this.metadataCache[metadataCacheKey] = agentMetadataText; + } + + return { workspaceName, ownerName, templateName, status, - ...agentNames, + agentNames, + agentMetadataText, + }; + } + + /** + * Check if a workspace matches the given search term using smart search logic. + * Prioritizes exact word matches over substring matches. + */ + private matchesSearchTerm( + workspace: WorkspaceTreeItem, + searchTerm: string, + ): boolean { + // Early return for empty search terms + if (!searchTerm || searchTerm.trim().length === 0) { + return true; + } + + // Extract all searchable fields once + const fields = this.extractSearchableFields(workspace); + + // Pre-compile regex patterns for exact word matching + const searchWords = searchTerm + .split(/\s+/) + .filter((word) => word.length > 0); + + const regexPatterns: RegExp[] = []; + for (const word of searchWords) { + try { + // Escape special regex characters to prevent injection + const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + regexPatterns.push(new RegExp(`\\b${escapedWord}\\b`, "i")); + } catch (error) { + // Handle invalid regex patterns + this.storage.output.warn( + `Invalid regex pattern for search word "${word}": ${error}`, + ); + // Fall back to simple substring matching for this word + continue; + } + } + + // Combine all text for exact word matching + const allText = [ + fields.workspaceName, + fields.ownerName, + fields.templateName, + fields.status, + ...fields.agentNames, + fields.agentMetadataText, ].join(" "); // Check for exact word matches (higher priority) const hasExactWordMatch = - searchWords.length > 0 && - searchWords.some((word) => { - // Escape special regex characters to prevent injection - const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const wordBoundaryRegex = new RegExp(`\\b${escapedWord}\\b`, "i"); - return wordBoundaryRegex.test(allText); + regexPatterns.length > 0 && + regexPatterns.some((pattern) => { + try { + return pattern.test(allText); + } catch (error) { + // Handle regex test errors gracefully + this.storage.output.warn( + `Regex test failed for pattern ${pattern}: ${error}`, + ); + return false; + } }); // Check for substring matches (lower priority) - only if no exact word match const hasSubstringMatch = !hasExactWordMatch && - (workspaceName.includes(searchTerm) || - ownerName.includes(searchTerm) || - templateName.includes(searchTerm) || - status.includes(searchTerm) || - hasMatchingAgent || - hasMatchingMetadata); + (fields.workspaceName.includes(searchTerm) || + fields.ownerName.includes(searchTerm) || + fields.templateName.includes(searchTerm) || + fields.status.includes(searchTerm) || + fields.agentNames.some((agentName) => agentName.includes(searchTerm)) || + fields.agentMetadataText.includes(searchTerm)); // Return true if either exact word match or substring match return hasExactWordMatch || hasSubstringMatch; } + + /** + * Clear the metadata cache when workspaces are refreshed to ensure data consistency. + * Also clears cache if it grows too large to prevent memory issues. + */ + private clearMetadataCache(): void { + // Clear cache if it grows too large (prevent memory issues) + const cacheSize = Object.keys(this.metadataCache).length; + if (cacheSize > 1000) { + this.storage.output.info( + `Clearing metadata cache due to size (${cacheSize} entries)`, + ); + } + this.metadataCache = {}; + } } /** From 9dd25a8a9d3856d8842e1ad53c56627d97b5d968 Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Tue, 29 Jul 2025 15:44:59 -0700 Subject: [PATCH 3/6] feat: add debouncing to workspace search for better user experience - Add 150ms debounce delay to prevent excessive search operations - Implement immediate clear functionality without debouncing - Add proper cleanup for debounce timers to prevent memory leaks - Improve search responsiveness, especially for users with many workspaces --- src/commands.ts | 7 ------- src/extension.ts | 7 +++---- src/workspacesProvider.ts | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index f6b229cd..8fdfc1a0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -724,13 +724,6 @@ export class Commands { quickPick.show(); } - /** - * Clear the workspace search filter. - */ - public clearWorkspaceSearch(): void { - vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", ""); - } - /** * Return agents from the workspace. * diff --git a/src/extension.ts b/src/extension.ts index ad42186d..25f70534 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -303,10 +303,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise { vscode.commands.registerCommand("coder.getWorkspaceSearchFilter", () => { return allWorkspacesProvider.getSearchFilter(); }); - vscode.commands.registerCommand( - "coder.clearWorkspaceSearch", - commands.clearWorkspaceSearch.bind(commands), - ); + vscode.commands.registerCommand("coder.clearWorkspaceSearch", () => { + allWorkspacesProvider.clearSearchFilter(); + }); // Since the "onResolveRemoteAuthority:ssh-remote" activation event exists // in package.json we're able to perform actions before the authority is diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index 3e1a9911..bf9422ce 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -44,6 +44,7 @@ export class WorkspaceProvider private visible = false; private searchFilter = ""; private metadataCache: Record = {}; + private searchDebounceTimer: NodeJS.Timeout | undefined; constructor( private readonly getWorkspacesQuery: WorkspaceQuery, @@ -59,14 +60,38 @@ export class WorkspaceProvider if (filter.length > 200) { filter = filter.substring(0, 200); } - this.searchFilter = filter; - this.refresh(undefined); + + // Clear any existing debounce timer + if (this.searchDebounceTimer) { + clearTimeout(this.searchDebounceTimer); + } + + // Debounce the search operation to improve performance + this.searchDebounceTimer = setTimeout(() => { + this.searchFilter = filter; + this.refresh(undefined); + this.searchDebounceTimer = undefined; + }, 150); // 150ms debounce delay - good balance between responsiveness and performance } getSearchFilter(): string { return this.searchFilter; } + /** + * Clear the search filter immediately without debouncing. + * Use this when the user explicitly clears the search. + */ + clearSearchFilter(): void { + // Clear any pending debounce timer + if (this.searchDebounceTimer) { + clearTimeout(this.searchDebounceTimer); + this.searchDebounceTimer = undefined; + } + this.searchFilter = ""; + this.refresh(undefined); + } + // fetchAndRefresh fetches new workspaces, re-renders the entire tree, then // keeps refreshing (if a timer length was provided) as long as the user is // still logged in and no errors were encountered fetching workspaces. @@ -219,6 +244,11 @@ export class WorkspaceProvider clearTimeout(this.timeout); this.timeout = undefined; } + // clear search debounce timer + if (this.searchDebounceTimer) { + clearTimeout(this.searchDebounceTimer); + this.searchDebounceTimer = undefined; + } } /** From 2d186c72ce8e969678b14ba57da1288b43ef3a18 Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Tue, 29 Jul 2025 16:02:26 -0700 Subject: [PATCH 4/6] update CHANGELOG.md to include changes from Add search functionality PR --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3073ba28..1b4b843f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ have this problem, only new connections are fixed. - Added an agent metadata monitor status bar item, so you can view your active agent metadata at a glance. +- Add search functionality to the "All Workspaces" view with performance optimizations. + - Implement smart search that prioritizes exact word matches over substring matches. + - Add debounced search input (150ms delay) to improve responsiveness during typing. + - Pre-compile regex patterns and cache metadata strings for better performance. + - Include comprehensive error handling for malformed data and edge cases. + - Add input validation to prevent performance issues from long search terms. ## [v1.9.2](https://github.com/coder/vscode-coder/releases/tag/v1.9.2) 2025-06-25 From a5c0ceb69973b5d29183328bd7492b4846d062ab Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Wed, 30 Jul 2025 13:22:57 -0700 Subject: [PATCH 5/6] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ケイラ --- CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03faa6cd..e1f66607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,7 @@ - Add binary signature verification. This can be disabled with `coder.disableSignatureVerification` if you purposefully run a binary that is not signed by Coder (for example a binary you built yourself). -- Add search functionality to the "All Workspaces" view with performance optimizations. - - Implement smart search that prioritizes exact word matches over substring matches. - - Add debounced search input (150ms delay) to improve responsiveness during typing. - - Pre-compile regex patterns and cache metadata strings for better performance. - - Include comprehensive error handling for malformed data and edge cases. - - Add input validation to prevent performance issues from long search terms. +- Add search functionality to the "All Workspaces" view. ## [v1.9.2](https://github.com/coder/vscode-coder/releases/tag/v1.9.2) 2025-06-25 From de2dd41f600e73e1f8aa1bc6a961586451726501 Mon Sep 17 00:00:00 2001 From: yelnatscoding Date: Wed, 30 Jul 2025 15:18:57 -0700 Subject: [PATCH 6/6] removed redundant null safety fallbacks, added cache corruption prevention, update search filter logic to regex word boundaries --- src/workspacesProvider.ts | 59 +++++++++++++-------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts index bf9422ce..7f7237ed 100644 --- a/src/workspacesProvider.ts +++ b/src/workspacesProvider.ts @@ -375,23 +375,20 @@ export class WorkspaceProvider agentMetadataText: string; } { // Handle null/undefined workspace data safely - const workspaceName = (workspace.workspace.name || "").toLowerCase(); - const ownerName = (workspace.workspace.owner_name || "").toLowerCase(); + const workspaceName = workspace.workspace.name.toLowerCase(); + const ownerName = workspace.workspace.owner_name.toLowerCase(); const templateName = ( workspace.workspace.template_display_name || - workspace.workspace.template_name || - "" + workspace.workspace.template_name ).toLowerCase(); const status = ( - workspace.workspace.latest_build?.status || "" + workspace.workspace.latest_build.status || "" ).toLowerCase(); // Extract agent names with null safety - const agents = extractAgents( - workspace.workspace.latest_build?.resources || [], - ); + const agents = extractAgents(workspace.workspace.latest_build.resources); const agentNames = agents - .map((agent) => (agent.name || "").toLowerCase()) + .map((agent) => agent.name.toLowerCase()) .filter((name) => name.length > 0); // Extract and cache agent metadata with error handling @@ -402,6 +399,8 @@ export class WorkspaceProvider agentMetadataText = this.metadataCache[metadataCacheKey]; } else { const metadataStrings: string[] = []; + let hasSerializationErrors = false; + agents.forEach((agent) => { const watcher = this.agentWatchers[agent.id]; if (watcher?.metadata) { @@ -409,6 +408,7 @@ export class WorkspaceProvider try { metadataStrings.push(JSON.stringify(metadata).toLowerCase()); } catch (error) { + hasSerializationErrors = true; // Handle JSON serialization errors gracefully this.storage.output.warn( `Failed to serialize metadata for agent ${agent.id}: ${error}`, @@ -417,8 +417,13 @@ export class WorkspaceProvider }); } }); + agentMetadataText = metadataStrings.join(" "); - this.metadataCache[metadataCacheKey] = agentMetadataText; + + // Only cache if all metadata serialized successfully + if (!hasSerializationErrors) { + this.metadataCache[metadataCacheKey] = agentMetadataText; + } } return { @@ -454,18 +459,8 @@ export class WorkspaceProvider const regexPatterns: RegExp[] = []; for (const word of searchWords) { - try { - // Escape special regex characters to prevent injection - const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - regexPatterns.push(new RegExp(`\\b${escapedWord}\\b`, "i")); - } catch (error) { - // Handle invalid regex patterns - this.storage.output.warn( - `Invalid regex pattern for search word "${word}": ${error}`, - ); - // Fall back to simple substring matching for this word - continue; - } + // Simple word boundary search + regexPatterns.push(new RegExp(`\\b${word}\\b`, "i")); } // Combine all text for exact word matching @@ -481,27 +476,11 @@ export class WorkspaceProvider // Check for exact word matches (higher priority) const hasExactWordMatch = regexPatterns.length > 0 && - regexPatterns.some((pattern) => { - try { - return pattern.test(allText); - } catch (error) { - // Handle regex test errors gracefully - this.storage.output.warn( - `Regex test failed for pattern ${pattern}: ${error}`, - ); - return false; - } - }); + regexPatterns.some((pattern) => pattern.test(allText)); // Check for substring matches (lower priority) - only if no exact word match const hasSubstringMatch = - !hasExactWordMatch && - (fields.workspaceName.includes(searchTerm) || - fields.ownerName.includes(searchTerm) || - fields.templateName.includes(searchTerm) || - fields.status.includes(searchTerm) || - fields.agentNames.some((agentName) => agentName.includes(searchTerm)) || - fields.agentMetadataText.includes(searchTerm)); + !hasExactWordMatch && allText.includes(searchTerm); // Return true if either exact word match or substring match return hasExactWordMatch || hasSubstringMatch;