diff --git a/apps/web/src/app/workspace/workspace-command-palette.component.html b/apps/web/src/app/workspace/workspace-command-palette.component.html
new file mode 100644
index 000000000..14f3ce9e2
--- /dev/null
+++ b/apps/web/src/app/workspace/workspace-command-palette.component.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ @if (commandGroups().length === 0) {
+
No commands found
+ } @else {
+ @for (group of commandGroups(); track group.scope) {
+
+ {{ group.title }}
+
+ @for (command of group.items; track command.id) {
+
+ }
+
+
+ }
+ }
+
+
diff --git a/apps/web/src/app/workspace/workspace-command-palette.component.scss b/apps/web/src/app/workspace/workspace-command-palette.component.scss
new file mode 100644
index 000000000..a60ae5d24
--- /dev/null
+++ b/apps/web/src/app/workspace/workspace-command-palette.component.scss
@@ -0,0 +1,160 @@
+:host {
+ display: block;
+ width: 100%;
+ max-height: 80vh;
+ app-region: no-drag;
+}
+
+.palette-shell {
+ padding: 14px;
+ background: var(--app-header-bg, var(--mat-sys-surface-container-low));
+ color: var(--mat-sys-on-surface);
+ border: 1px solid var(--mat-sys-outline-variant);
+ border-radius: 14px;
+ box-shadow:
+ 0 24px 48px -28px rgba(0, 0, 0, 0.5),
+ 0 10px 24px -18px rgba(0, 0, 0, 0.35);
+}
+
+.palette-header {
+ display: grid;
+ gap: 2px;
+ margin-bottom: 10px;
+
+ h2 {
+ margin: 0;
+ font-size: 1rem;
+ letter-spacing: -0.01em;
+ font-weight: 640;
+ }
+
+ p {
+ margin: 0;
+ font-size: 0.78rem;
+ color: var(--mat-sys-on-surface-variant);
+ }
+}
+
+.palette-search {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border-radius: 10px;
+ border: 1px solid var(--mat-sys-outline-variant);
+ background: var(--mat-sys-surface-container);
+ min-height: 38px;
+ padding: 0 10px;
+
+ &:focus-within {
+ border-color: var(--mat-sys-primary);
+ }
+
+ mat-icon {
+ font-size: 18px;
+ width: 18px;
+ height: 18px;
+ color: var(--mat-sys-on-surface-variant);
+ flex-shrink: 0;
+ }
+
+ input {
+ border: none;
+ outline: none;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ font-size: 0.9rem;
+ width: 100%;
+
+ &::placeholder {
+ color: var(--mat-sys-on-surface-variant);
+ opacity: 0.7;
+ }
+ }
+}
+
+.palette-search__hint {
+ font-size: 0.72rem;
+ color: var(--mat-sys-on-surface-variant);
+ border: 1px solid var(--mat-sys-outline-variant);
+ border-radius: 6px;
+ padding: 2px 6px;
+ line-height: 1;
+ flex-shrink: 0;
+}
+
+.palette-results {
+ max-height: min(56vh, 520px);
+ overflow: auto;
+ margin-top: 12px;
+ display: grid;
+ gap: 10px;
+ padding-right: 2px;
+}
+
+.palette-group {
+ display: grid;
+ gap: 6px;
+
+ h3 {
+ margin: 0;
+ font-size: 0.74rem;
+ color: var(--mat-sys-on-surface-variant);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-weight: 620;
+ }
+}
+
+.palette-group__items {
+ display: grid;
+ gap: 4px;
+}
+
+.palette-command {
+ display: grid;
+ gap: 1px;
+ width: 100%;
+ text-align: left;
+ border: 1px solid transparent;
+ border-radius: 10px;
+ background: transparent;
+ color: inherit;
+ padding: 8px 10px;
+ cursor: pointer;
+
+ &:hover:not(:disabled),
+ &.is-selected:not(:disabled) {
+ background: linear-gradient(
+ 145deg,
+ var(--app-selection-surface-strong),
+ var(--app-selection-surface)
+ );
+ border-color: var(--app-selection-border);
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+ }
+}
+
+.palette-command__label {
+ font-size: 0.9rem;
+ font-weight: 560;
+ letter-spacing: -0.006em;
+}
+
+.palette-command__description {
+ font-size: 0.76rem;
+ color: var(--mat-sys-on-surface-variant);
+}
+
+.palette-empty {
+ border: 1px dashed var(--mat-sys-outline-variant);
+ border-radius: 10px;
+ padding: 18px;
+ text-align: center;
+ color: var(--mat-sys-on-surface-variant);
+ font-size: 0.88rem;
+}
diff --git a/apps/web/src/app/workspace/workspace-command-palette.component.spec.ts b/apps/web/src/app/workspace/workspace-command-palette.component.spec.ts
new file mode 100644
index 000000000..14a95abf2
--- /dev/null
+++ b/apps/web/src/app/workspace/workspace-command-palette.component.spec.ts
@@ -0,0 +1,66 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { WorkspaceCommandPaletteComponent } from './workspace-command-palette.component';
+
+describe('WorkspaceCommandPaletteComponent', () => {
+ let fixture: ComponentFixture;
+ let component: WorkspaceCommandPaletteComponent;
+ let dialogRef: { close: jest.Mock };
+
+ beforeEach(async () => {
+ dialogRef = {
+ close: jest.fn(),
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [WorkspaceCommandPaletteComponent],
+ providers: [
+ {
+ provide: MatDialogRef,
+ useValue: dialogRef,
+ },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: {
+ query: 'recent',
+ commands: [
+ {
+ id: 'global-search',
+ label: 'Search all Xtream playlists',
+ description: 'Open global search overlay',
+ scope: 'global',
+ enabled: true,
+ },
+ {
+ id: 'playlist-search',
+ label: 'Search this playlist',
+ description: 'Open playlist search route',
+ scope: 'playlist',
+ enabled: true,
+ },
+ ],
+ },
+ },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(WorkspaceCommandPaletteComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('creates and applies initial query', () => {
+ expect(component).toBeTruthy();
+ expect(component.query()).toBe('recent');
+ });
+
+ it('closes with selected command and query on click', () => {
+ const command = component.flatCommands()[0];
+ component.onCommandClick(command);
+
+ expect(dialogRef.close).toHaveBeenCalledWith({
+ commandId: 'global-search',
+ query: 'recent',
+ });
+ });
+});
diff --git a/apps/web/src/app/workspace/workspace-command-palette.component.ts b/apps/web/src/app/workspace/workspace-command-palette.component.ts
new file mode 100644
index 000000000..8d83180ab
--- /dev/null
+++ b/apps/web/src/app/workspace/workspace-command-palette.component.ts
@@ -0,0 +1,222 @@
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ computed,
+ effect,
+ inject,
+ signal,
+ viewChild,
+} from '@angular/core';
+import {
+ MAT_DIALOG_DATA,
+ MatDialogModule,
+ MatDialogRef,
+} from '@angular/material/dialog';
+import { MatIconModule } from '@angular/material/icon';
+
+export type WorkspaceCommandScope = 'global' | 'playlist' | 'section';
+
+export type WorkspaceCommandId =
+ | 'global-search'
+ | 'playlist-search'
+ | 'open-global-favorites'
+ | 'open-downloads'
+ | 'open-global-recent';
+
+export interface WorkspaceCommandItem {
+ id: WorkspaceCommandId;
+ label: string;
+ description: string;
+ scope: WorkspaceCommandScope;
+ enabled: boolean;
+}
+
+export interface WorkspaceCommandSelection {
+ commandId: WorkspaceCommandId;
+ query: string;
+}
+
+interface WorkspaceCommandPaletteData {
+ commands: WorkspaceCommandItem[];
+ query?: string;
+}
+
+interface WorkspaceCommandGroup {
+ scope: WorkspaceCommandScope;
+ title: string;
+ items: WorkspaceCommandItem[];
+}
+
+@Component({
+ selector: 'app-workspace-command-palette',
+ imports: [MatDialogModule, MatIconModule],
+ templateUrl: './workspace-command-palette.component.html',
+ styleUrl: './workspace-command-palette.component.scss',
+})
+export class WorkspaceCommandPaletteComponent implements AfterViewInit {
+ private readonly dialogRef = inject(
+ MatDialogRef<
+ WorkspaceCommandPaletteComponent,
+ WorkspaceCommandSelection | undefined
+ >
+ );
+ private readonly data =
+ inject(MAT_DIALOG_DATA);
+
+ private readonly queryInputRef =
+ viewChild>('queryInput');
+
+ readonly query = signal(this.data?.query ?? '');
+ readonly selectedIndex = signal(0);
+
+ readonly filteredCommands = computed(() => {
+ const term = this.query().trim().toLowerCase();
+ if (!term) {
+ return this.data.commands;
+ }
+
+ return this.data.commands.filter((command) => {
+ const haystack =
+ `${command.label} ${command.description}`.toLowerCase();
+ return haystack.includes(term);
+ });
+ });
+
+ readonly commandGroups = computed(() => {
+ const commands = this.filteredCommands();
+ const grouped: WorkspaceCommandGroup[] = [];
+
+ const buildGroup = (
+ scope: WorkspaceCommandScope,
+ title: string
+ ): WorkspaceCommandGroup | null => {
+ const items = commands.filter((command) => command.scope === scope);
+ if (items.length === 0) {
+ return null;
+ }
+ return { scope, title, items };
+ };
+
+ const groups = [
+ buildGroup('global', 'Global actions'),
+ buildGroup('playlist', 'This playlist'),
+ buildGroup('section', 'This section'),
+ ].filter((group): group is WorkspaceCommandGroup => group !== null);
+
+ grouped.push(...groups);
+ return grouped;
+ });
+
+ readonly flatCommands = computed(() =>
+ this.commandGroups().reduce(
+ (items, group) => items.concat(group.items),
+ []
+ )
+ );
+
+ constructor() {
+ effect(() => {
+ const items = this.flatCommands();
+ if (items.length === 0) {
+ this.selectedIndex.set(-1);
+ return;
+ }
+
+ const currentIndex = this.selectedIndex();
+ if (currentIndex < 0 || currentIndex >= items.length) {
+ this.selectedIndex.set(0);
+ }
+ });
+ }
+
+ ngAfterViewInit(): void {
+ queueMicrotask(() => {
+ this.queryInputRef()?.nativeElement.focus();
+ this.queryInputRef()?.nativeElement.select();
+ });
+ }
+
+ onQueryInput(event: Event): void {
+ const target = event.target as HTMLInputElement | null;
+ this.query.set(target?.value ?? '');
+ }
+
+ onInputKeydown(event: KeyboardEvent): void {
+ if (event.key === 'ArrowDown') {
+ event.preventDefault();
+ this.moveSelection(1);
+ return;
+ }
+
+ if (event.key === 'ArrowUp') {
+ event.preventDefault();
+ this.moveSelection(-1);
+ return;
+ }
+
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ this.selectCurrent();
+ return;
+ }
+
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ this.dialogRef.close();
+ }
+ }
+
+ onCommandHover(command: WorkspaceCommandItem): void {
+ const index = this.flatCommands().findIndex(
+ (item) => item.id === command.id
+ );
+ if (index >= 0) {
+ this.selectedIndex.set(index);
+ }
+ }
+
+ onCommandClick(command: WorkspaceCommandItem): void {
+ if (!command.enabled) {
+ return;
+ }
+
+ this.dialogRef.close({
+ commandId: command.id,
+ query: this.query().trim(),
+ });
+ }
+
+ isCommandSelected(command: WorkspaceCommandItem): boolean {
+ const index = this.flatCommands().findIndex(
+ (item) => item.id === command.id
+ );
+ return index >= 0 && this.selectedIndex() === index;
+ }
+
+ private moveSelection(direction: 1 | -1): void {
+ const items = this.flatCommands();
+ if (items.length === 0) {
+ return;
+ }
+
+ const currentIndex = this.selectedIndex();
+ const safeIndex = currentIndex < 0 ? 0 : currentIndex;
+ const nextIndex = (safeIndex + direction + items.length) % items.length;
+ this.selectedIndex.set(nextIndex);
+ }
+
+ private selectCurrent(): void {
+ const items = this.flatCommands();
+ if (items.length === 0) {
+ return;
+ }
+
+ const index = this.selectedIndex();
+ if (index < 0 || index >= items.length) {
+ return;
+ }
+
+ this.onCommandClick(items[index]);
+ }
+}
diff --git a/apps/web/src/app/workspace/workspace-context-panel.component.html b/apps/web/src/app/workspace/workspace-context-panel.component.html
index c6c4d139f..338491b9c 100644
--- a/apps/web/src/app/workspace/workspace-context-panel.component.html
+++ b/apps/web/src/app/workspace/workspace-context-panel.component.html
@@ -2,16 +2,6 @@