feat: new plugin system

This commit is contained in:
Simos Mikelatos
2026-03-05 22:51:27 +00:00
parent f4615dfca3
commit b4169887ab
22 changed files with 2276 additions and 61 deletions

View File

@@ -1,8 +1,17 @@
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
import {
MessageSquare,
Terminal,
Folder,
GitBranch,
ClipboardCheck,
type LucideIcon,
} from 'lucide-react';
import Tooltip from '../../../Tooltip';
import type { AppTab } from '../../../../types/app';
import type { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { usePlugins } from '../../../../contexts/PluginsContext';
import PluginIcon from '../../../plugins/PluginIcon';
type MainContentTabSwitcherProps = {
activeTab: AppTab;
@@ -10,20 +19,32 @@ type MainContentTabSwitcherProps = {
shouldShowTasksTab: boolean;
};
type TabDefinition = {
type BuiltInTab = {
kind: 'builtin';
id: AppTab;
labelKey: string;
icon: LucideIcon;
};
const BASE_TABS: TabDefinition[] = [
{ id: 'chat', labelKey: 'tabs.chat', icon: MessageSquare },
{ id: 'shell', labelKey: 'tabs.shell', icon: Terminal },
{ id: 'files', labelKey: 'tabs.files', icon: Folder },
{ id: 'git', labelKey: 'tabs.git', icon: GitBranch },
type PluginTab = {
kind: 'plugin';
id: AppTab;
label: string;
pluginName: string;
iconFile: string;
};
type TabDefinition = BuiltInTab | PluginTab;
const BASE_TABS: BuiltInTab[] = [
{ kind: 'builtin', id: 'chat', labelKey: 'tabs.chat', icon: MessageSquare },
{ kind: 'builtin', id: 'shell', labelKey: 'tabs.shell', icon: Terminal },
{ kind: 'builtin', id: 'files', labelKey: 'tabs.files', icon: Folder },
{ kind: 'builtin', id: 'git', labelKey: 'tabs.git', icon: GitBranch },
];
const TASKS_TAB: TabDefinition = {
const TASKS_TAB: BuiltInTab = {
kind: 'builtin',
id: 'tasks',
labelKey: 'tabs.tasks',
icon: ClipboardCheck,
@@ -35,17 +56,30 @@ export default function MainContentTabSwitcher({
shouldShowTasksTab,
}: MainContentTabSwitcherProps) {
const { t } = useTranslation();
const { plugins } = usePlugins();
const tabs = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
const builtInTabs: BuiltInTab[] = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
const pluginTabs: PluginTab[] = plugins
.filter((p) => p.enabled)
.map((p) => ({
kind: 'plugin',
id: `plugin:${p.name}` as AppTab,
label: p.displayName,
pluginName: p.name,
iconFile: p.icon,
}));
const tabs: TabDefinition[] = [...builtInTabs, ...pluginTabs];
return (
<div className="inline-flex items-center bg-muted/60 rounded-lg p-[3px] gap-[2px]">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = tab.id === activeTab;
const displayLabel = tab.kind === 'builtin' ? t(tab.labelKey) : tab.label;
return (
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
<Tooltip key={tab.id} content={displayLabel} position="bottom">
<button
onClick={() => setActiveTab(tab.id)}
className={`relative flex items-center gap-1.5 px-2.5 py-[5px] text-sm font-medium rounded-md transition-all duration-150 ${
@@ -54,8 +88,16 @@ export default function MainContentTabSwitcher({
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className="w-3.5 h-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
<span className="hidden lg:inline">{t(tab.labelKey)}</span>
{tab.kind === 'builtin' ? (
<tab.icon className="w-3.5 h-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
) : (
<PluginIcon
pluginName={tab.pluginName}
iconFile={tab.iconFile}
className="w-3.5 h-3.5 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full"
/>
)}
<span className="hidden lg:inline">{displayLabel}</span>
</button>
</Tooltip>
);

View File

@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import type { AppTab, Project, ProjectSession } from '../../../../types/app';
import { usePlugins } from '../../../../contexts/PluginsContext';
type MainContentTitleProps = {
activeTab: AppTab;
@@ -9,7 +10,11 @@ type MainContentTitleProps = {
shouldShowTasksTab: boolean;
};
function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: string) => string) {
function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: string) => string, pluginDisplayName?: string) {
if (activeTab.startsWith('plugin:') && pluginDisplayName) {
return pluginDisplayName;
}
if (activeTab === 'files') {
return t('mainContent.projectFiles');
}
@@ -40,6 +45,11 @@ export default function MainContentTitle({
shouldShowTasksTab,
}: MainContentTitleProps) {
const { t } = useTranslation();
const { plugins } = usePlugins();
const pluginDisplayName = activeTab.startsWith('plugin:')
? plugins.find((p) => p.name === activeTab.replace('plugin:', ''))?.displayName
: undefined;
const showSessionIcon = activeTab === 'chat' && Boolean(selectedSession);
const showChatNewSession = activeTab === 'chat' && !selectedSession;
@@ -68,7 +78,7 @@ export default function MainContentTitle({
) : (
<div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground leading-tight">
{getTabTitle(activeTab, shouldShowTasksTab, t)}
{getTabTitle(activeTab, shouldShowTasksTab, t, pluginDisplayName)}
</h2>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
</div>