* refactor(settings): new IA shell + projects section + skills catalog discoverability * chore(settings): split providers list by scope; show user before project * fix: navigation flow in mobile Settings * feat: redesign settings pages to use modern elevated surface patterns * feat: replace helper text with tooltips in settings * ui: redesign update dialog and fix external link routing - Restructures UpdateDialog to focus on changelog readability with a wider max-w-4xl canvas - Highlights @username contributor mentions with theme primary color - Strips excessive vertical padding and right-aligns compact action buttons - Disables streamdown's internal link safety dialog in favor of direct Tauri shell routing * feat: refactor Git identities into dedicated Git settings page * feat: unify sidebar background styling across VS Code and web/mobile * fix: adjust button styling and layout for mobile settings pages * feat: add MCP settings page and sidebar * feat: hide models in provider view (thanks to @nguyenngothuong) * feat: add "Add new provider" option to model selector dropdown * fix: local evroc logo + provider dropdown icons * fix: increase width of provider menu * fix: dark theme background color for better contrast * feat: update @opencode-ai/sdk dependency to v1.2.10 * fix: restore session sorting to only use updated time * fix: added settings for sessions deletion dialog * fix: adjust padding on settings pages for better layout * fix: standardize select dropdown height across UI * fix: agent selector UI and notification settings * fix: remove redundant helper text from settings pages * fix: update UI layout for description fields * fix: remove border-none and shadow-none from textarea classes * fix: enable context menu on sidebar items * feat: refactor UI controls and layout patterns across settings pages * fix: use headerless blocks when page title already provides context * fix: remove subtask option from command settings * fix: refactor mcp page settings * fix: reduce spacing in skills configuration pages * feat: refactor voice settings * feat: refactor settings sidebar sections
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { RiArrowDownSLine, RiFolderLine } from '@remixicon/react';
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const formatProjectLabel = (label: string): string => {
|
|
return label.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
};
|
|
|
|
export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ className }) => {
|
|
const projects = useProjectsStore((state) => state.projects);
|
|
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
|
const setActiveProject = useProjectsStore((state) => state.setActiveProject);
|
|
|
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
|
|
|
const sortedProjects = React.useMemo(() => {
|
|
return [...projects].sort((a, b) => (a.label || a.path).localeCompare(b.label || b.path));
|
|
}, [projects]);
|
|
|
|
const activeProject = React.useMemo(() => {
|
|
if (sortedProjects.length === 0) {
|
|
return null;
|
|
}
|
|
return sortedProjects.find((p) => p.id === activeProjectId) ?? sortedProjects[0];
|
|
}, [activeProjectId, sortedProjects]);
|
|
|
|
if (isVSCode || sortedProjects.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const rawLabel = activeProject?.label && activeProject.label.trim().length > 0
|
|
? activeProject.label
|
|
: (activeProject?.path.split('/').filter(Boolean).pop() || activeProject?.path || 'Project');
|
|
const label = formatProjectLabel(rawLabel);
|
|
|
|
return (
|
|
<div className={cn(className)}>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
aria-label="Switch project"
|
|
title="Switch project"
|
|
className={cn(
|
|
// Mirror Input sizing so headers align visually.
|
|
'text-foreground border border-border/80 appearance-none flex h-8 w-full min-w-0 rounded-lg bg-transparent px-3 py-1 outline-none',
|
|
'hover:border-input focus-visible:ring-1 focus-visible:ring-primary/50 focus-visible:border-primary/70',
|
|
'flex items-center gap-1.5 text-left'
|
|
)}
|
|
>
|
|
<RiFolderLine className="h-4 w-4 opacity-70" />
|
|
<span className="min-w-0 flex-1 truncate typography-ui-label font-medium">{label}</span>
|
|
<RiArrowDownSLine className="size-4 opacity-50" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-auto">
|
|
<DropdownMenuRadioGroup
|
|
value={activeProject?.id ?? ''}
|
|
onValueChange={(value) => {
|
|
if (!value) return;
|
|
setActiveProject(value);
|
|
}}
|
|
>
|
|
{sortedProjects.map((project) => {
|
|
const raw = project.label?.trim()
|
|
? project.label.trim()
|
|
: (project.path.split('/').filter(Boolean).pop() || project.path);
|
|
const itemLabel = formatProjectLabel(raw);
|
|
return (
|
|
<DropdownMenuRadioItem key={project.id} value={project.id}>
|
|
<span className="min-w-0 truncate typography-ui">{itemLabel}</span>
|
|
</DropdownMenuRadioItem>
|
|
);
|
|
})}
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
};
|