diff --git a/ui/src/app/dashboard.component.ts b/ui/src/app/dashboard.component.ts
index f514fdd..b322874 100644
--- a/ui/src/app/dashboard.component.ts
+++ b/ui/src/app/dashboard.component.ts
@@ -5,6 +5,7 @@ import { ProviderDiscoveryService, type ProviderInfo } from '../services/provide
import { TranslationService } from '../services/translation.service';
import { WebSocketService } from '../services/websocket.service';
import { ProviderHostComponent } from '../components/provider-host.component';
+import { UiStateService } from '../services/ui-state.service';
@Component({
selector: 'dashboard-view',
@@ -48,6 +49,10 @@ import { ProviderHostComponent } from '../components/provider-host.component';
API base
{{ apiBase() }}
+
+ Search
+ {{ searchQuery() || 'All providers' }}
+
@@ -58,7 +63,9 @@ import { ProviderHostComponent } from '../components/provider-host.component';
Discovered providers
Renderable capabilities
- {{ providerCount() }} total
+
+ {{ filteredProviders().length }} shown / {{ providerCount() }} total
+
@@ -84,8 +91,16 @@ import { ProviderHostComponent } from '../components/provider-host.component';
- No providers discovered yet.
- The shell will populate this view once the backend exposes provider metadata.
+
+ {{ searchQuery() ? 'No providers match the current search.' : 'No providers discovered yet.' }}
+
+
+ {{
+ searchQuery()
+ ? 'Clear the search box to restore the full provider list.'
+ : 'The shell will populate this view once the backend exposes provider metadata.'
+ }}
+
@@ -103,6 +118,10 @@ import { ProviderHostComponent } from '../components/provider-host.component';
Provider discovery
Loads provider metadata and registers custom element scripts automatically.
+
+ Global search
+ Filters navigation and provider cards from a single shell-level search box.
+
Realtime status
Tracks the websocket connection used for backend events.
@@ -156,6 +175,7 @@ export class DashboardComponent {
private readonly translations = inject(TranslationService);
private readonly websocket = inject(WebSocketService);
private readonly destroyRef = inject(DestroyRef);
+ private readonly uiState = inject(UiStateService);
protected readonly title = signal('Core GUI');
protected readonly subtitle = signal('Desktop orchestration console');
@@ -166,9 +186,31 @@ export class DashboardComponent {
protected readonly providerCount = computed(() => this.providers().length);
protected readonly connected = this.websocket.connected;
protected readonly apiBase = computed(() => this.apiConfig.effectiveBaseUrl);
+ protected readonly searchQuery = this.uiState.searchQuery;
+
+ protected readonly filteredProviders = computed(() => {
+ const query = this.searchQuery().trim().toLowerCase();
+ const providers = this.providers();
+ if (!query) {
+ return providers;
+ }
+
+ return providers.filter((provider) => {
+ const haystack = [
+ provider.name,
+ provider.basePath,
+ provider.status ?? '',
+ provider.element?.tag ?? '',
+ provider.element?.source ?? '',
+ ]
+ .join(' ')
+ .toLowerCase();
+ return haystack.includes(query);
+ });
+ });
protected readonly featuredProviders = computed(() =>
- this.providers().filter((provider) => provider.element?.tag).slice(0, 6),
+ this.filteredProviders().filter((provider) => provider.element?.tag).slice(0, 6),
);
protected readonly selectedRenderableProvider = computed(() => {
@@ -178,7 +220,7 @@ export class DashboardComponent {
}
return (
- this.providers().find((provider) => provider.name === selection && provider.element?.tag) ??
+ this.filteredProviders().find((provider) => provider.name === selection && provider.element?.tag) ??
this.featuredProviders()[0] ??
null
);
diff --git a/ui/src/frame/application-frame.component.html b/ui/src/frame/application-frame.component.html
index 8f5202f..8b65f87 100644
--- a/ui/src/frame/application-frame.component.html
+++ b/ui/src/frame/application-frame.component.html
@@ -17,23 +17,33 @@
-