diff --git a/frontend/src/components/layout/AISidebar.tsx b/frontend/src/components/layout/AISidebar.tsx
index ef2a35f..45ef3e6 100644
--- a/frontend/src/components/layout/AISidebar.tsx
+++ b/frontend/src/components/layout/AISidebar.tsx
@@ -5,6 +5,7 @@ import { SuggestionList } from '@/components/ai/SuggestionSidebar';
import { ImprovementPanel } from '@/components/ai/ImprovementPanel';
import { createSession, fetchSessions } from '@/api/ai';
import { useUIStore } from '@/store/uiStore';
+import { getContributedTabs } from './sidebarTabs';
import { useTranslation } from 'react-i18next';
import { useUsers, useGroups } from '@/api/hooks';
import { Avatar } from '@/components/ui/Avatar';
@@ -35,7 +36,7 @@ const chevronRightIcon = (
);
interface TabDef {
- key: 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom';
+ key: string;
label: string;
icon: (cls: string) => React.ReactNode;
testId: string;
@@ -139,6 +140,13 @@ export function AISidebar() {
{ key: 'notifications', label: t('topbar.notifications'), icon: bellIcon, testId: 'ai-sidebar-tab-notifications' },
{ key: 'team', label: 'Team', icon: teamIcon, testId: 'ai-sidebar-tab-team' },
{ key: 'chatroom', label: 'Chat', icon: chatBubbleIcon, testId: 'ai-sidebar-tab-chatroom' },
+ // Plugin-contributed tabs (Block H / HC-G)
+ ...getContributedTabs().map((ct) => ({
+ key: ct.key,
+ label: ct.label,
+ icon: ct.icon,
+ testId: ct.testId,
+ })),
];
const activeTab = tabs.find((tab) => tab.key === aiSidebarTab) || tabs[0];
@@ -209,6 +217,12 @@ export function AISidebar() {
if (aiSidebarTab === 'chatroom') {
return ;
}
+ // Plugin-contributed tabs (Block H / HC-G)
+ const contributed = getContributedTabs().find((ct) => ct.key === aiSidebarTab);
+ if (contributed) {
+ const ContributedComponent = contributed.component;
+ return ;
+ }
// chat tab
if (loading) {
return (
diff --git a/frontend/src/components/layout/sidebarTabs.ts b/frontend/src/components/layout/sidebarTabs.ts
new file mode 100644
index 0000000..1eeaa54
--- /dev/null
+++ b/frontend/src/components/layout/sidebarTabs.ts
@@ -0,0 +1,42 @@
+/**
+ * AI sidebar tab registry.
+ *
+ * Core owns the base tabs (chat/proactive/notifications/team/chatroom);
+ * plugins contribute additional tabs via ``registerSidebarTab`` during their
+ * frontend activation lifecycle (Block H / HC-G). Contributed tabs render a
+ * generic container component provided by the plugin.
+ */
+import type { ComponentType } from 'react';
+
+type IconFn = (className: string) => JSX.Element;
+
+export interface ContributedTab {
+ key: string;
+ label: string;
+ icon: IconFn;
+ testId: string;
+ component: ComponentType;
+}
+
+const _tabs = new Map();
+
+export function registerSidebarTab(tab: ContributedTab): void {
+ if (_tabs.has(tab.key)) {
+ // eslint-disable-next-line no-console
+ console.warn(`[sidebarTabs] tab '${tab.key}' re-registered — overwriting`);
+ }
+ _tabs.set(tab.key, tab);
+}
+
+export function unregisterSidebarTabs(owner: string): void {
+ for (const [key, tab] of _tabs.entries()) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((tab.component as any).__sidebar_owner__ === owner) {
+ _tabs.delete(key);
+ }
+ }
+}
+
+export function getContributedTabs(): ContributedTab[] {
+ return Array.from(_tabs.values());
+}
diff --git a/frontend/src/store/uiStore.ts b/frontend/src/store/uiStore.ts
index 848a8ff..a67094e 100644
--- a/frontend/src/store/uiStore.ts
+++ b/frontend/src/store/uiStore.ts
@@ -2,7 +2,9 @@ import { create } from 'zustand';
export type Theme = 'light' | 'dark' | 'system';
export type Locale = 'de' | 'en';
-export type AISidebarTab = 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom';
+// Plugin-contributed tabs add arbitrary keys (Block H / HC-G) — the union
+// keeps autocomplete for the base tabs while allowing any string key.
+export type AISidebarTab = 'proactive' | 'chat' | 'notifications' | 'team' | 'chatroom' | (string & {});
export interface Toast {
id: string;