diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
index 6c6b44a6..6d376a83 100644
--- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
+++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
@@ -9,7 +9,10 @@ import {
   FrontendApplicationContribution,
   FrontendApplication as TheiaFrontendApplication,
 } from '@theia/core/lib/browser/frontend-application';
-import { LibraryListWidget } from './library/library-list-widget';
+import {
+  LibraryListWidget,
+  LibraryListWidgetSearchOptions,
+} from './library/library-list-widget';
 import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
 import {
   LibraryService,
@@ -25,7 +28,10 @@ import {
 } from '../common/protocol/sketches-service';
 import { SketchesServiceClientImpl } from './sketches-service-client-impl';
 import { CoreService, CoreServicePath } from '../common/protocol/core-service';
-import { BoardsListWidget } from './boards/boards-list-widget';
+import {
+  BoardsListWidget,
+  BoardsListWidgetSearchOptions,
+} from './boards/boards-list-widget';
 import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
 import { BoardsServiceProvider } from './boards/boards-service-provider';
 import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
@@ -73,7 +79,10 @@ import {
 } from '../common/protocol/config-service';
 import { MonitorWidget } from './serial/monitor/monitor-widget';
 import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
-import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
+import {
+  TabBarDecorator,
+  TabBarDecoratorService as TheiaTabBarDecoratorService,
+} from '@theia/core/lib/browser/shell/tab-bar-decorator';
 import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
 import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
 import { ProblemManager } from './theia/markers/problem-manager';
@@ -313,10 +322,10 @@ import { PreferencesEditorWidget } from './theia/preferences/preference-editor-w
 import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
 import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
 import {
-  BoardsFilterRenderer,
-  LibraryFilterRenderer,
-} from './widgets/component-list/filter-renderer';
-import { CheckForUpdates } from './contributions/check-for-updates';
+  CheckForUpdates,
+  BoardsUpdates,
+  LibraryUpdates,
+} from './contributions/check-for-updates';
 import { OutputEditorFactory } from './theia/output/output-editor-factory';
 import { StartupTaskProvider } from '../electron-common/startup-task';
 import { DeleteSketch } from './contributions/delete-sketch';
@@ -356,6 +365,11 @@ import { Account } from './contributions/account';
 import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
 import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
 import { CreateCloudCopy } from './contributions/create-cloud-copy';
+import {
+  BoardsListWidgetTabBarDecorator,
+  LibraryListWidgetTabBarDecorator,
+} from './widgets/component-list/list-widget-tabbar-decorator';
+import { HoverService } from './theia/core/hover-service';
 
 export default new ContainerModule((bind, unbind, isBound, rebind) => {
   // Commands and toolbar items
@@ -371,8 +385,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 
   // Renderer for both the library and the core widgets.
   bind(ListItemRenderer).toSelf().inSingletonScope();
-  bind(LibraryFilterRenderer).toSelf().inSingletonScope();
-  bind(BoardsFilterRenderer).toSelf().inSingletonScope();
 
   // Library service
   bind(LibraryService)
@@ -395,6 +407,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
     LibraryListWidgetFrontendContribution
   );
   bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
+  bind(TabBarToolbarContribution).toService(
+    LibraryListWidgetFrontendContribution
+  );
+  bind(CommandContribution).toService(LibraryListWidgetFrontendContribution);
+  bind(LibraryListWidgetSearchOptions).toSelf().inSingletonScope();
 
   // Sketch list service
   bind(SketchesService)
@@ -464,6 +481,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
     BoardsListWidgetFrontendContribution
   );
   bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
+  bind(TabBarToolbarContribution).toService(
+    BoardsListWidgetFrontendContribution
+  );
+  bind(CommandContribution).toService(BoardsListWidgetFrontendContribution);
+  bind(BoardsListWidgetSearchOptions).toSelf().inSingletonScope();
 
   // Board select dialog
   bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -1034,4 +1056,20 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
   bind(FrontendApplicationContribution).toService(DaemonPort);
   bind(IsOnline).toSelf().inSingletonScope();
   bind(FrontendApplicationContribution).toService(IsOnline);
+
+  bind(HoverService).toSelf().inSingletonScope();
+  bind(LibraryUpdates).toSelf().inSingletonScope();
+  bind(FrontendApplicationContribution).toService(LibraryUpdates);
+  bind(LibraryListWidgetTabBarDecorator).toSelf().inSingletonScope();
+  bind(TabBarDecorator).toService(LibraryListWidgetTabBarDecorator);
+  bind(FrontendApplicationContribution).toService(
+    LibraryListWidgetTabBarDecorator
+  );
+  bind(BoardsUpdates).toSelf().inSingletonScope();
+  bind(FrontendApplicationContribution).toService(BoardsUpdates);
+  bind(BoardsListWidgetTabBarDecorator).toSelf().inSingletonScope();
+  bind(TabBarDecorator).toService(BoardsListWidgetTabBarDecorator);
+  bind(FrontendApplicationContribution).toService(
+    BoardsListWidgetTabBarDecorator
+  );
 });
diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts
index 7067225d..b71f352e 100644
--- a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts
+++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts
@@ -1,3 +1,4 @@
+import { nls } from '@theia/core/lib/common';
 import {
   inject,
   injectable,
@@ -8,10 +9,18 @@ import {
   BoardsPackage,
   BoardsService,
 } from '../../common/protocol/boards-service';
-import { ListWidget } from '../widgets/component-list/list-widget';
 import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
-import { nls } from '@theia/core/lib/common';
-import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
+import {
+  ListWidget,
+  ListWidgetSearchOptions,
+} from '../widgets/component-list/list-widget';
+
+@injectable()
+export class BoardsListWidgetSearchOptions extends ListWidgetSearchOptions<BoardSearch> {
+  get defaultOptions(): Required<BoardSearch> {
+    return { query: '', type: 'All' };
+  }
+}
 
 @injectable()
 export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
@@ -21,7 +30,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
   constructor(
     @inject(BoardsService) service: BoardsService,
     @inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
-    @inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
+    @inject(BoardsListWidgetSearchOptions)
+    searchOptions: BoardsListWidgetSearchOptions
   ) {
     super({
       id: BoardsListWidget.WIDGET_ID,
@@ -31,8 +41,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
       installable: service,
       itemLabel: (item: BoardsPackage) => item.name,
       itemRenderer,
-      filterRenderer,
-      defaultSearchOptions: { query: '', type: 'All' },
+      searchOptions,
     });
   }
 
diff --git a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts
index c64d0869..e483a968 100644
--- a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts
+++ b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts
@@ -1,17 +1,28 @@
-import { injectable } from '@theia/core/shared/inversify';
+import { MenuPath } from '@theia/core';
+import { Command } from '@theia/core/lib/common/command';
+import { nls } from '@theia/core/lib/common/nls';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { Type as TypeLabel } from '../../common/nls';
 import {
   BoardSearch,
   BoardsPackage,
 } from '../../common/protocol/boards-service';
 import { URI } from '../contributions/contribution';
+import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
 import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
-import { BoardsListWidget } from './boards-list-widget';
+import {
+  BoardsListWidget,
+  BoardsListWidgetSearchOptions,
+} from './boards-list-widget';
 
 @injectable()
 export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
   BoardsPackage,
   BoardSearch
 > {
+  @inject(BoardsListWidgetSearchOptions)
+  protected readonly searchOptions: BoardsListWidgetSearchOptions;
+
   constructor() {
     super({
       widgetId: BoardsListWidget.WIDGET_ID,
@@ -37,4 +48,51 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
   protected parse(uri: URI): BoardSearch | undefined {
     return BoardSearch.UriParser.parse(uri);
   }
+
+  protected buildFilterMenuGroup(
+    menuPath: MenuPath
+  ): Array<MenuActionTemplate | SubmenuTemplate> {
+    const typeSubmenuPath = [...menuPath, TypeLabel];
+    return [
+      {
+        submenuPath: typeSubmenuPath,
+        menuLabel: `${TypeLabel}: "${
+          BoardSearch.TypeLabels[this.searchOptions.options.type]
+        }"`,
+        options: { order: String(0) },
+      },
+      ...this.buildMenuActions<BoardSearch.Type>(
+        typeSubmenuPath,
+        BoardSearch.TypeLiterals.slice(),
+        (type) => this.searchOptions.options.type === type,
+        (type) => this.searchOptions.update({ type }),
+        (type) => BoardSearch.TypeLabels[type]
+      ),
+    ];
+  }
+
+  protected get showViewFilterContextMenuCommand(): Command & {
+    label: string;
+  } {
+    return BoardsListWidgetFrontendContribution.Commands
+      .SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU;
+  }
+
+  protected get showInstalledCommandId(): string {
+    return 'arduino-show-installed-boards';
+  }
+
+  protected get showUpdatesCommandId(): string {
+    return 'arduino-show-boards-updates';
+  }
+}
+export namespace BoardsListWidgetFrontendContribution {
+  export namespace Commands {
+    export const SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
+      label: string;
+    } = {
+      id: 'arduino-boards-list-widget-show-filter-context-menu',
+      label: nls.localize('arduino/boards/filterBoards', 'Filter Boards...'),
+    };
+  }
 }
diff --git a/arduino-ide-extension/src/browser/contributions/check-for-updates.ts b/arduino-ide-extension/src/browser/contributions/check-for-updates.ts
index d305f9db..7b2decdb 100644
--- a/arduino-ide-extension/src/browser/contributions/check-for-updates.ts
+++ b/arduino-ide-extension/src/browser/contributions/check-for-updates.ts
@@ -1,45 +1,55 @@
+import { DisposableCollection } from '@theia/core/lib/common/disposable';
+import { FrontendApplicationContribution } from '@theia/core/lib/browser';
 import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
 import { nls } from '@theia/core/lib/common/nls';
 import { inject, injectable } from '@theia/core/shared/inversify';
 import { InstallManually, Later } from '../../common/nls';
 import {
   ArduinoComponent,
+  BoardSearch,
   BoardsPackage,
   BoardsService,
   LibraryPackage,
+  LibrarySearch,
   LibraryService,
   ResponseServiceClient,
   Searchable,
+  Updatable,
 } from '../../common/protocol';
 import { Installable } from '../../common/protocol/installable';
 import { ExecuteWithProgress } from '../../common/protocol/progressible';
 import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
 import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
+import { NotificationCenter } from '../notification-center';
 import { WindowServiceExt } from '../theia/core/window-service-ext';
 import type { ListWidget } from '../widgets/component-list/list-widget';
 import { Command, CommandRegistry, Contribution } from './contribution';
+import { Emitter } from '@theia/core';
+import debounce = require('lodash.debounce');
+import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
+import { ArduinoPreferences } from '../arduino-preferences';
 
-const NoUpdates = nls.localize(
+const noUpdates = nls.localize(
   'arduino/checkForUpdates/noUpdates',
   'There are no recent updates available.'
 );
-const PromptUpdateBoards = nls.localize(
+const promptUpdateBoards = nls.localize(
   'arduino/checkForUpdates/promptUpdateBoards',
   'Updates are available for some of your boards.'
 );
-const PromptUpdateLibraries = nls.localize(
+const promptUpdateLibraries = nls.localize(
   'arduino/checkForUpdates/promptUpdateLibraries',
   'Updates are available for some of your libraries.'
 );
-const UpdatingBoards = nls.localize(
+const updatingBoards = nls.localize(
   'arduino/checkForUpdates/updatingBoards',
   'Updating boards...'
 );
-const UpdatingLibraries = nls.localize(
+const updatingLibraries = nls.localize(
   'arduino/checkForUpdates/updatingLibraries',
   'Updating libraries...'
 );
-const InstallAll = nls.localize(
+const installAll = nls.localize(
   'arduino/checkForUpdates/installAll',
   'Install All'
 );
@@ -49,7 +59,24 @@ interface Task<T extends ArduinoComponent> {
   readonly item: T;
 }
 
-const Updatable = { type: 'Updatable' } as const;
+const updatableLibrariesSearchOption: LibrarySearch = {
+  query: '',
+  topic: 'All',
+  ...Updatable,
+};
+const updatableBoardsSearchOption: BoardSearch = {
+  query: '',
+  ...Updatable,
+};
+const installedLibrariesSearchOptions: LibrarySearch = {
+  query: '',
+  topic: 'All',
+  type: 'Installed',
+};
+const installedBoardsSearchOptions: BoardSearch = {
+  query: '',
+  type: 'Installed',
+};
 
 @injectable()
 export class CheckForUpdates extends Contribution {
@@ -70,6 +97,37 @@ export class CheckForUpdates extends Contribution {
     register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
       execute: () => this.checkForUpdates(false),
     });
+    register.registerCommand(CheckForUpdates.Commands.SHOW_BOARDS_UPDATES, {
+      execute: () =>
+        this.showUpdatableItems(
+          this.boardsContribution,
+          updatableBoardsSearchOption
+        ),
+    });
+    register.registerCommand(CheckForUpdates.Commands.SHOW_LIBRARY_UPDATES, {
+      execute: () =>
+        this.showUpdatableItems(
+          this.librariesContribution,
+          updatableLibrariesSearchOption
+        ),
+    });
+    register.registerCommand(CheckForUpdates.Commands.SHOW_INSTALLED_BOARDS, {
+      execute: () =>
+        this.showUpdatableItems(
+          this.boardsContribution,
+          installedBoardsSearchOptions
+        ),
+    });
+    register.registerCommand(
+      CheckForUpdates.Commands.SHOW_INSTALLED_LIBRARIES,
+      {
+        execute: () =>
+          this.showUpdatableItems(
+            this.librariesContribution,
+            installedLibrariesSearchOptions
+          ),
+      }
+    );
   }
 
   override async onReady(): Promise<void> {
@@ -85,13 +143,13 @@ export class CheckForUpdates extends Contribution {
 
   private async checkForUpdates(silent = true) {
     const [boardsPackages, libraryPackages] = await Promise.all([
-      this.boardsService.search(Updatable),
-      this.libraryService.search(Updatable),
+      this.boardsService.search(updatableBoardsSearchOption),
+      this.libraryService.search(updatableLibrariesSearchOption),
     ]);
     this.promptUpdateBoards(boardsPackages);
     this.promptUpdateLibraries(libraryPackages);
     if (!libraryPackages.length && !boardsPackages.length && !silent) {
-      this.messageService.info(NoUpdates);
+      this.messageService.info(noUpdates);
     }
   }
 
@@ -100,9 +158,9 @@ export class CheckForUpdates extends Contribution {
       items,
       installable: this.boardsService,
       viewContribution: this.boardsContribution,
-      viewSearchOptions: { query: '', ...Updatable },
-      promptMessage: PromptUpdateBoards,
-      updatingMessage: UpdatingBoards,
+      viewSearchOptions: updatableBoardsSearchOption,
+      promptMessage: promptUpdateBoards,
+      updatingMessage: updatingBoards,
     });
   }
 
@@ -111,9 +169,9 @@ export class CheckForUpdates extends Contribution {
       items,
       installable: this.libraryService,
       viewContribution: this.librariesContribution,
-      viewSearchOptions: { query: '', topic: 'All', ...Updatable },
-      promptMessage: PromptUpdateLibraries,
-      updatingMessage: UpdatingLibraries,
+      viewSearchOptions: updatableLibrariesSearchOption,
+      promptMessage: promptUpdateLibraries,
+      updatingMessage: updatingLibraries,
     });
   }
 
@@ -141,21 +199,30 @@ export class CheckForUpdates extends Contribution {
       return;
     }
     this.messageService
-      .info(message, Later, InstallManually, InstallAll)
+      .info(message, Later, InstallManually, installAll)
       .then((answer) => {
-        if (answer === InstallAll) {
+        if (answer === installAll) {
           const tasks = items.map((item) =>
             this.createInstallTask(item, installable)
           );
-          this.executeTasks(updatingMessage, tasks);
+          return this.executeTasks(updatingMessage, tasks);
         } else if (answer === InstallManually) {
-          viewContribution
-            .openView({ reveal: true })
-            .then((widget) => widget.refresh(viewSearchOptions));
+          return this.showUpdatableItems(viewContribution, viewSearchOptions);
         }
       });
   }
 
+  private async showUpdatableItems<
+    T extends ArduinoComponent,
+    S extends Searchable.Options
+  >(
+    viewContribution: AbstractViewContribution<ListWidget<T, S>>,
+    viewSearchOptions: S
+  ): Promise<void> {
+    const widget = await viewContribution.openView({ reveal: true });
+    widget.refresh(viewSearchOptions);
+  }
+
   private async executeTasks(
     message: string,
     tasks: Task<ArduinoComponent>[]
@@ -217,5 +284,127 @@ export namespace CheckForUpdates {
       },
       'arduino/checkForUpdates/checkForUpdates'
     );
+    export const SHOW_BOARDS_UPDATES: Command & { label: string } = {
+      id: 'arduino-show-boards-updates',
+      label: nls.localize(
+        'arduino/checkForUpdates/showBoardsUpdates',
+        'Boards Updates'
+      ),
+      category: 'Arduino',
+    };
+    export const SHOW_LIBRARY_UPDATES: Command & { label: string } = {
+      id: 'arduino-show-library-updates',
+      label: nls.localize(
+        'arduino/checkForUpdates/showLibraryUpdates',
+        'Library Updates'
+      ),
+      category: 'Arduino',
+    };
+    export const SHOW_INSTALLED_BOARDS: Command & { label: string } = {
+      id: 'arduino-show-installed-boards',
+      label: nls.localize(
+        'arduino/checkForUpdates/showInstalledBoards',
+        'Installed Boards'
+      ),
+      category: 'Arduino',
+    };
+    export const SHOW_INSTALLED_LIBRARIES: Command & { label: string } = {
+      id: 'arduino-show-installed-libraries',
+      label: nls.localize(
+        'arduino/checkForUpdates/showInstalledLibraries',
+        'Installed Libraries'
+      ),
+      category: 'Arduino',
+    };
+  }
+}
+
+@injectable()
+abstract class ComponentUpdates<T extends ArduinoComponent>
+  implements FrontendApplicationContribution
+{
+  @inject(FrontendApplicationStateService)
+  private readonly appStateService: FrontendApplicationStateService;
+  @inject(ArduinoPreferences)
+  private readonly preferences: ArduinoPreferences;
+  @inject(NotificationCenter)
+  protected readonly notificationCenter: NotificationCenter;
+  private _updates: T[] | undefined;
+  private readonly onDidChangeEmitter = new Emitter<T[]>();
+  protected readonly toDispose = new DisposableCollection(
+    this.onDidChangeEmitter
+  );
+
+  readonly onDidChange = this.onDidChangeEmitter.event;
+  readonly refresh = debounce(() => this.refreshDebounced(), 200);
+
+  onStart(): void {
+    this.appStateService.reachedState('ready').then(() => this.refresh());
+    this.toDispose.push(
+      this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
+        if (
+          preferenceName === 'arduino.checkForUpdates' &&
+          typeof newValue === 'boolean'
+        ) {
+          this.refresh();
+        }
+      })
+    );
+  }
+
+  onStop(): void {
+    this.toDispose.dispose();
+  }
+
+  get updates(): T[] | undefined {
+    return this._updates;
+  }
+
+  /**
+   * Search updatable components (libraries and platforms) via the CLI.
+   */
+  abstract searchUpdates(): Promise<T[]>;
+
+  private async refreshDebounced(): Promise<void> {
+    const checkForUpdates = this.preferences['arduino.checkForUpdates'];
+    this._updates = checkForUpdates ? await this.searchUpdates() : [];
+    this.onDidChangeEmitter.fire(this._updates.slice());
+  }
+}
+
+@injectable()
+export class LibraryUpdates extends ComponentUpdates<LibraryPackage> {
+  @inject(LibraryService)
+  private readonly libraryService: LibraryService;
+
+  override onStart(): void {
+    super.onStart();
+    this.toDispose.pushAll([
+      this.notificationCenter.onLibraryDidInstall(() => this.refresh()),
+      this.notificationCenter.onLibraryDidUninstall(() => this.refresh()),
+    ]);
+  }
+
+  override searchUpdates(): Promise<LibraryPackage[]> {
+    return this.libraryService.search(updatableLibrariesSearchOption);
+  }
+}
+
+@injectable()
+export class BoardsUpdates extends ComponentUpdates<BoardsPackage> {
+  @inject(BoardsService)
+  private readonly boardsService: BoardsService;
+
+  override onStart(): void {
+    super.onStart();
+    this.toDispose.pushAll([
+      this.notificationCenter.onPlatformDidInstall(() => this.refresh()),
+      this.notificationCenter.onPlatformDidUninstall(() => this.refresh()),
+      this.notificationCenter.onIndexUpdateDidComplete(() => this.refresh()),
+    ]);
+  }
+
+  override searchUpdates(): Promise<BoardsPackage[]> {
+    return this.boardsService.search(updatableBoardsSearchOption);
   }
 }
diff --git a/arduino-ide-extension/src/browser/contributions/sketch-control.ts b/arduino-ide-extension/src/browser/contributions/sketch-control.ts
index 64bbb1ce..9e6f5ef5 100644
--- a/arduino-ide-extension/src/browser/contributions/sketch-control.ts
+++ b/arduino-ide-extension/src/browser/contributions/sketch-control.ts
@@ -8,7 +8,10 @@ import {
 import { nls } from '@theia/core/lib/common/nls';
 import { inject, injectable } from '@theia/core/shared/inversify';
 import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
-import { ArduinoMenus } from '../menu/arduino-menus';
+import {
+  ArduinoMenus,
+  showDisabledContextMenuOptions,
+} from '../menu/arduino-menus';
 import { CurrentSketch } from '../sketches-service-client-impl';
 import {
   Command,
@@ -119,7 +122,7 @@ export class SketchControl extends SketchContribution {
               )
             );
           }
-          const options = {
+          const options = showDisabledContextMenuOptions({
             menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
             anchor: {
               x: parentElement.getBoundingClientRect().left,
@@ -127,8 +130,7 @@ export class SketchControl extends SketchContribution {
                 parentElement.getBoundingClientRect().top +
                 parentElement.offsetHeight,
             },
-            showDisabled: true,
-          };
+          });
           this.contextMenuRenderer.render(options);
         },
       }
diff --git a/arduino-ide-extension/src/browser/data/dark.color-theme.json b/arduino-ide-extension/src/browser/data/dark.color-theme.json
index 9e9d1571..17bb95b2 100644
--- a/arduino-ide-extension/src/browser/data/dark.color-theme.json
+++ b/arduino-ide-extension/src/browser/data/dark.color-theme.json
@@ -38,7 +38,8 @@
     "activityBar.foreground": "#dae3e3",
     "activityBar.inactiveForeground": "#4e5b61",
     "activityBar.activeBorder": "#0ca1a6",
-    "statusBar.background": "#171e21",
+    "activityBarBadge.background": "#008184",
+    "statusBar.background": "#0ca1a6",
     "secondaryButton.background": "#ff000000",
     "secondaryButton.foreground": "#dae3e3",
     "secondaryButton.hoverBackground": "#ffffff1a",
diff --git a/arduino-ide-extension/src/browser/data/default.color-theme.json b/arduino-ide-extension/src/browser/data/default.color-theme.json
index e81e4baa..3b45dc4c 100644
--- a/arduino-ide-extension/src/browser/data/default.color-theme.json
+++ b/arduino-ide-extension/src/browser/data/default.color-theme.json
@@ -38,6 +38,7 @@
     "activityBar.foreground": "#4e5b61",
     "activityBar.inactiveForeground": "#bdc7c7",
     "activityBar.activeBorder": "#008184",
+    "activityBarBadge.background": "#008184",
     "statusBar.background": "#006d70",
     "secondaryButton.background": "#ff000000",
     "secondaryButton.foreground": "#008184",
diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts
index 05078381..1c007802 100644
--- a/arduino-ide-extension/src/browser/library/library-list-widget.ts
+++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts
@@ -1,25 +1,32 @@
+import { DialogProps } from '@theia/core/lib/browser/dialogs';
+import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
+import { nls } from '@theia/core/lib/common/nls';
+import { Message } from '@theia/core/shared/@phosphor/messaging';
 import {
+  inject,
   injectable,
   postConstruct,
-  inject,
 } from '@theia/core/shared/inversify';
-import { Message } from '@theia/core/shared/@phosphor/messaging';
-import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
-import { DialogProps } from '@theia/core/lib/browser/dialogs';
-import { AbstractDialog } from '../theia/dialogs/dialogs';
+import { Installable } from '../../common/protocol';
 import {
   LibraryPackage,
   LibrarySearch,
   LibraryService,
 } from '../../common/protocol/library-service';
+import { AbstractDialog } from '../theia/dialogs/dialogs';
+import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
 import {
   ListWidget,
+  ListWidgetSearchOptions,
   UserAbortError,
 } from '../widgets/component-list/list-widget';
-import { Installable } from '../../common/protocol';
-import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
-import { nls } from '@theia/core/lib/common';
-import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
+
+@injectable()
+export class LibraryListWidgetSearchOptions extends ListWidgetSearchOptions<LibrarySearch> {
+  get defaultOptions(): Required<LibrarySearch> {
+    return { query: '', type: 'All', topic: 'All' };
+  }
+}
 
 @injectable()
 export class LibraryListWidget extends ListWidget<
@@ -35,7 +42,8 @@ export class LibraryListWidget extends ListWidget<
   constructor(
     @inject(LibraryService) private service: LibraryService,
     @inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
-    @inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
+    @inject(LibraryListWidgetSearchOptions)
+    searchOptions: LibraryListWidgetSearchOptions
   ) {
     super({
       id: LibraryListWidget.WIDGET_ID,
@@ -45,8 +53,7 @@ export class LibraryListWidget extends ListWidget<
       installable: service,
       itemLabel: (item: LibraryPackage) => item.name,
       itemRenderer,
-      filterRenderer,
-      defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
+      searchOptions,
     });
   }
 
diff --git a/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts
index 74d5de4a..01fc0383 100644
--- a/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts
+++ b/arduino-ide-extension/src/browser/library/library-widget-frontend-contribution.ts
@@ -1,17 +1,30 @@
-import { nls } from '@theia/core/lib/common';
-import { MenuModelRegistry } from '@theia/core/lib/common/menu';
-import { injectable } from '@theia/core/shared/inversify';
-import { LibraryPackage, LibrarySearch } from '../../common/protocol';
+import { Command } from '@theia/core/lib/common/command';
+import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
+import { nls } from '@theia/core/lib/common/nls';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { Type as TypeLabel } from '../../common/nls';
+import {
+  LibraryPackage,
+  LibrarySearch,
+  TopicLabel,
+} from '../../common/protocol';
 import { URI } from '../contributions/contribution';
 import { ArduinoMenus } from '../menu/arduino-menus';
+import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
 import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
-import { LibraryListWidget } from './library-list-widget';
+import {
+  LibraryListWidget,
+  LibraryListWidgetSearchOptions,
+} from './library-list-widget';
 
 @injectable()
 export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
   LibraryPackage,
   LibrarySearch
 > {
+  @inject(LibraryListWidgetSearchOptions)
+  protected readonly searchOptions: LibraryListWidgetSearchOptions;
+
   constructor() {
     super({
       widgetId: LibraryListWidget.WIDGET_ID,
@@ -38,7 +51,7 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
     }
   }
 
-  protected canParse(uri: URI): boolean {
+  protected override canParse(uri: URI): boolean {
     try {
       LibrarySearch.UriParser.parse(uri);
       return true;
@@ -47,7 +60,72 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
     }
   }
 
-  protected parse(uri: URI): LibrarySearch | undefined {
+  protected override parse(uri: URI): LibrarySearch | undefined {
     return LibrarySearch.UriParser.parse(uri);
   }
+
+  protected override buildFilterMenuGroup(
+    menuPath: MenuPath
+  ): Array<MenuActionTemplate | SubmenuTemplate> {
+    const typeSubmenuPath = [...menuPath, TypeLabel];
+    const topicSubmenuPath = [...menuPath, TopicLabel];
+    return [
+      {
+        submenuPath: typeSubmenuPath,
+        menuLabel: `${TypeLabel}: "${
+          LibrarySearch.TypeLabels[this.searchOptions.options.type]
+        }"`,
+        options: { order: String(0) },
+      },
+      ...this.buildMenuActions<LibrarySearch.Type>(
+        typeSubmenuPath,
+        LibrarySearch.TypeLiterals.slice(),
+        (type) => this.searchOptions.options.type === type,
+        (type) => this.searchOptions.update({ type }),
+        (type) => LibrarySearch.TypeLabels[type]
+      ),
+      {
+        submenuPath: topicSubmenuPath,
+        menuLabel: `${TopicLabel}: "${
+          LibrarySearch.TopicLabels[this.searchOptions.options.topic]
+        }"`,
+        options: { order: String(1) },
+      },
+      ...this.buildMenuActions<LibrarySearch.Topic>(
+        topicSubmenuPath,
+        LibrarySearch.TopicLiterals.slice(),
+        (topic) => this.searchOptions.options.topic === topic,
+        (topic) => this.searchOptions.update({ topic }),
+        (topic) => LibrarySearch.TopicLabels[topic]
+      ),
+    ];
+  }
+
+  protected override get showViewFilterContextMenuCommand(): Command & {
+    label: string;
+  } {
+    return LibraryListWidgetFrontendContribution.Commands
+      .SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU;
+  }
+
+  protected get showInstalledCommandId(): string {
+    return 'arduino-show-installed-libraries';
+  }
+
+  protected get showUpdatesCommandId(): string {
+    return 'arduino-show-library-updates';
+  }
+}
+export namespace LibraryListWidgetFrontendContribution {
+  export namespace Commands {
+    export const SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
+      label: string;
+    } = {
+      id: 'arduino-library-list-widget-show-filter-context-menu',
+      label: nls.localize(
+        'arduino/libraries/filterLibraries',
+        'Filter Libraries...'
+      ),
+    };
+  }
 }
diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts
index 9ecfec55..dd9208eb 100644
--- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts
+++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts
@@ -1,3 +1,4 @@
+import { RenderContextMenuOptions } from '@theia/core/lib/browser';
 import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
 import {
   MAIN_MENU_BAR,
@@ -244,3 +245,13 @@ export class PlaceholderMenuNode implements MenuNode {
 }
 
 export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples');
+
+/**
+ * Helper function to optionally show disabled context menu items in IDE2. They're invisible in Theia.
+ * See `ElectronContextMenuRenderer#showDisabled` for more details.
+ */
+export function showDisabledContextMenuOptions(
+  options: RenderContextMenuOptions
+): RenderContextMenuOptions {
+  return Object.assign(options, { showDisabled: true });
+}
diff --git a/arduino-ide-extension/src/browser/menu/register-menu.ts b/arduino-ide-extension/src/browser/menu/register-menu.ts
new file mode 100644
index 00000000..0f1bfebf
--- /dev/null
+++ b/arduino-ide-extension/src/browser/menu/register-menu.ts
@@ -0,0 +1,151 @@
+import {
+  CommandHandler,
+  CommandRegistry,
+} from '@theia/core/lib/common/command';
+import {
+  Disposable,
+  DisposableCollection,
+} from '@theia/core/lib/common/disposable';
+import {
+  MenuModelRegistry,
+  MenuPath,
+  SubMenuOptions,
+} from '@theia/core/lib/common/menu';
+import { unregisterSubmenu } from './arduino-menus';
+
+export interface MenuTemplate {
+  readonly menuLabel: string;
+}
+
+export function isMenuTemplate(arg: unknown): arg is MenuTemplate {
+  return (
+    typeof arg === 'object' &&
+    (arg as MenuTemplate).menuLabel !== undefined &&
+    typeof (arg as MenuTemplate).menuLabel === 'string'
+  );
+}
+
+export interface MenuActionTemplate extends MenuTemplate {
+  readonly menuPath: MenuPath;
+  readonly handler: CommandHandler;
+  /**
+   * If not defined the insertion oder will be the order string.
+   */
+  readonly order?: string;
+}
+
+export function isMenuActionTemplate(
+  arg: MenuTemplate
+): arg is MenuActionTemplate {
+  return (
+    isMenuTemplate(arg) &&
+    (arg as MenuActionTemplate).handler !== undefined &&
+    typeof (arg as MenuActionTemplate).handler === 'object' &&
+    (arg as MenuActionTemplate).menuPath !== undefined &&
+    Array.isArray((arg as MenuActionTemplate).menuPath)
+  );
+}
+
+export function menuActionWithCommandDelegate(
+  template: Omit<MenuActionTemplate, 'handler' | 'menuLabel'> & {
+    command: string;
+  },
+  commandRegistry: CommandRegistry
+): MenuActionTemplate {
+  const id = template.command;
+  const command = commandRegistry.getCommand(id);
+  if (!command) {
+    throw new Error(`Could not find the registered command with ID: ${id}`);
+  }
+  return {
+    ...template,
+    menuLabel: command.label ?? id,
+    handler: {
+      execute: (args) => commandRegistry.executeCommand(id, args),
+      isEnabled: (args) => commandRegistry.isEnabled(id, args),
+      isVisible: (args) => commandRegistry.isVisible(id, args),
+      isToggled: (args) => commandRegistry.isToggled(id, args),
+    },
+  };
+}
+
+export interface SubmenuTemplate extends MenuTemplate {
+  readonly menuLabel: string;
+  readonly submenuPath: MenuPath;
+  readonly options?: SubMenuOptions;
+}
+
+interface Services {
+  readonly commandRegistry: CommandRegistry;
+  readonly menuRegistry: MenuModelRegistry;
+}
+
+class MenuIndexCounter {
+  private _counter: number;
+  constructor(counter = 0) {
+    this._counter = counter;
+  }
+  getAndIncrement(): number {
+    const counter = this._counter;
+    this._counter++;
+    return counter;
+  }
+}
+
+export function registerMenus(
+  options: {
+    contextId: string;
+    templates: Array<MenuActionTemplate | SubmenuTemplate>;
+  } & Services
+): Disposable {
+  const { templates } = options;
+  const menuIndexCounter = new MenuIndexCounter();
+  return new DisposableCollection(
+    ...templates.map((template) =>
+      registerMenu({ template, menuIndexCounter, ...options })
+    )
+  );
+}
+
+function registerMenu(
+  options: {
+    contextId: string;
+    menuIndexCounter: MenuIndexCounter;
+    template: MenuActionTemplate | SubmenuTemplate;
+  } & Services
+): Disposable {
+  const {
+    template,
+    commandRegistry,
+    menuRegistry,
+    contextId,
+    menuIndexCounter,
+  } = options;
+  if (isMenuActionTemplate(template)) {
+    const { menuLabel, menuPath, handler, order } = template;
+    const id = generateCommandId(contextId, menuLabel, menuPath);
+    const index = menuIndexCounter.getAndIncrement();
+    return new DisposableCollection(
+      commandRegistry.registerCommand({ id }, handler),
+      menuRegistry.registerMenuAction(menuPath, {
+        commandId: id,
+        label: menuLabel,
+        order: typeof order === 'string' ? order : String(index).padStart(4),
+      })
+    );
+  } else {
+    const { menuLabel, submenuPath, options } = template;
+    return new DisposableCollection(
+      menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
+      Disposable.create(() => unregisterSubmenu(submenuPath, menuRegistry))
+    );
+  }
+
+  function generateCommandId(
+    contextId: string,
+    menuLabel: string,
+    menuPath: MenuPath
+  ): string {
+    return `arduino-${contextId}-context-${menuPath.join('-')}-${menuLabel}`;
+  }
+}
diff --git a/arduino-ide-extension/src/browser/style/hover-service.css b/arduino-ide-extension/src/browser/style/hover-service.css
new file mode 100644
index 00000000..0468d424
--- /dev/null
+++ b/arduino-ide-extension/src/browser/style/hover-service.css
@@ -0,0 +1,82 @@
+/* Copied from https://github.com/eclipse-theia/theia/commit/909f4106e8c15c5c2c320401da4f48f8c6080734 */
+/* Remove when IDE2 uses 1.32.0 */
+
+/* Adapted from https://github.com/microsoft/vscode/blob/7d9b1c37f8e5ae3772782ba3b09d827eb3fdd833/src/vs/workbench/services/hover/browser/hoverService.ts */
+
+:root {
+    --theia-hover-max-width: 200px;
+}
+
+.theia-hover {
+    font-family: var(--theia-ui-font-family);
+    font-size: var(--theia-ui-font-size1);
+    color: var(--theia-editorHoverWidget-foreground);
+    background-color: var(--theia-editorHoverWidget-background);
+    border: 1px solid var(--theia-editorHoverWidget-border);
+    padding: var(--theia-ui-padding);
+    max-width: var(--theia-hover-max-width);
+}
+
+.theia-hover .hover-row:not(:first-child):not(:empty) {
+    border-top: 1px solid var(--theia-editorHoverWidgetInternalBorder);
+}
+
+.theia-hover hr {
+    border-top: 1px solid var(--theia-editorHoverWidgetInternalBorder);
+    border-bottom: 0px solid var(--theia-editorHoverWidgetInternalBorder);
+    margin: var(--theia-ui-padding) calc(var(--theia-ui-padding) * -1);
+}
+
+.theia-hover a {
+    color: var(--theia-textLink-foreground);
+}
+
+.theia-hover a:hover {
+    color: var(--theia-textLink-active-foreground);
+}
+
+.theia-hover .hover-row .actions {
+    background-color: var(--theia-editorHoverWidget-statusBarBackground);
+}
+
+.theia-hover code {
+    background-color: var(--theia-textCodeBlock-background);
+    font-family: var(--theia-code-font-family);
+}
+
+.theia-hover::before {
+    content: '';
+    position: absolute;
+}
+
+.theia-hover.top::before {
+    left: var(--theia-hover-before-position);
+    bottom: -5px;
+    border-top: 5px solid var(--theia-editorHoverWidget-border);
+    border-left: 5px solid transparent;
+    border-right: 5px solid transparent;
+}
+
+.theia-hover.bottom::before {
+    left: var(--theia-hover-before-position);
+    top: -5px;
+    border-bottom: 5px solid var(--theia-editorHoverWidget-border);
+    border-left: 5px solid transparent;
+    border-right: 5px solid transparent;
+}
+
+.theia-hover.left::before {
+    top: var(--theia-hover-before-position);
+    right: -5px;
+    border-left: 5px solid var(--theia-editorHoverWidget-border);
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+}
+
+.theia-hover.right::before {
+    top: var(--theia-hover-before-position);
+    left: -5px;
+    border-right: 5px solid var(--theia-editorHoverWidget-border);
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+}
\ No newline at end of file
diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css
index d0ac1e45..07513615 100644
--- a/arduino-ide-extension/src/browser/style/index.css
+++ b/arduino-ide-extension/src/browser/style/index.css
@@ -22,6 +22,7 @@
 
 :root {
   --arduino-button-height: 28px;
+  --arduino-side-panel-min-width: 220px;
 }
 
 /* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */
@@ -68,9 +69,9 @@ body.theia-dark {
 
 /* Makes the sidepanel a bit wider when opening the widget */
 .p-DockPanel-widget {
-  min-width: 220px;
+  min-width: var(--arduino-side-panel-min-width);
   min-height: 20px;
-  height: 220px;
+  height: var(--arduino-side-panel-min-width);
 }
 
 /* Overrule the default Theia CSS button styles. */
diff --git a/arduino-ide-extension/src/browser/theia/core/hover-service.ts b/arduino-ide-extension/src/browser/theia/core/hover-service.ts
new file mode 100644
index 00000000..4dd2bee9
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/core/hover-service.ts
@@ -0,0 +1,225 @@
+// Copied from https://github.com/eclipse-theia/theia/commit/909f4106e8c15c5c2c320401da4f48f8c6080734
+// Remove when IDE2 uses 1.32.0
+
+import { animationFrame } from '@theia/core/lib/browser/browser';
+import {
+  MarkdownRenderer,
+  MarkdownRendererFactory,
+} from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
+import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
+import {
+  Disposable,
+  DisposableCollection,
+  disposableTimeout,
+} from '@theia/core/lib/common/disposable';
+import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
+import { isOSX } from '@theia/core/lib/common/os';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import '../../../../src/browser/style/hover-service.css';
+
+export type HoverPosition = 'left' | 'right' | 'top' | 'bottom';
+
+export namespace HoverPosition {
+  export function invertIfNecessary(
+    position: HoverPosition,
+    target: DOMRect,
+    host: DOMRect,
+    totalWidth: number,
+    totalHeight: number
+  ): HoverPosition {
+    if (position === 'left') {
+      if (target.left - host.width - 5 < 0) {
+        return 'right';
+      }
+    } else if (position === 'right') {
+      if (target.right + host.width + 5 > totalWidth) {
+        return 'left';
+      }
+    } else if (position === 'top') {
+      if (target.top - host.height - 5 < 0) {
+        return 'bottom';
+      }
+    } else if (position === 'bottom') {
+      if (target.bottom + host.height + 5 > totalHeight) {
+        return 'top';
+      }
+    }
+    return position;
+  }
+}
+
+export interface HoverRequest {
+  content: string | MarkdownString | HTMLElement;
+  target: HTMLElement;
+  /**
+   * The position where the hover should appear.
+   * Note that the hover service will try to invert the position (i.e. right -> left)
+   * if the specified content does not fit in the window next to the target element
+   */
+  position: HoverPosition;
+}
+
+@injectable()
+export class HoverService {
+  protected static hostClassName = 'theia-hover';
+  protected static styleSheetId = 'theia-hover-style';
+  @inject(PreferenceService) protected readonly preferences: PreferenceService;
+  @inject(MarkdownRendererFactory)
+  protected readonly markdownRendererFactory: MarkdownRendererFactory;
+
+  protected _markdownRenderer: MarkdownRenderer | undefined;
+  protected get markdownRenderer(): MarkdownRenderer {
+    this._markdownRenderer ||= this.markdownRendererFactory();
+    return this._markdownRenderer;
+  }
+
+  protected _hoverHost: HTMLElement | undefined;
+  protected get hoverHost(): HTMLElement {
+    if (!this._hoverHost) {
+      this._hoverHost = document.createElement('div');
+      this._hoverHost.classList.add(HoverService.hostClassName);
+      this._hoverHost.style.position = 'absolute';
+    }
+    return this._hoverHost;
+  }
+  protected pendingTimeout: Disposable | undefined;
+  protected hoverTarget: HTMLElement | undefined;
+  protected lastHidHover = Date.now();
+  protected readonly disposeOnHide = new DisposableCollection();
+
+  requestHover(request: HoverRequest): void {
+    if (request.target !== this.hoverTarget) {
+      this.cancelHover();
+      this.pendingTimeout = disposableTimeout(
+        () => this.renderHover(request),
+        this.getHoverDelay()
+      );
+    }
+  }
+
+  protected getHoverDelay(): number {
+    return Date.now() - this.lastHidHover < 200
+      ? 0
+      : this.preferences.get('workbench.hover.delay', isOSX ? 1500 : 500);
+  }
+
+  protected async renderHover(request: HoverRequest): Promise<void> {
+    const host = this.hoverHost;
+    const { target, content, position } = request;
+    this.hoverTarget = target;
+    if (content instanceof HTMLElement) {
+      host.appendChild(content);
+    } else if (typeof content === 'string') {
+      host.textContent = content;
+    } else {
+      const renderedContent = this.markdownRenderer.render(content);
+      this.disposeOnHide.push(renderedContent);
+      host.appendChild(renderedContent.element);
+    }
+    // browsers might insert linebreaks when the hover appears at the edge of the window
+    // resetting the position prevents that
+    host.style.left = '0px';
+    host.style.top = '0px';
+    document.body.append(host);
+    await animationFrame(); // Allow the browser to size the host
+    const updatedPosition = this.setHostPosition(target, host, position);
+
+    this.disposeOnHide.push({
+      dispose: () => {
+        this.lastHidHover = Date.now();
+        host.classList.remove(updatedPosition);
+      },
+    });
+
+    this.listenForMouseOut();
+  }
+
+  protected setHostPosition(
+    target: HTMLElement,
+    host: HTMLElement,
+    position: HoverPosition
+  ): HoverPosition {
+    const targetDimensions = target.getBoundingClientRect();
+    const hostDimensions = host.getBoundingClientRect();
+    const documentWidth = document.body.getBoundingClientRect().width;
+    // document.body.getBoundingClientRect().height doesn't work as expected
+    // scrollHeight will always be accurate here: https://stackoverflow.com/a/44077777
+    const documentHeight = document.documentElement.scrollHeight;
+    position = HoverPosition.invertIfNecessary(
+      position,
+      targetDimensions,
+      hostDimensions,
+      documentWidth,
+      documentHeight
+    );
+    if (position === 'top' || position === 'bottom') {
+      const targetMiddleWidth =
+        targetDimensions.left + targetDimensions.width / 2;
+      const middleAlignment = targetMiddleWidth - hostDimensions.width / 2;
+      const furthestRight = Math.min(
+        documentWidth - hostDimensions.width,
+        middleAlignment
+      );
+      const left = Math.max(0, furthestRight);
+      const top =
+        position === 'top'
+          ? targetDimensions.top - hostDimensions.height - 5
+          : targetDimensions.bottom + 5;
+      host.style.setProperty(
+        '--theia-hover-before-position',
+        `${targetMiddleWidth - left - 5}px`
+      );
+      host.style.top = `${top}px`;
+      host.style.left = `${left}px`;
+    } else {
+      const targetMiddleHeight =
+        targetDimensions.top + targetDimensions.height / 2;
+      const middleAlignment = targetMiddleHeight - hostDimensions.height / 2;
+      const furthestTop = Math.min(
+        documentHeight - hostDimensions.height,
+        middleAlignment
+      );
+      const top = Math.max(0, furthestTop);
+      const left =
+        position === 'left'
+          ? targetDimensions.left - hostDimensions.width - 5
+          : targetDimensions.right + 5;
+      host.style.setProperty(
+        '--theia-hover-before-position',
+        `${targetMiddleHeight - top - 5}px`
+      );
+      host.style.left = `${left}px`;
+      host.style.top = `${top}px`;
+    }
+    host.classList.add(position);
+    return position;
+  }
+
+  protected listenForMouseOut(): void {
+    const handleMouseMove = (e: MouseEvent) => {
+      if (
+        e.target instanceof Node &&
+        !this.hoverHost.contains(e.target) &&
+        !this.hoverTarget?.contains(e.target)
+      ) {
+        this.cancelHover();
+      }
+    };
+    document.addEventListener('mousemove', handleMouseMove);
+    this.disposeOnHide.push({
+      dispose: () => document.removeEventListener('mousemove', handleMouseMove),
+    });
+  }
+
+  cancelHover(): void {
+    this.pendingTimeout?.dispose();
+    this.unRenderHover();
+    this.disposeOnHide.dispose();
+    this.hoverTarget = undefined;
+  }
+
+  protected unRenderHover(): void {
+    this.hoverHost.remove();
+    this.hoverHost.replaceChildren();
+  }
+}
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filter-renderer.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filter-renderer.tsx
deleted file mode 100644
index 9f4a9cff..00000000
--- a/arduino-ide-extension/src/browser/widgets/component-list/filter-renderer.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { injectable } from '@theia/core/shared/inversify';
-import * as React from '@theia/core/shared/react';
-import {
-  BoardSearch,
-  LibrarySearch,
-  Searchable,
-} from '../../../common/protocol';
-
-@injectable()
-export abstract class FilterRenderer<S extends Searchable.Options> {
-  render(
-    options: S,
-    handlePropChange: (prop: keyof S, value: S[keyof S]) => void
-  ): React.ReactNode {
-    const props = this.props();
-    return (
-      <div className="filter-bar">
-        {Object.entries(options)
-          .filter(([prop]) => props.includes(prop as keyof S))
-          .map(([prop, value]) => (
-            <div key={prop} className="filter">
-              <div className="filter-label">
-                {`${this.propertyLabel(prop as keyof S)}:`}
-              </div>
-              <select
-                className="theia-select"
-                value={value}
-                onChange={(event) =>
-                  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-                  handlePropChange(prop as keyof S, event.target.value as any)
-                }
-              >
-                {this.options(prop as keyof S).map((key) => (
-                  <option key={key} value={key}>
-                    {this.valueLabel(prop as keyof S, key)}
-                  </option>
-                ))}
-              </select>
-            </div>
-          ))}
-      </div>
-    );
-  }
-  protected abstract props(): (keyof S)[];
-  protected abstract options(prop: keyof S): string[];
-  protected abstract valueLabel(prop: keyof S, key: string): string;
-  protected abstract propertyLabel(prop: keyof S): string;
-}
-
-@injectable()
-export class BoardsFilterRenderer extends FilterRenderer<BoardSearch> {
-  protected props(): (keyof BoardSearch)[] {
-    return ['type'];
-  }
-  protected options(prop: keyof BoardSearch): string[] {
-    switch (prop) {
-      case 'type':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return BoardSearch.TypeLiterals as any;
-      default:
-        throw new Error(`Unexpected prop: ${prop}`);
-    }
-  }
-  protected valueLabel(prop: keyof BoardSearch, key: string): string {
-    switch (prop) {
-      case 'type':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return (BoardSearch.TypeLabels as any)[key];
-      default:
-        throw new Error(`Unexpected key: ${prop}`);
-    }
-  }
-  protected propertyLabel(prop: keyof BoardSearch): string {
-    switch (prop) {
-      case 'type':
-        return BoardSearch.PropertyLabels[prop];
-      default:
-        throw new Error(`Unexpected key: ${prop}`);
-    }
-  }
-}
-
-@injectable()
-export class LibraryFilterRenderer extends FilterRenderer<LibrarySearch> {
-  protected props(): (keyof LibrarySearch)[] {
-    return ['type', 'topic'];
-  }
-  protected options(prop: keyof LibrarySearch): string[] {
-    switch (prop) {
-      case 'type':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return LibrarySearch.TypeLiterals as any;
-      case 'topic':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return LibrarySearch.TopicLiterals as any;
-      default:
-        throw new Error(`Unexpected prop: ${prop}`);
-    }
-  }
-  protected propertyLabel(prop: keyof LibrarySearch): string {
-    switch (prop) {
-      case 'type':
-      case 'topic':
-        return LibrarySearch.PropertyLabels[prop];
-      default:
-        throw new Error(`Unexpected key: ${prop}`);
-    }
-  }
-  protected valueLabel(prop: keyof LibrarySearch, key: string): string {
-    switch (prop) {
-      case 'type':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return (LibrarySearch.TypeLabels as any)[key] as any;
-      case 'topic':
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        return (LibrarySearch.TopicLabels as any)[key] as any;
-      default:
-        throw new Error(`Unexpected prop: ${prop}`);
-    }
-  }
-}
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx
index 05e0e95b..d9f4f153 100644
--- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx
+++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx
@@ -9,12 +9,11 @@ import { ExecuteWithProgress } from '../../../common/protocol/progressible';
 import { Installable } from '../../../common/protocol/installable';
 import { ArduinoComponent } from '../../../common/protocol/arduino-component';
 import { SearchBar } from './search-bar';
-import { ListWidget } from './list-widget';
+import { ListWidget, ListWidgetSearchOptions } from './list-widget';
 import { ComponentList } from './component-list';
 import { ListItemRenderer } from './list-item-renderer';
 import { ResponseServiceClient } from '../../../common/protocol';
 import { nls } from '@theia/core/lib/common';
-import { FilterRenderer } from './filter-renderer';
 import { DisposableCollection } from '@theia/core/lib/common/disposable';
 
 export class FilterableListContainer<
@@ -29,7 +28,7 @@ export class FilterableListContainer<
   constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
     super(props);
     this.state = {
-      searchOptions: props.defaultSearchOptions,
+      searchOptions: props.searchOptions.options,
       items: [],
     };
     this.toDispose = new DisposableCollection();
@@ -39,7 +38,7 @@ export class FilterableListContainer<
     this.search = debounce(this.search, 500, { trailing: true });
     this.search(this.state.searchOptions);
     this.toDispose.pushAll([
-      this.props.searchOptionsDidChange((newSearchOptions) => {
+      this.props.searchOptions.onDidChange((newSearchOptions) => {
         const { searchOptions } = this.state;
         this.setSearchOptionsAndUpdate({
           ...searchOptions,
@@ -64,7 +63,6 @@ export class FilterableListContainer<
     return (
       <div className={'filterable-list-container'}>
         {this.renderSearchBar()}
-        {this.renderSearchFilter()}
         <div className="filterable-list-container">
           {this.renderComponentList()}
         </div>
@@ -72,17 +70,6 @@ export class FilterableListContainer<
     );
   }
 
-  protected renderSearchFilter(): React.ReactNode {
-    return (
-      <>
-        {this.props.filterRenderer.render(
-          this.state.searchOptions,
-          this.handlePropChange.bind(this)
-        )}
-      </>
-    );
-  }
-
   protected renderSearchBar(): React.ReactNode {
     return (
       <SearchBar
@@ -115,7 +102,7 @@ export class FilterableListContainer<
       ...this.state.searchOptions,
       [prop]: value,
     };
-    this.setSearchOptionsAndUpdate(searchOptions);
+    this.props.searchOptions.update(searchOptions);
   };
 
   private setSearchOptionsAndUpdate(searchOptions: S) {
@@ -180,14 +167,12 @@ export namespace FilterableListContainer {
     T extends ArduinoComponent,
     S extends Searchable.Options
   > {
-    readonly defaultSearchOptions: S;
+    readonly searchOptions: ListWidgetSearchOptions<S>;
     readonly container: ListWidget<T, S>;
     readonly searchable: Searchable<T, S>;
     readonly itemLabel: (item: T) => string;
     readonly itemRenderer: ListItemRenderer<T>;
-    readonly filterRenderer: FilterRenderer<S>;
     readonly resolveFocus: (element: HTMLElement | undefined) => void;
-    readonly searchOptionsDidChange: Event<Partial<S> | undefined>;
     readonly messageService: MessageService;
     readonly responseService: ResponseServiceClient;
     readonly onDidShow: Event<void>;
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx
index 945b563d..238a4a6f 100644
--- a/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx
+++ b/arduino-ide-extension/src/browser/widgets/component-list/list-item-renderer.tsx
@@ -1,4 +1,3 @@
-import { ApplicationError } from '@theia/core';
 import {
   Anchor,
   ContextMenuRenderer,
@@ -6,20 +5,14 @@ import {
 import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
 import { codicon } from '@theia/core/lib/browser/widgets/widget';
 import { WindowService } from '@theia/core/lib/browser/window/window-service';
+import { ApplicationError } from '@theia/core/lib/common/application-error';
 import {
-  CommandHandler,
   CommandRegistry,
   CommandService,
 } from '@theia/core/lib/common/command';
-import {
-  Disposable,
-  DisposableCollection,
-} from '@theia/core/lib/common/disposable';
-import {
-  MenuModelRegistry,
-  MenuPath,
-  SubMenuOptions,
-} from '@theia/core/lib/common/menu';
+import { DisposableCollection } from '@theia/core/lib/common/disposable';
+import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
+import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
 import { MessageService } from '@theia/core/lib/common/message-service';
 import { nls } from '@theia/core/lib/common/nls';
 import { inject, injectable } from '@theia/core/shared/inversify';
@@ -33,6 +26,7 @@ import {
   SketchContainer,
   SketchesService,
   SketchRef,
+  TopicLabel,
 } from '../../../common/protocol';
 import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
 import { Installable } from '../../../common/protocol/installable';
@@ -40,8 +34,14 @@ import { openClonedExample } from '../../contributions/examples';
 import {
   ArduinoMenus,
   examplesLabel,
-  unregisterSubmenu,
+  showDisabledContextMenuOptions,
 } from '../../menu/arduino-menus';
+import {
+  MenuActionTemplate,
+  registerMenus,
+  SubmenuTemplate,
+} from '../../menu/register-menu';
+import { HoverService } from '../../theia/core/hover-service';
 
 const moreInfoLabel = nls.localize('arduino/component/moreInfo', 'More info');
 const otherVersionsLabel = nls.localize(
@@ -63,9 +63,6 @@ function installVersionLabel(selectedVersion: string) {
 const updateLabel = nls.localize('arduino/component/update', 'Update');
 const removeLabel = nls.localize('arduino/component/remove', 'Remove');
 const byLabel = nls.localize('arduino/component/by', 'by');
-function nameAuthorLabel(name: string, author: string) {
-  return nls.localize('arduino/component/title', '{0} by {1}', name, author);
-}
 function installedLabel(installedVersion: string) {
   return nls.localize(
     'arduino/component/installed',
@@ -81,39 +78,6 @@ function clickToOpenInBrowserLabel(href: string): string | undefined {
   );
 }
 
-interface MenuTemplate {
-  readonly menuLabel: string;
-}
-interface MenuActionTemplate extends MenuTemplate {
-  readonly menuPath: MenuPath;
-  readonly handler: CommandHandler;
-  /**
-   * If not defined the insertion oder will be the order string.
-   */
-  readonly order?: string;
-}
-interface SubmenuTemplate extends MenuTemplate {
-  readonly menuLabel: string;
-  readonly submenuPath: MenuPath;
-  readonly options?: SubMenuOptions;
-}
-function isMenuTemplate(arg: unknown): arg is MenuTemplate {
-  return (
-    typeof arg === 'object' &&
-    (arg as MenuTemplate).menuLabel !== undefined &&
-    typeof (arg as MenuTemplate).menuLabel === 'string'
-  );
-}
-function isMenuActionTemplate(arg: MenuTemplate): arg is MenuActionTemplate {
-  return (
-    isMenuTemplate(arg) &&
-    (arg as MenuActionTemplate).handler !== undefined &&
-    typeof (arg as MenuActionTemplate).handler === 'object' &&
-    (arg as MenuActionTemplate).menuPath !== undefined &&
-    Array.isArray((arg as MenuActionTemplate).menuPath)
-  );
-}
-
 @injectable()
 export class ArduinoComponentContextMenuRenderer {
   @inject(CommandRegistry)
@@ -124,54 +88,26 @@ export class ArduinoComponentContextMenuRenderer {
   private readonly contextMenuRenderer: ContextMenuRenderer;
 
   private readonly toDisposeBeforeRender = new DisposableCollection();
-  private menuIndexCounter = 0;
 
   async render(
     anchor: Anchor,
-    ...templates: (MenuActionTemplate | SubmenuTemplate)[]
+    ...templates: Array<MenuActionTemplate | SubmenuTemplate>
   ): Promise<void> {
     this.toDisposeBeforeRender.dispose();
-    this.toDisposeBeforeRender.pushAll([
-      Disposable.create(() => (this.menuIndexCounter = 0)),
-      ...templates.map((template) => this.registerMenu(template)),
-    ]);
-    const options = {
+    this.toDisposeBeforeRender.push(
+      registerMenus({
+        contextId: 'component',
+        commandRegistry: this.commandRegistry,
+        menuRegistry: this.menuRegistry,
+        templates,
+      })
+    );
+    const options = showDisabledContextMenuOptions({
       menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
       anchor,
-      showDisabled: true,
-    };
+    });
     this.contextMenuRenderer.render(options);
   }
-
-  private registerMenu(
-    template: MenuActionTemplate | SubmenuTemplate
-  ): Disposable {
-    if (isMenuActionTemplate(template)) {
-      const { menuLabel, menuPath, handler, order } = template;
-      const id = this.generateCommandId(menuLabel, menuPath);
-      const index = this.menuIndexCounter++;
-      return new DisposableCollection(
-        this.commandRegistry.registerCommand({ id }, handler),
-        this.menuRegistry.registerMenuAction(menuPath, {
-          commandId: id,
-          label: menuLabel,
-          order: typeof order === 'string' ? order : String(index).padStart(4),
-        })
-      );
-    } else {
-      const { menuLabel, submenuPath, options } = template;
-      return new DisposableCollection(
-        this.menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
-        Disposable.create(() =>
-          unregisterSubmenu(submenuPath, this.menuRegistry)
-        )
-      );
-    }
-  }
-
-  private generateCommandId(menuLabel: string, menuPath: MenuPath): string {
-    return `arduino--component-context-${menuPath.join('-')}-${menuLabel}`;
-  }
 }
 
 interface ListItemRendererParams<T extends ArduinoComponent> {
@@ -201,6 +137,8 @@ export class ListItemRenderer<T extends ArduinoComponent> {
   private readonly messageService: MessageService;
   @inject(CommandService)
   private readonly commandService: CommandService;
+  @inject(HoverService)
+  private readonly hoverService: HoverService;
   @inject(CoreService)
   private readonly coreService: CoreService;
   @inject(ExamplesService)
@@ -216,12 +154,26 @@ export class ListItemRenderer<T extends ArduinoComponent> {
     }
   };
 
+  private readonly showHover = (
+    event: React.MouseEvent<HTMLElement>,
+    markdown: string
+  ) => {
+    this.hoverService.requestHover({
+      content: new MarkdownStringImpl(markdown),
+      target: event.currentTarget,
+      position: 'right',
+    });
+  };
+
   renderItem(params: ListItemRendererParams<T>): React.ReactNode {
     const action = this.action(params);
     return (
       <>
         <Separator />
-        <div className="component-list-item noselect">
+        <div
+          className="component-list-item noselect"
+          onMouseEnter={(event) => this.showHover(event, this.markdown(params))}
+        >
           <Header
             params={params}
             action={action}
@@ -247,6 +199,30 @@ export class ListItemRenderer<T extends ArduinoComponent> {
     });
   }
 
+  private markdown(params: ListItemRendererParams<T>): string {
+    // TODO: dedicated library and boards services for the markdown content generation
+    const {
+      item,
+      item: { name, author, description, summary, installedVersion },
+    } = params;
+    let title = `__${name}__ ${byLabel} ${author}`;
+    if (installedVersion) {
+      title += `\n\n(${installedLabel(`\`${installedVersion}\``)})`;
+    }
+    if (LibraryPackage.is(item)) {
+      let content = `\n\n${summary}`;
+      // do not repeat the same info if paragraph and sentence are the same
+      // example: https://github.com/arduino-libraries/ArduinoCloudThing/blob/8cbcee804e99fed614366c1b87143b1f1634c45f/library.properties#L5-L6
+      if (description !== summary) {
+        content += `\n_____\n\n${description}`;
+      }
+      return `${title}\n\n____${content}\n\n____\n${TopicLabel}: \`${item.category}\``;
+    }
+    return `${title}\n\n____\n\n${summary}\n\n - ${description
+      .split(',')
+      .join('\n - ')}`;
+  }
+
   private get services(): ListItemRendererServices {
     return {
       windowService: this.windowService,
@@ -361,7 +337,7 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
     };
   }
 
-  private get examples(): Promise<(MenuActionTemplate | SubmenuTemplate)[]> {
+  private get examples(): Promise<Array<MenuActionTemplate | SubmenuTemplate>> {
     const {
       params: {
         item,
@@ -394,8 +370,8 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
     container: SketchContainer,
     menuPath: MenuPath,
     depth = 0
-  ): (MenuActionTemplate | SubmenuTemplate)[] {
-    const templates: (MenuActionTemplate | SubmenuTemplate)[] = [];
+  ): Array<MenuActionTemplate | SubmenuTemplate> {
+    const templates: Array<MenuActionTemplate | SubmenuTemplate> = [];
     const { label } = container;
     if (depth > 0) {
       menuPath = [...menuPath, label];
@@ -464,7 +440,7 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
     };
   }
 
-  private get otherVersions(): (MenuActionTemplate | SubmenuTemplate)[] {
+  private get otherVersions(): Array<MenuActionTemplate | SubmenuTemplate> {
     const {
       params: {
         item: { availableVersions },
@@ -566,10 +542,8 @@ class Title<T extends ArduinoComponent> extends React.Component<
 > {
   override render(): React.ReactNode {
     const { name, author } = this.props.params.item;
-    const title =
-      name && author ? nameAuthorLabel(name, author) : name ? name : Unknown;
     return (
-      <div className="title" title={title}>
+      <div className="title">
         {name && author ? (
           <>
             {<span className="name">{name}</span>}{' '}
@@ -627,7 +601,7 @@ class Content<T extends ArduinoComponent> extends React.Component<
     } = this.props;
     const content = [summary, description].filter(Boolean).join(' ');
     return (
-      <div className="content" title={content}>
+      <div className="content">
         <p>{content}</p>
         <MoreInfo {...this.props} />
       </div>
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/widgets/component-list/list-widget-frontend-contribution.ts
index 56dec744..bc67ee7c 100644
--- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget-frontend-contribution.ts
+++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget-frontend-contribution.ts
@@ -1,15 +1,39 @@
+import {
+  Disposable,
+  DisposableCollection,
+} from '@theia/core/lib/common/disposable';
+import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
 import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
 import {
   OpenerOptions,
   OpenHandler,
 } from '@theia/core/lib/browser/opener-service';
+import {
+  TabBarToolbarContribution,
+  TabBarToolbarRegistry,
+} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
 import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
-import { MenuModelRegistry } from '@theia/core/lib/common/menu';
+import { codicon } from '@theia/core/lib/browser/widgets/widget';
+import {
+  Command,
+  CommandContribution,
+  CommandRegistry,
+} from '@theia/core/lib/common/command';
+import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
 import { URI } from '@theia/core/lib/common/uri';
-import { injectable } from '@theia/core/shared/inversify';
+import { Widget } from '@theia/core/shared/@phosphor/widgets';
+import { inject, injectable } from '@theia/core/shared/inversify';
 import { Searchable } from '../../../common/protocol';
 import { ArduinoComponent } from '../../../common/protocol/arduino-component';
-import { ListWidget } from './list-widget';
+import { showDisabledContextMenuOptions } from '../../menu/arduino-menus';
+import {
+  MenuActionTemplate,
+  menuActionWithCommandDelegate,
+  registerMenus,
+  SubmenuTemplate,
+} from '../../menu/register-menu';
+import { ListWidget, ListWidgetSearchOptions } from './list-widget';
+import { Event, nls } from '@theia/core';
 
 @injectable()
 export abstract class ListWidgetFrontendContribution<
@@ -17,14 +41,32 @@ export abstract class ListWidgetFrontendContribution<
     S extends Searchable.Options
   >
   extends AbstractViewContribution<ListWidget<T, S>>
-  implements FrontendApplicationContribution, OpenHandler
+  implements
+    FrontendApplicationContribution,
+    OpenHandler,
+    TabBarToolbarContribution,
+    CommandContribution
 {
+  @inject(ContextMenuRenderer)
+  private readonly contextMenuRenderer: ContextMenuRenderer;
+  @inject(CommandRegistry)
+  private readonly commandRegistry: CommandRegistry;
+  @inject(MenuModelRegistry)
+  private readonly menuRegistry: MenuModelRegistry;
+  protected abstract readonly searchOptions: ListWidgetSearchOptions<S>;
+
+  private readonly toDisposeBeforeShowContextMenu = new DisposableCollection();
+
   readonly id: string = `http-opener-${this.viewId}`;
 
   async initializeLayout(): Promise<void> {
     this.openView();
   }
 
+  onStop(): void {
+    this.toDisposeBeforeShowContextMenu.dispose();
+  }
+
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   override registerMenus(_: MenuModelRegistry): void {
     // NOOP
@@ -62,4 +104,131 @@ export abstract class ListWidgetFrontendContribution<
 
   protected abstract canParse(uri: URI): boolean;
   protected abstract parse(uri: URI): S | undefined;
+
+  registerToolbarItems(registry: TabBarToolbarRegistry): void {
+    const filterCommand = this.showViewFilterContextMenuCommand;
+    registry.registerItem({
+      id: filterCommand.id,
+      command: filterCommand.id,
+      icon: () =>
+        codicon(
+          this.searchOptions.hasFilters() ? 'filter-filled' : 'filter',
+          true
+        ),
+      onDidChange: this.searchOptions
+        .onDidChange as Event<unknown> as Event<void>,
+    });
+  }
+
+  override registerCommands(registry: CommandRegistry): void {
+    const filterCommand = this.showViewFilterContextMenuCommand;
+    registry.registerCommand(filterCommand, {
+      execute: () => this.showFilterContextMenu(filterCommand.id),
+      isVisible: (arg: unknown) =>
+        arg instanceof Widget && arg.id === this.viewId,
+    });
+  }
+
+  protected abstract get showViewFilterContextMenuCommand(): Command & {
+    label: string;
+  };
+
+  protected abstract get showInstalledCommandId(): string;
+
+  protected abstract get showUpdatesCommandId(): string;
+
+  protected abstract buildFilterMenuGroup(
+    menuPath: MenuPath
+  ): Array<MenuActionTemplate | SubmenuTemplate>;
+
+  private buildQuickFiltersMenuGroup(
+    menuPath: MenuPath
+  ): Array<MenuActionTemplate | SubmenuTemplate> {
+    return [
+      menuActionWithCommandDelegate(
+        {
+          menuPath,
+          command: this.showInstalledCommandId,
+        },
+        this.commandRegistry
+      ),
+      menuActionWithCommandDelegate(
+        { menuPath, command: this.showUpdatesCommandId },
+        this.commandRegistry
+      ),
+    ];
+  }
+
+  private buildActionsMenuGroup(
+    menuPath: MenuPath
+  ): Array<MenuActionTemplate | SubmenuTemplate> {
+    if (!this.searchOptions.hasFilters()) {
+      return [];
+    }
+    return [
+      {
+        menuPath,
+        menuLabel: nls.localize('arduino/filter/clearAll', 'Clear All Filters'),
+        handler: {
+          execute: () => this.searchOptions.clearFilters(),
+        },
+      },
+    ];
+  }
+
+  protected buildMenuActions<T>(
+    menuPath: MenuPath,
+    literals: T[],
+    isSelected: (literal: T) => boolean,
+    select: (literal: T) => void,
+    menuLabelProvider: (literal: T) => string
+  ): MenuActionTemplate[] {
+    return literals
+      .map((literal) => ({ literal, label: menuLabelProvider(literal) }))
+      .map(({ literal, label }) => ({
+        menuPath,
+        menuLabel: label,
+        handler: {
+          execute: () => select(literal),
+          isToggled: () => isSelected(literal),
+        },
+      }));
+  }
+
+  private showFilterContextMenu(commandId: string): void {
+    this.toDisposeBeforeShowContextMenu.dispose();
+    const element = document.getElementById(commandId);
+    if (!element) {
+      return;
+    }
+    const client = element.getBoundingClientRect();
+    const menuPath = [`${this.viewId}-filter-context-menu`];
+    this.toDisposeBeforeShowContextMenu.pushAll([
+      this.registerMenuGroup(
+        this.buildFilterMenuGroup([...menuPath, '0_filter'])
+      ),
+      this.registerMenuGroup(
+        this.buildQuickFiltersMenuGroup([...menuPath, '1_quick_filters'])
+      ),
+      this.registerMenuGroup(
+        this.buildActionsMenuGroup([...menuPath, '2_actions'])
+      ),
+    ]);
+    const options = showDisabledContextMenuOptions({
+      menuPath,
+      anchor: { x: client.left, y: client.bottom + client.height / 2 },
+    });
+    this.contextMenuRenderer.render(options);
+  }
+
+  private registerMenuGroup(
+    templates: Array<MenuActionTemplate | SubmenuTemplate>
+  ): Disposable {
+    return registerMenus({
+      commandRegistry: this.commandRegistry,
+      menuRegistry: this.menuRegistry,
+      contextId: this.viewId,
+      templates,
+    });
+  }
 }
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget-tabbar-decorator.ts b/arduino-ide-extension/src/browser/widgets/component-list/list-widget-tabbar-decorator.ts
new file mode 100644
index 00000000..0958c2a2
--- /dev/null
+++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget-tabbar-decorator.ts
@@ -0,0 +1,109 @@
+import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
+import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
+import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
+import { DisposableCollection } from '@theia/core/lib/common/disposable';
+import { Emitter, Event } from '@theia/core/lib/common/event';
+import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { BoardsListWidget } from '../../boards/boards-list-widget';
+import {
+  BoardsUpdates,
+  LibraryUpdates,
+} from '../../contributions/check-for-updates';
+import { LibraryListWidget } from '../../library/library-list-widget';
+import { NotificationCenter } from '../../notification-center';
+
+@injectable()
+abstract class ListWidgetTabBarDecorator
+  implements TabBarDecorator, FrontendApplicationContribution
+{
+  @inject(NotificationCenter)
+  protected readonly notificationCenter: NotificationCenter;
+
+  private count = 0;
+  private readonly onDidChangeDecorationsEmitter = new Emitter<void>();
+  protected readonly toDispose = new DisposableCollection(
+    this.onDidChangeDecorationsEmitter
+  );
+
+  abstract readonly id: string;
+  readonly onDidChangeDecorations: Event<void> =
+    this.onDidChangeDecorationsEmitter.event;
+
+  onStop(): void {
+    this.toDispose.dispose();
+  }
+
+  decorate(title: Title<Widget>): WidgetDecoration.Data[] {
+    const { owner } = title;
+    if (this.isListWidget(owner)) {
+      if (this.count > 0) {
+        return [{ badge: this.count }];
+      }
+    }
+    return [];
+  }
+
+  protected async update(count: number): Promise<void> {
+    this.count = count;
+    this.onDidChangeDecorationsEmitter.fire();
+  }
+
+  protected abstract isListWidget(widget: Widget): boolean;
+
+  protected abstract get updatableCount(): number | undefined;
+}
+
+@injectable()
+export class LibraryListWidgetTabBarDecorator extends ListWidgetTabBarDecorator {
+  @inject(LibraryUpdates)
+  private readonly libraryUpdates: LibraryUpdates;
+
+  readonly id = `${LibraryListWidget.WIDGET_ID}-badge-decorator`;
+
+  onStart(): void {
+    this.toDispose.push(
+      this.libraryUpdates.onDidChange((libraries) =>
+        this.update(libraries.length)
+      )
+    );
+    const count = this.updatableCount;
+    if (count) {
+      this.update(count);
+    }
+  }
+
+  protected isListWidget(widget: Widget): boolean {
+    return widget instanceof LibraryListWidget;
+  }
+
+  protected get updatableCount(): number | undefined {
+    return this.libraryUpdates.updates?.length;
+  }
+}
+
+@injectable()
+export class BoardsListWidgetTabBarDecorator extends ListWidgetTabBarDecorator {
+  @inject(BoardsUpdates)
+  private readonly boardsUpdates: BoardsUpdates;
+
+  readonly id = `${BoardsListWidget.WIDGET_ID}-badge-decorator`;
+
+  onStart(): void {
+    this.toDispose.push(
+      this.boardsUpdates.onDidChange((boards) => this.update(boards.length))
+    );
+    const count = this.updatableCount;
+    if (count) {
+      this.update(count);
+    }
+  }
+
+  protected isListWidget(widget: Widget): boolean {
+    return widget instanceof BoardsListWidget;
+  }
+
+  protected get updatableCount(): number | undefined {
+    return this.boardsUpdates.updates?.length;
+  }
+}
diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx
index ca340a01..e6dfd41b 100644
--- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx
+++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx
@@ -6,7 +6,7 @@ import {
 } from '@theia/core/shared/inversify';
 import { Widget } from '@theia/core/shared/@phosphor/widgets';
 import { Message } from '@theia/core/shared/@phosphor/messaging';
-import { Emitter } from '@theia/core/lib/common/event';
+import { Emitter, Event } from '@theia/core/lib/common/event';
 import { Deferred } from '@theia/core/lib/common/promise-util';
 import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
 import { CommandService } from '@theia/core/lib/common/command';
@@ -20,13 +20,16 @@ import {
 import { FilterableListContainer } from './filterable-list-container';
 import { ListItemRenderer } from './list-item-renderer';
 import { NotificationCenter } from '../../notification-center';
-import { FilterRenderer } from './filter-renderer';
+import { StatefulWidget } from '@theia/core/lib/browser';
 
 @injectable()
 export abstract class ListWidget<
-  T extends ArduinoComponent,
-  S extends Searchable.Options
-> extends ReactWidget {
+    T extends ArduinoComponent,
+    S extends Searchable.Options
+  >
+  extends ReactWidget
+  implements StatefulWidget
+{
   @inject(MessageService)
   protected readonly messageService: MessageService;
   @inject(NotificationCenter)
@@ -41,9 +44,7 @@ export abstract class ListWidget<
    */
   private focusNode: HTMLElement | undefined;
   private readonly didReceiveFirstFocus = new Deferred();
-  private readonly searchOptionsChangeEmitter = new Emitter<
-    Partial<S> | undefined
-  >();
+  private readonly searchOptions: ListWidgetSearchOptions<S>;
   private readonly onDidShowEmitter = new Emitter<void>();
   /**
    * Instead of running an `update` from the `postConstruct` `init` method,
@@ -53,7 +54,7 @@ export abstract class ListWidget<
 
   constructor(protected options: ListWidget.Options<T, S>) {
     super();
-    const { id, label, iconClass } = options;
+    const { id, label, iconClass, searchOptions } = options;
     this.id = id;
     this.title.label = label;
     this.title.caption = label;
@@ -62,10 +63,8 @@ export abstract class ListWidget<
     this.addClass('arduino-list-widget');
     this.node.tabIndex = 0; // To be able to set the focus on the widget.
     this.scrollOptions = undefined;
-    this.toDispose.pushAll([
-      this.searchOptionsChangeEmitter,
-      this.onDidShowEmitter,
-    ]);
+    this.searchOptions = searchOptions;
+    this.toDispose.push(this.onDidShowEmitter);
   }
 
   @postConstruct()
@@ -79,6 +78,16 @@ export abstract class ListWidget<
     ]);
   }
 
+  storeState(): S | undefined {
+    return this.searchOptions.options;
+  }
+
+  restoreState(oldState: unknown): void {
+    if (oldState) {
+      this.searchOptions.update(oldState as S);
+    }
+  }
+
   protected override onAfterShow(message: Message): void {
     this.maybeUpdateOnFirstRender();
     super.onAfterShow(message);
@@ -141,7 +150,7 @@ export abstract class ListWidget<
   override render(): React.ReactNode {
     return (
       <FilterableListContainer<T, S>
-        defaultSearchOptions={this.options.defaultSearchOptions}
+        searchOptions={this.searchOptions}
         container={this}
         resolveFocus={this.onFocusResolved}
         searchable={this.options.searchable}
@@ -149,8 +158,6 @@ export abstract class ListWidget<
         uninstall={this.uninstall.bind(this)}
         itemLabel={this.options.itemLabel}
         itemRenderer={this.options.itemRenderer}
-        filterRenderer={this.options.filterRenderer}
-        searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
         messageService={this.messageService}
         commandService={this.commandService}
         responseService={this.responseService}
@@ -164,9 +171,13 @@ export abstract class ListWidget<
    * If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
    */
   refresh(searchOptions: Partial<S> | undefined): void {
-    this.didReceiveFirstFocus.promise.then(() =>
-      this.searchOptionsChangeEmitter.fire(searchOptions)
-    );
+    this.didReceiveFirstFocus.promise.then(() => {
+      if (searchOptions) {
+        this.searchOptions.update(searchOptions);
+      } else {
+        this.searchOptions.options = this.searchOptions.options; // triggers a refresh. TODO fix this!
+      }
+    });
   }
 
   updateScrollBar(): void {
@@ -188,8 +199,7 @@ export namespace ListWidget {
     readonly searchable: Searchable<T, S>;
     readonly itemLabel: (item: T) => string;
     readonly itemRenderer: ListItemRenderer<T>;
-    readonly filterRenderer: FilterRenderer<S>;
-    readonly defaultSearchOptions: S;
+    readonly searchOptions: ListWidgetSearchOptions<S>;
   }
 }
 
@@ -199,3 +209,57 @@ export class UserAbortError extends Error {
     Object.setPrototypeOf(this, UserAbortError.prototype);
   }
 }
+
+@injectable()
+export abstract class ListWidgetSearchOptions<S extends Searchable.Options> {
+  private readonly onDidChangeEmitter = new Emitter<Required<S>>();
+  protected _options: Required<S>;
+
+  @postConstruct()
+  protected init(): void {
+    this.options = this.defaultOptions;
+  }
+
+  get onDidChange(): Event<Required<S>> {
+    return this.onDidChangeEmitter.event;
+  }
+
+  get options(): Required<S> {
+    return this._options;
+  }
+
+  set options(options: Required<S>) {
+    this._options = options;
+    this.onDidChangeEmitter.fire({ ...this._options });
+  }
+
+  update(options: Partial<S>): void {
+    this.options = { ...this.options, ...options };
+  }
+
+  clearFilters(): void {
+    const { query } = this.options;
+    this.options = { ...this.defaultOptions, query };
+  }
+
+  /**
+   * `true` if all property values of the `options` object equals with the `defaultOptions` property values. The `query` property is ignored in the comparison.
+   */
+  hasFilters(): boolean {
+    const defaultOptions = this.defaultOptions;
+    const currentOptions = this.options;
+    for (const key of Object.keys(currentOptions)) {
+      if (key === 'query') {
+        continue;
+      }
+      const defaultValue = (defaultOptions as Record<string, unknown>)[key];
+      const currentValue = (currentOptions as Record<string, unknown>)[key];
+      if (defaultValue !== currentValue) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  abstract get defaultOptions(): Required<S>;
+}
diff --git a/arduino-ide-extension/src/common/nls.ts b/arduino-ide-extension/src/common/nls.ts
index a2e58b86..6b0707df 100644
--- a/arduino-ide-extension/src/common/nls.ts
+++ b/arduino-ide-extension/src/common/nls.ts
@@ -3,6 +3,10 @@ import { nls } from '@theia/core/lib/common/nls';
 export const Unknown = nls.localize('arduino/common/unknown', 'Unknown');
 export const Later = nls.localize('arduino/common/later', 'Later');
 export const Updatable = nls.localize('arduino/common/updateable', 'Updatable');
+export const Installed = nls.localize(
+  'arduino/libraryType/installed', // TODO: rename `libraryType` to `common`?
+  'Installed'
+);
 export const All = nls.localize('arduino/common/all', 'All');
 export const Type = nls.localize('arduino/common/type', 'Type');
 export const Partner = nls.localize('arduino/common/partner', 'Partner');
diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts
index c955b946..d7df7bff 100644
--- a/arduino-ide-extension/src/common/protocol/boards-service.ts
+++ b/arduino-ide-extension/src/common/protocol/boards-service.ts
@@ -6,6 +6,7 @@ import { nls } from '@theia/core/lib/common/nls';
 import {
   All,
   Contributed,
+  Installed,
   Partner,
   Type as TypeLabel,
   Updatable,
@@ -174,6 +175,7 @@ export namespace BoardSearch {
   export const TypeLiterals = [
     'All',
     'Updatable',
+    'Installed',
     'Arduino',
     'Contributed',
     'Arduino Certified',
@@ -189,6 +191,7 @@ export namespace BoardSearch {
   export const TypeLabels: Record<Type, string> = {
     All: All,
     Updatable: Updatable,
+    Installed: Installed,
     Arduino: 'Arduino',
     Contributed: Contributed,
     'Arduino Certified': nls.localize(
diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts
index e8a32d90..f5e996f1 100644
--- a/arduino-ide-extension/src/common/protocol/library-service.ts
+++ b/arduino-ide-extension/src/common/protocol/library-service.ts
@@ -5,6 +5,7 @@ import { nls } from '@theia/core/lib/common/nls';
 import {
   All,
   Contributed,
+  Installed,
   Partner,
   Recommended,
   Retired,
@@ -13,6 +14,11 @@ import {
 } from '../nls';
 import URI from '@theia/core/lib/common/uri';
 
+export const TopicLabel = nls.localize(
+  'arduino/librarySearchProperty/topic',
+  'Topic'
+);
+
 export const LibraryServicePath = '/services/library-service';
 export const LibraryService = Symbol('LibraryService');
 export interface LibraryService
@@ -76,7 +82,7 @@ export namespace LibrarySearch {
   export const TypeLabels: Record<Type, string> = {
     All: All,
     Updatable: Updatable,
-    Installed: nls.localize('arduino/libraryType/installed', 'Installed'),
+    Installed: Installed,
     Arduino: 'Arduino',
     Partner: Partner,
     Recommended: Recommended,
@@ -137,7 +143,7 @@ export namespace LibrarySearch {
     keyof Omit<LibrarySearch, 'query'>,
     string
   > = {
-    topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
+    topic: TopicLabel,
     type: TypeLabel,
   };
   export namespace UriParser {
diff --git a/arduino-ide-extension/src/common/protocol/searchable.ts b/arduino-ide-extension/src/common/protocol/searchable.ts
index 2caf5373..0854336f 100644
--- a/arduino-ide-extension/src/common/protocol/searchable.ts
+++ b/arduino-ide-extension/src/common/protocol/searchable.ts
@@ -1,6 +1,8 @@
 import URI from '@theia/core/lib/common/uri';
 import type { ArduinoComponent } from './arduino-component';
 
+export const Updatable = { type: 'Updatable' } as const;
+
 export interface Searchable<T, O extends Searchable.Options> {
   search(options: O): Promise<T[]>;
 }
diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts
index b336d04c..20b1f179 100644
--- a/arduino-ide-extension/src/node/boards-service-impl.ts
+++ b/arduino-ide-extension/src/node/boards-service-impl.ts
@@ -402,8 +402,8 @@ export class BoardsServiceImpl
       }
     }
 
-    const filter = this.typePredicate(options);
-    const boardsPackages = [...packages.values()].filter(filter);
+    const typeFilter = this.typePredicate(options);
+    const boardsPackages = [...packages.values()].filter(typeFilter);
     return sortComponents(boardsPackages, boardsPackageSortGroup);
   }
 
@@ -415,6 +415,8 @@ export class BoardsServiceImpl
       return () => true;
     }
     switch (options.type) {
+      case 'Installed':
+        return Installable.Installed;
       case 'Updatable':
         return Installable.Updateable;
       case 'Arduino':
diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts
index 1bb836dc..e3102099 100644
--- a/arduino-ide-extension/src/node/library-service-impl.ts
+++ b/arduino-ide-extension/src/node/library-service-impl.ts
@@ -221,8 +221,8 @@ export class LibraryServiceImpl
           {
             name: library.getName(),
             installedVersion,
-            description: library.getSentence(),
-            summary: library.getParagraph(),
+            description: library.getParagraph(),
+            summary: library.getSentence(),
             moreInfoLink: library.getWebsite(),
             includes: library.getProvidesIncludesList(),
             location: this.mapLocation(library.getLocation()),
@@ -462,9 +462,9 @@ function toLibrary(
     author: lib.getAuthor(),
     availableVersions,
     includes: lib.getProvidesIncludesList(),
-    description: lib.getSentence(),
+    description: lib.getParagraph(),
     moreInfoLink: lib.getWebsite(),
-    summary: lib.getParagraph(),
+    summary: lib.getSentence(),
     category: lib.getCategory(),
     types: lib.getTypesList(),
   };
diff --git a/i18n/en.json b/i18n/en.json
index 22419c5b..7cf6f927 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -46,6 +46,9 @@
       "typeOfPorts": "{0} ports",
       "unknownBoard": "Unknown board"
     },
+    "boards": {
+      "filterBoards": "Filter Boards..."
+    },
     "boardsManager": "Boards Manager",
     "boardsType": {
       "arduinoCertified": "Arduino Certified"
@@ -81,6 +84,10 @@
       "noUpdates": "There are no recent updates available.",
       "promptUpdateBoards": "Updates are available for some of your boards.",
       "promptUpdateLibraries": "Updates are available for some of your libraries.",
+      "showBoardsUpdates": "Boards Updates",
+      "showInstalledBoards": "Installed Boards",
+      "showInstalledLibraries": "Installed Libraries",
+      "showLibraryUpdates": "Library Updates",
       "updatingBoards": "Updating boards...",
       "updatingLibraries": "Updating libraries..."
     },
@@ -169,7 +176,6 @@
       "moreInfo": "More info",
       "otherVersions": "Other Versions",
       "remove": "Remove",
-      "title": "{0} by {1}",
       "uninstall": "Uninstall",
       "uninstallMsg": "Do you want to uninstall {0}?",
       "update": "Update"
@@ -242,6 +248,9 @@
       "forAny": "Examples for any board",
       "menu": "Examples"
     },
+    "filter": {
+      "clearAll": "Clear All Filters"
+    },
     "firmware": {
       "checkUpdates": "Check Updates",
       "failedInstall": "Installation failed. Please try again.",
@@ -282,6 +291,9 @@
       "updateAvailable": "Update Available",
       "versionDownloaded": "Arduino IDE {0} has been downloaded."
     },
+    "libraries": {
+      "filterLibraries": "Filter Libraries..."
+    },
     "library": {
       "addZip": "Add .ZIP Library...",
       "arduinoLibraries": "Arduino libraries",