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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy