chore: remove dead code (59 unused files + ~125 unused exports) (#1835)

* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:27:53 +03:00
committed by GitHub
co-authored by Serhii Dziupin Bohdan Triapitsyn
parent 4a37b9a005
commit 00821700de
324 changed files with 444 additions and 14876 deletions
@@ -1,46 +0,0 @@
import React from 'react';
import { SIDEBAR_SECTION_CONFIG_MAP, SIDEBAR_SECTION_DESCRIPTIONS } from '@/constants/sidebar';
import type { SidebarSection } from '@/constants/sidebar';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
import { McpIcon } from '@/components/icons/McpIcon';
interface SectionPlaceholderProps {
sectionId: SidebarSection;
variant: 'sidebar' | 'page';
}
export const SectionPlaceholder: React.FC<SectionPlaceholderProps> = ({ sectionId, variant }) => {
const { t } = useI18n();
const config = SIDEBAR_SECTION_CONFIG_MAP[sectionId];
const icon = config.icon;
if (variant === 'sidebar') {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
<div className="rounded-full bg-accent/40 p-3 text-muted-foreground">
{icon === 'mcp-custom' ? <McpIcon className="h-5 w-5" /> : <Icon name={icon} className="h-5 w-5" />}
</div>
<h3 className="typography-ui-label font-semibold text-foreground">{config.label}</h3>
<p className="typography-meta max-w-xs text-muted-foreground">
{SIDEBAR_SECTION_DESCRIPTIONS[sectionId]}
</p>
</div>
);
}
return (
<div className="flex h-full flex-col items-center justify-center gap-4 px-6 text-center">
<div className="rounded-full bg-accent/40 p-4 text-muted-foreground">
{icon === 'mcp-custom' ? <McpIcon className="h-8 w-8" /> : <Icon name={icon} className="h-8 w-8" />}
</div>
<div className="flex flex-col gap-2">
<h2 className="typography-h2 font-semibold text-foreground">{config.label}</h2>
<p className="typography-body max-w-md text-muted-foreground">
{SIDEBAR_SECTION_DESCRIPTIONS[sectionId]}
</p>
</div>
<p className="typography-meta text-muted-foreground/60">{t('settings.common.state.comingSoon')}</p>
</div>
);
};
@@ -384,5 +384,5 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
};
// Re-export for easy sidebar icon usage
export { McpIcon } from '@/components/icons/McpIcon';
import { Icon } from "@/components/icon/Icon";
@@ -17,7 +17,7 @@ export interface ImportedMcpResult {
readonly enabled: boolean;
}
export type ImportedMcpError =
type ImportedMcpError =
| { readonly ok: false; readonly error: string }
| { readonly ok: false; readonly error: string; readonly parsed: unknown };
@@ -53,7 +53,7 @@ export const parseMcpOAuthCallbackStateKey = (params: URLSearchParams): string |
return trimmed || null;
};
export const parseMcpOAuthState = (raw: string | null | undefined): {
const parseMcpOAuthState = (raw: string | null | undefined): {
name: string;
directory: string | null;
} | null => {
@@ -234,7 +234,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
return mode === 'markdown' ? 'markdown' : 'plain';
};
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
type VisibleSetting = 'theme' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'wideChatLayout' | 'splitAssistantMessageActions' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'queueMode' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar';
interface OpenChamberVisualSettingsProps {
/** Which settings to show. If undefined, shows all. */
@@ -300,5 +300,3 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
</Dialog>
);
};
export default AddPluginDialog;
@@ -1,3 +1,2 @@
export { PluginsSidebar } from './PluginsSidebar';
export { PluginsPage } from './PluginsPage';
export { AddPluginDialog } from './AddPluginDialog';
@@ -1,240 +0,0 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { SettingsSidebarLayout } from '@/components/sections/shared/SettingsSidebarLayout';
import { SettingsSidebarItem } from '@/components/sections/shared/SettingsSidebarItem';
import { useDesktopSshStore } from '@/stores/useDesktopSshStore';
import { useUIStore } from '@/stores/useUIStore';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import type { DesktopSshInstance } from '@/lib/desktopSsh';
import { useI18n } from '@/lib/i18n';
type RemoteInstancesSidebarProps = {
onItemSelect?: () => void;
};
const makeId = (): string => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `ssh-${Date.now()}-${Math.random().toString(16).slice(2)}`;
};
const DIRECT_INSTANCES_ID = '__direct_instances__';
const randomPort = (): number => {
return Math.floor(20000 + Math.random() * 30000);
};
const isPortInUseError = (error: unknown): boolean => {
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
return message.includes('address already in use') || message.includes('eaddrinuse') || message.includes('port already in use');
};
const phaseLabelKey = (phase?: string) => {
switch (phase) {
case 'ready':
return 'settings.remoteInstances.sidebar.phase.ready';
case 'error':
return 'settings.remoteInstances.sidebar.phase.error';
case 'degraded':
return 'settings.remoteInstances.sidebar.phase.reconnect';
case 'installing':
return 'settings.remoteInstances.sidebar.phase.installing';
case 'updating':
return 'settings.remoteInstances.sidebar.phase.updating';
case 'forwarding':
return 'settings.remoteInstances.sidebar.phase.forwarding';
case 'server_starting':
return 'settings.remoteInstances.sidebar.phase.starting';
case 'master_connecting':
return 'settings.remoteInstances.sidebar.phase.connecting';
default:
return 'settings.remoteInstances.sidebar.phase.idle';
}
};
export const RemoteInstancesSidebar: React.FC<RemoteInstancesSidebarProps> = ({ onItemSelect }) => {
const { t } = useI18n();
const instances = useDesktopSshStore((state) => state.instances);
const statusesById = useDesktopSshStore((state) => state.statusesById);
const isLoading = useDesktopSshStore((state) => state.isLoading);
const load = useDesktopSshStore((state) => state.load);
const loadImports = useDesktopSshStore((state) => state.loadImports);
const createFromCommand = useDesktopSshStore((state) => state.createFromCommand);
const connect = useDesktopSshStore((state) => state.connect);
const disconnect = useDesktopSshStore((state) => state.disconnect);
const retry = useDesktopSshStore((state) => state.retry);
const removeInstance = useDesktopSshStore((state) => state.removeInstance);
const upsertInstance = useDesktopSshStore((state) => state.upsertInstance);
const selectedId = useUIStore((state) => state.settingsRemoteInstancesSelectedId);
const setSelectedId = useUIStore((state) => state.setSettingsRemoteInstancesSelectedId);
React.useEffect(() => {
void load();
void loadImports();
}, [load, loadImports]);
React.useEffect(() => {
if (isLoading) return;
if (selectedId === DIRECT_INSTANCES_ID) {
return;
}
if (instances.length === 0) {
if (selectedId !== null) {
setSelectedId(null);
}
return;
}
if (selectedId && instances.some((instance) => instance.id === selectedId)) {
return;
}
setSelectedId(instances[0].id);
}, [instances, isLoading, selectedId, setSelectedId]);
const handleAdd = React.useCallback(async () => {
const id = makeId();
try {
await createFromCommand(id, 'ssh user@example.com', t('settings.remoteInstances.sidebar.newSshInstanceName'));
setSelectedId(id);
onItemSelect?.();
} catch (error) {
toast.error(t('settings.remoteInstances.sidebar.toast.createFailed'), {
description: error instanceof Error ? error.message : String(error),
});
}
}, [createFromCommand, onItemSelect, setSelectedId, t]);
const connectWithPortRecovery = React.useCallback(async (instance: DesktopSshInstance) => {
try {
await connect(instance.id);
return;
} catch (error) {
if (!isPortInUseError(error)) {
throw error;
}
const allow = window.confirm(t('settings.remoteInstances.sidebar.confirm.localPortInUseRetry'));
if (!allow) {
throw error;
}
const nextInstance: DesktopSshInstance = {
...instance,
localForward: {
...instance.localForward,
preferredLocalPort: randomPort(),
},
};
await upsertInstance(nextInstance);
await connect(nextInstance.id);
toast.success(t('settings.remoteInstances.sidebar.toast.retriedWithRandomPort'));
}
}, [connect, t, upsertInstance]);
return (
<SettingsSidebarLayout
variant="background"
header={
<div className="border-b px-3 pt-4 pb-3">
<h2 className="text-base font-semibold text-foreground mb-3">{t('settings.remoteInstances.sidebar.title')}</h2>
<div className="flex items-center justify-between gap-2">
<span className="typography-meta text-muted-foreground">{t('settings.remoteInstances.sidebar.total', { count: instances.length })}</span>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 -my-1 text-muted-foreground"
onClick={() => void handleAdd()}
aria-label={t('settings.remoteInstances.sidebar.actions.addSshInstance')}
>
<Icon name="add" className="size-4" />
</Button>
</div>
</div>
}
>
<SettingsSidebarItem
title={t('settings.remoteInstances.direct.sidebarTitle')}
metadata={t('settings.remoteInstances.direct.sidebarDescription')}
selected={selectedId === DIRECT_INSTANCES_ID || (!selectedId && instances.length === 0)}
onSelect={() => {
setSelectedId(DIRECT_INSTANCES_ID);
onItemSelect?.();
}}
icon={<Icon name="global" className="h-4 w-4 text-muted-foreground" />}
/>
{instances.map((instance) => {
const status = statusesById[instance.id];
const selected = instance.id === selectedId;
const title = instance.nickname?.trim() || instance.sshParsed?.destination || instance.id;
const metadata = `${t(phaseLabelKey(status?.phase))}${status?.localUrl ? ` · ${status.localUrl}` : ''}`;
const isReady = status?.phase === 'ready';
const canRetry = status?.phase === 'error' || status?.phase === 'degraded';
return (
<SettingsSidebarItem
key={instance.id}
title={title}
metadata={metadata}
selected={selected}
onSelect={() => {
setSelectedId(instance.id);
onItemSelect?.();
}}
actions={[
{
label: isReady ? t('settings.remoteInstances.sidebar.actions.disconnect') : t('settings.remoteInstances.sidebar.actions.connect'),
icon: isReady ? 'stop' : 'plug-2',
onClick: () => {
const op = isReady ? disconnect(instance.id) : connectWithPortRecovery(instance);
void op.catch((error) => {
toast.error(
isReady
? t('settings.remoteInstances.sidebar.toast.disconnectFailed')
: t('settings.remoteInstances.sidebar.toast.connectFailed'),
{
description: error instanceof Error ? error.message : String(error),
}
);
});
},
},
{
label: t('settings.remoteInstances.sidebar.actions.retry'),
icon: "refresh",
onClick: () => {
if (!canRetry) return;
void retry(instance.id).catch((error) => {
toast.error(t('settings.remoteInstances.sidebar.toast.retryFailed'), {
description: error instanceof Error ? error.message : String(error),
});
});
},
},
{
label: t('settings.remoteInstances.sidebar.actions.remove'),
icon: "delete-bin",
destructive: true,
onClick: () => {
void removeInstance(instance.id).then(() => {
if (selectedId === instance.id) {
const next = instances.find((item) => item.id !== instance.id);
setSelectedId(next?.id || null);
}
}).catch((error) => {
toast.error(t('settings.remoteInstances.sidebar.toast.removeFailed'), {
description: error instanceof Error ? error.message : String(error),
});
});
},
},
]}
/>
);
})}
</SettingsSidebarLayout>
);
};
@@ -1,62 +0,0 @@
import React from 'react';
import { cn } from '@/lib/utils';
interface SettingsSectionProps {
/** Section content */
children: React.ReactNode;
/** Optional section title */
title?: string;
/** Optional section description */
description?: string;
/** If true, adds a top border divider */
divider?: boolean;
/** Additional className */
className?: string;
}
/**
* Standard section wrapper for settings page content.
* Provides consistent spacing and optional divider.
*
* @example
* <SettingsSection title="Appearance" description="Customize the look and feel">
* <ThemeSelector />
* <FontSizeSelector />
* </SettingsSection>
*
* <SettingsSection divider>
* <DangerZoneSettings />
* </SettingsSection>
*/
export const SettingsSection: React.FC<SettingsSectionProps> = ({
children,
title,
description,
divider = false,
className,
}) => {
return (
<div
className={cn(
divider && 'border-t border-border/40 pt-6',
className
)}
>
{(title || description) && (
<div className="mb-4 space-y-1">
{title && (
<h3 className="typography-ui-header font-semibold text-foreground">
{title}
</h3>
)}
{description && (
<p className="typography-meta text-muted-foreground">
{description}
</p>
)}
</div>
)}
{children}
</div>
);
};
@@ -1,63 +0,0 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from "@/components/icon/Icon";
import { useDeviceInfo } from '@/lib/device';
import { cn } from '@/lib/utils';
interface SettingsSidebarHeaderProps {
/** Total count to display (e.g., "Total 5") */
count: number;
/** Callback when add button is clicked. If undefined, no add button is shown. */
onAdd?: () => void;
/** Custom label prefix (default: "Total") */
label?: string;
/** Aria label for the add button */
addButtonLabel?: string;
}
/**
* Standard header for settings sidebars.
* Displays "Total X" on the left and an optional add button on the right.
*
* @example
* <SettingsSidebarHeader
* count={agents.length}
* onAdd={() => setCreateDialogOpen(true)}
* addButtonLabel="Create new agent"
* />
*/
export const SettingsSidebarHeader: React.FC<SettingsSidebarHeaderProps> = ({
count,
onAdd,
label = 'Total',
addButtonLabel = 'Add new item',
}) => {
const { isMobile } = useDeviceInfo();
return (
<div
className={cn(
'border-b px-3',
isMobile ? 'mt-2 py-3' : 'py-3'
)}
>
<div className="flex items-center justify-between gap-2">
<span className="typography-meta text-muted-foreground">
{label} {count}
</span>
{onAdd && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 -my-1 text-muted-foreground"
onClick={onAdd}
aria-label={addButtonLabel}
>
<Icon name="add" className="size-4" />
</Button>
)}
</div>
</div>
);
};
@@ -10,7 +10,7 @@ import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { cn } from '@/lib/utils';
export interface SettingsSidebarItemAction {
interface SettingsSidebarItemAction {
/** Label shown in dropdown menu */
label: string;
/** Icon component to show before label */
@@ -1,57 +0,0 @@
/**
* Shared boilerplate components for settings sections.
*
* These components provide consistent styling and behavior for settings sidebars and pages.
* Use them as building blocks when creating new settings sections.
*
* @example Sidebar usage:
* ```tsx
* import {
* SettingsSidebarLayout,
* SettingsSidebarHeader,
* SettingsSidebarItem,
* } from '@/components/sections/shared';
*
* export const MySidebar = () => (
* <SettingsSidebarLayout
* header={<SettingsSidebarHeader count={items.length} onAdd={handleAdd} />}
* >
* {items.map(item => (
* <SettingsSidebarItem
* key={item.id}
* title={item.name}
* metadata={item.description}
* selected={selectedId === item.id}
* onSelect={() => setSelectedId(item.id)}
* actions={[
* { label: 'Delete', onClick: () => handleDelete(item.id), destructive: true }
* ]}
* />
* ))}
* </SettingsSidebarLayout>
* );
* ```
*
* @example Page usage:
* ```tsx
* import { SettingsPageLayout, SettingsSection } from '@/components/sections/shared';
*
* export const MyPage = () => (
* <SettingsPageLayout>
* <SettingsSection title="General Settings">
* <MySettingsForm />
* </SettingsSection>
* <SettingsSection title="Advanced" divider>
* <AdvancedSettingsForm />
* </SettingsSection>
* </SettingsPageLayout>
* );
* ```
*/
export { SettingsSidebarLayout } from './SettingsSidebarLayout';
export { SettingsSidebarHeader } from './SettingsSidebarHeader';
export { SettingsSidebarItem, type SettingsSidebarItemAction } from './SettingsSidebarItem';
export { SettingsPageLayout } from './SettingsPageLayout';
export { SettingsSection } from './SettingsSection';
export { SidebarGroup } from './SidebarGroup';
@@ -1,597 +0,0 @@
import React from 'react';
import { toast } from '@/components/ui';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Icon } from "@/components/icon/Icon";
import { isVSCodeRuntime } from '@/lib/desktop';
import type { SkillsCatalogItem } from '@/lib/api/types';
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { InstallConflictsDialog, type ConflictDecision, type SkillConflict } from './InstallConflictsDialog';
import { useI18n } from '@/lib/i18n';
import {
SKILL_LOCATION_OPTIONS,
locationPartsFrom,
locationValueFrom,
type SkillLocationValue,
} from '../skillLocations';
interface InstallFromRepoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
type IdentityOption = { id: string; name: string };
export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ open, onOpenChange }) => {
const { t } = useI18n();
const scanRepo = useSkillsCatalogStore((s) => s.scanRepo);
const installSkills = useSkillsCatalogStore((s) => s.installSkills);
const isScanning = useSkillsCatalogStore((s) => s.isScanning);
const isInstalling = useSkillsCatalogStore((s) => s.isInstalling);
const installedSkills = useSkillsStore((s) => s.skills);
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
const projects = useProjectsStore((s) => s.projects);
const activeProjectId = useProjectsStore((s) => s.activeProjectId);
const [targetProjectId, setTargetProjectId] = React.useState<string | null>(null);
const [source, setSource] = React.useState('');
const [subpath, setSubpath] = React.useState('');
const [scope, setScope] = React.useState<'user' | 'project'>('user');
const [targetSource, setTargetSource] = React.useState<'opencode' | 'agents'>('opencode');
const [items, setItems] = React.useState<SkillsCatalogItem[]>([]);
const [selected, setSelected] = React.useState<Record<string, boolean>>({});
const [search, setSearch] = React.useState('');
const [identities, setIdentities] = React.useState<IdentityOption[]>([]);
const [gitIdentityId, setGitIdentityId] = React.useState<string | null>(null);
const scanRequestIdRef = React.useRef(0);
const invalidateScan = React.useCallback((options?: { clearIdentities?: boolean }) => {
scanRequestIdRef.current += 1;
setItems([]);
setSelected({});
if (options?.clearIdentities) {
setIdentities([]);
setGitIdentityId(null);
}
}, []);
const [conflictsOpen, setConflictsOpen] = React.useState(false);
const [conflicts, setConflicts] = React.useState<SkillConflict[]>([]);
const [baseInstallRequest, setBaseInstallRequest] = React.useState<{
source: string;
subpath?: string;
scope: 'user' | 'project';
targetSource: 'opencode' | 'agents';
selections: Array<{ skillDir: string }>;
gitIdentityId?: string;
directoryOverride?: string | null;
} | null>(null);
React.useEffect(() => {
scanRequestIdRef.current += 1;
if (!open) return;
setSource('');
setSubpath('');
setScope('user');
setTargetSource('opencode');
setTargetProjectId(activeProjectId);
setItems([]);
setSelected({});
setSearch('');
setIdentities([]);
setGitIdentityId(null);
void loadDefaultGitIdentityId();
setConflictsOpen(false);
setConflicts([]);
setBaseInstallRequest(null);
}, [open, loadDefaultGitIdentityId, activeProjectId]);
const resolvedTargetProjectId = React.useMemo(() => {
if (projects.length === 0) {
return null;
}
if (targetProjectId && projects.some((p) => p.id === targetProjectId)) {
return targetProjectId;
}
if (activeProjectId && projects.some((p) => p.id === activeProjectId)) {
return activeProjectId;
}
return projects[0]?.id ?? null;
}, [activeProjectId, projects, targetProjectId]);
const directoryOverride = React.useMemo(() => {
if (scope !== 'project') {
return null;
}
const id = resolvedTargetProjectId;
if (!id) {
return null;
}
const project = projects.find((p) => p.id === id);
return project?.path ?? null;
}, [projects, resolvedTargetProjectId, scope]);
const installedByName = React.useMemo(() => {
const map = new Map<string, { scope: 'user' | 'project'; source: 'opencode' | 'claude' | 'agents' }>();
for (const s of installedSkills) {
map.set(s.name, { scope: s.scope, source: s.source });
}
return map;
}, [installedSkills]);
const filteredItems = React.useMemo(() => {
const q = search.trim().toLowerCase();
if (!q) return items;
return items.filter((item) => {
const name = item.skillName.toLowerCase();
const desc = (item.description || '').toLowerCase();
const fm = (item.frontmatterName || '').toLowerCase();
return name.includes(q) || desc.includes(q) || fm.includes(q);
});
}, [items, search]);
const selectedDirs = React.useMemo(() => Object.keys(selected).filter((k) => selected[k]), [selected]);
const toggleAll = (value: boolean) => {
const next: Record<string, boolean> = {};
for (const item of items) {
if (!item.installable) continue;
next[item.skillDir] = value;
}
setSelected(next);
};
const locationLabelText = React.useCallback((value: SkillLocationValue) => {
switch (value) {
case 'project-opencode':
return t('settings.skills.location.option.projectOpencode.label');
case 'user-agents':
return t('settings.skills.location.option.userAgents.label');
case 'project-agents':
return t('settings.skills.location.option.projectAgents.label');
default:
return t('settings.skills.location.option.userOpencode.label');
}
}, [t]);
const locationDescriptionText = React.useCallback((value: SkillLocationValue) => {
switch (value) {
case 'project-opencode':
return t('settings.skills.location.option.projectOpencode.description');
case 'user-agents':
return t('settings.skills.location.option.userAgents.description');
case 'project-agents':
return t('settings.skills.location.option.projectAgents.description');
default:
return t('settings.skills.location.option.userOpencode.description');
}
}, [t]);
const handleScan = async () => {
const trimmed = source.trim();
if (!trimmed) {
toast.error(t('settings.skills.catalog.shared.toast.repositoryRequired'));
return;
}
setItems([]);
setSelected({});
const requestId = scanRequestIdRef.current + 1;
scanRequestIdRef.current = requestId;
const result = await scanRepo({
source: trimmed,
subpath: subpath.trim() || undefined,
gitIdentityId: gitIdentityId || undefined,
});
if (scanRequestIdRef.current !== requestId) {
return;
}
if (!result.ok) {
if (result.error?.kind === 'authRequired') {
if (isVSCodeRuntime()) {
toast.error(t('settings.skills.catalog.shared.toast.privateRepoNotSupportedVsCode'));
return;
}
const ids = (result.error.identities || []) as IdentityOption[];
setIdentities(ids);
if (!gitIdentityId && ids.length > 0) {
const preferred =
defaultGitIdentityId &&
defaultGitIdentityId !== 'global' &&
ids.some((i) => i.id === defaultGitIdentityId)
? defaultGitIdentityId
: ids[0].id;
setGitIdentityId(preferred);
}
toast.error(t('settings.skills.catalog.installFromRepo.toast.authenticationRequiredScan'));
return;
}
toast.error(result.error?.message || t('settings.skills.catalog.installFromRepo.toast.scanFailed'));
return;
}
const nextItems = result.items || [];
setItems(nextItems);
// Auto-select all installable items when scanning returns a small set.
const nextSelected: Record<string, boolean> = {};
for (const item of nextItems) {
if (item.installable) {
nextSelected[item.skillDir] = true;
}
}
setSelected(nextSelected);
setIdentities([]);
toast.success(t('settings.skills.catalog.shared.toast.foundSkills', { count: nextItems.length }));
};
const doInstall = async (opts: { conflictDecisions?: Record<string, ConflictDecision> }) => {
if (selectedDirs.length === 0) {
toast.error(t('settings.skills.catalog.installFromRepo.toast.selectAtLeastOne'));
return;
}
const request = {
source: source.trim(),
subpath: subpath.trim() || undefined,
scope,
targetSource,
selections: selectedDirs.map((dir) => ({ skillDir: dir })),
gitIdentityId: gitIdentityId || undefined,
directoryOverride,
};
const result = await installSkills(
{
source: request.source,
subpath: request.subpath,
scope: request.scope,
targetSource: request.targetSource,
selections: request.selections,
gitIdentityId: request.gitIdentityId,
conflictPolicy: 'prompt',
conflictDecisions: opts.conflictDecisions,
},
{ directory: request.directoryOverride ?? null }
);
if (result.ok) {
const installedCount = result.installed?.length || 0;
toast.success(
installedCount > 0
? t('settings.skills.catalog.installFromRepo.toast.installedCount', { count: installedCount })
: t('settings.skills.catalog.installFromRepo.toast.installCompleted')
);
onOpenChange(false);
return;
}
if (result.error?.kind === 'conflicts') {
setBaseInstallRequest(request);
setConflicts(result.error.conflicts);
setConflictsOpen(true);
return;
}
if (result.error?.kind === 'authRequired') {
if (isVSCodeRuntime()) {
toast.error(t('settings.skills.catalog.shared.toast.privateRepoNotSupportedVsCode'));
return;
}
const ids = (result.error.identities || []) as IdentityOption[];
setIdentities(ids);
if (!gitIdentityId && ids.length > 0) {
const preferred =
defaultGitIdentityId &&
defaultGitIdentityId !== 'global' &&
ids.some((i) => i.id === defaultGitIdentityId)
? defaultGitIdentityId
: ids[0].id;
setGitIdentityId(preferred);
}
toast.error(t('settings.skills.catalog.installFromRepo.toast.authenticationRequiredInstall'));
return;
}
toast.error(result.error?.message || t('settings.skills.catalog.installFromRepo.toast.installFailed'));
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[85vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>{t('settings.skills.catalog.installFromRepo.title')}</DialogTitle>
<DialogDescription>
{t('settings.skills.catalog.installFromRepo.descriptionPrefix')}
{' '}
<code className="font-mono">SKILL.md</code>
{t('settings.skills.catalog.installFromRepo.descriptionSuffix')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 flex-shrink-0">
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">{t('settings.skills.catalog.shared.field.repository')}</label>
<div className="flex items-center gap-2">
<Input
value={source}
onChange={(e) => {
setSource(e.target.value);
invalidateScan({ clearIdentities: true });
}}
placeholder={t('settings.skills.catalog.shared.field.repositoryPlaceholder')}
className="text-foreground placeholder:text-muted-foreground"
/>
<Button
type="button"
variant="outline"
onClick={() => void handleScan()}
disabled={isScanning || !source.trim()}
className="gap-2"
>
<Icon name="git-repository" className="h-4 w-4" />
{isScanning ? t('settings.skills.catalog.shared.actions.scanning') : t('settings.skills.catalog.shared.actions.scan')}
</Button>
</div>
<p className="typography-meta text-muted-foreground">
{t('settings.skills.catalog.installFromRepo.repositoryHintPrefix')}
{' '}
<code className="font-mono">owner/repo/skills</code>
{'.'}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">{t('settings.skills.catalog.shared.field.optionalSubpath')}</label>
<Input
value={subpath}
onChange={(e) => {
setSubpath(e.target.value);
invalidateScan({ clearIdentities: true });
}}
placeholder={t('settings.skills.catalog.shared.field.subpathPlaceholder')}
className="text-foreground placeholder:text-muted-foreground"
/>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">{t('settings.skills.catalog.shared.field.targetLocation')}</label>
<Select
value={locationValueFrom(scope, targetSource)}
onValueChange={(v) => {
const next = locationPartsFrom(v as SkillLocationValue);
setScope(next.scope);
setTargetSource(next.source === 'agents' ? 'agents' : 'opencode');
}}
>
<SelectTrigger size="lg" className="w-full gap-1.5">
{scope === 'user' ? <Icon name="user-3" className="h-4 w-4" /> : <Icon name="folder" className="h-4 w-4" />}
{targetSource === 'agents' ? <Icon name="robot-2" className="h-4 w-4" /> : null}
<span>{locationLabelText(locationValueFrom(scope, targetSource))}</span>
</SelectTrigger>
<SelectContent align="start">
{SKILL_LOCATION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value} className="pr-2 [&>span:first-child]:hidden">
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
{option.scope === 'user' ? <Icon name="user-3" className="h-4 w-4" /> : <Icon name="folder" className="h-4 w-4" />}
{option.source === 'agents' ? <Icon name="robot-2" className="h-4 w-4" /> : null}
<span>{locationLabelText(option.value)}</span>
</div>
<span className="typography-micro text-muted-foreground ml-6">{locationDescriptionText(option.value)}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{scope === 'project' && (
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">{t('settings.skills.catalog.shared.field.project')}</label>
{projects.length === 0 ? (
<p className="typography-meta text-muted-foreground">{t('settings.skills.catalog.shared.field.noProjects')}</p>
) : (
<Select
value={resolvedTargetProjectId ?? ''}
onValueChange={(v) => setTargetProjectId(v)}
disabled={projects.length === 1}
>
<SelectTrigger size="lg" className="w-full justify-between">
<SelectValue placeholder={t('settings.skills.catalog.shared.field.chooseProjectPlaceholder')} />
</SelectTrigger>
<SelectContent align="start">
{projects.map((p) => (
<SelectItem key={p.id} value={p.id} className="pr-2 [&>span:first-child]:hidden">
{p.label || p.path}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
)}
{identities.length > 0 && !isVSCodeRuntime() ? (
<div className="rounded-lg border bg-muted/20 px-3 py-2">
<div className="typography-ui-label font-medium text-foreground">{t('settings.skills.catalog.shared.auth.title')}</div>
<div className="typography-meta text-muted-foreground mt-1">
{t('settings.skills.catalog.installFromRepo.authDescription')}
</div>
<div className="mt-2">
<Select
value={gitIdentityId || ''}
onValueChange={(v) => {
setGitIdentityId(v);
invalidateScan();
}}
>
<SelectTrigger size="lg" className="w-full justify-between">
<span>{identities.find((i) => i.id === gitIdentityId)?.name || t('settings.skills.catalog.shared.auth.chooseIdentity')}</span>
</SelectTrigger>
<SelectContent align="start">
{identities.map((id) => (
<SelectItem key={id.id} value={id.id} className="pr-2 [&>span:first-child]:hidden">
{id.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="typography-micro text-muted-foreground mt-2">
{t('settings.skills.catalog.shared.auth.footerHintArrow')}
</div>
</div>
) : null}
</div>
<div className="flex-1 min-h-0">
{items.length === 0 ? (
<div className="flex h-full items-center justify-center text-center text-muted-foreground">
<div>
<p className="typography-body">{t('settings.skills.catalog.installFromRepo.empty.noScanResultsTitle')}</p>
<p className="typography-meta mt-1 opacity-75">{t('settings.skills.catalog.installFromRepo.empty.noScanResultsDescription')}</p>
</div>
</div>
) : (
<div className="h-full flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('settings.skills.catalog.shared.field.searchSkillsPlaceholder')}
className="max-w-sm"
/>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => toggleAll(true)}>{t('settings.skills.catalog.installFromRepo.actions.selectAll')}</Button>
<Button variant="outline" size="sm" onClick={() => toggleAll(false)}>{t('settings.skills.catalog.installFromRepo.actions.selectNone')}</Button>
</div>
</div>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-2">
{filteredItems.map((item) => {
const installed = installedByName.get(item.skillName);
const checked = Boolean(selected[item.skillDir]);
const disabled = !item.installable;
return (
<label
key={item.skillDir}
className={
'flex items-start gap-3 rounded-lg border bg-muted/10 px-3 py-2 cursor-pointer transition-colors ' +
(disabled ? 'opacity-60 cursor-not-allowed' : 'hover:bg-interactive-hover/20')
}
>
<div className="mt-1">
<Checkbox
checked={checked}
disabled={disabled}
onChange={(newChecked) => setSelected((prev) => ({ ...prev, [item.skillDir]: newChecked }))}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<div className="typography-ui-label truncate">{item.skillName}</div>
{installed ? (
<span className="typography-micro text-muted-foreground bg-muted px-1 rounded flex-shrink-0 leading-none pb-px border border-border/50">
{t('settings.skills.catalog.installFromRepo.badge.installed', {
scope: installed.scope,
source: installed.source,
})}
</span>
) : null}
</div>
{item.description ? (
<div className="typography-meta text-muted-foreground mt-0.5 line-clamp-2">{item.description}</div>
) : (
<div className="typography-micro text-muted-foreground mt-0.5">{t('settings.skills.catalog.shared.noDescription')}</div>
)}
{item.warnings?.length ? (
<div className="typography-micro text-muted-foreground mt-1">
{item.warnings.join(' · ')}
</div>
) : null}
</div>
</label>
);
})}
</ScrollableOverlay>
<div className="typography-meta text-muted-foreground">
{t('settings.skills.catalog.installFromRepo.selectedCount', {
selected: selectedDirs.length,
total: items.filter((i) => i.installable).length,
})}
</div>
</div>
)}
</div>
<DialogFooter className="flex-shrink-0">
<Button size="sm" variant="ghost" onClick={() => onOpenChange(false)}>
{t('settings.common.actions.cancel')}
</Button>
<Button
size="sm"
disabled={isInstalling || selectedDirs.length === 0 || !source.trim() || (scope === 'project' && !directoryOverride)}
onClick={() => void doInstall({})}
>
{isInstalling ? t('settings.skills.catalog.shared.actions.installing') : t('settings.skills.catalog.installFromRepo.actions.installSelected')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<InstallConflictsDialog
open={conflictsOpen}
onOpenChange={setConflictsOpen}
conflicts={conflicts}
onConfirm={(decisions) => {
if (!baseInstallRequest) {
setConflictsOpen(false);
return;
}
void doInstall({ conflictDecisions: decisions });
setConflictsOpen(false);
}}
/>
</>
);
};
@@ -1,2 +0,0 @@
export { SkillsSidebar } from './SkillsSidebar';
export { SkillsPage } from './SkillsPage';
@@ -57,10 +57,3 @@ export function locationPartsFrom(value: SkillLocationValue): { scope: SkillScop
}
return { scope: match.scope, source: match.source };
}
export function locationLabel(scope: SkillScope, source: SkillSource): string {
if (scope === 'user' && source === 'claude') return 'User / Claude';
if (scope === 'project' && source === 'claude') return 'Project / Claude';
const match = SKILL_LOCATION_OPTIONS.find((option) => option.scope === scope && option.source === source);
return match?.label || `${scope} / ${source}`;
}