feat: add multi-project support (#110)
* feat: Implement project management store with project path validation and synchronization - Added `useProjectsStore` for managing projects, including adding, removing, renaming, and validating project paths. - Implemented persistence for projects and active project ID using safe storage. - Introduced synchronization from desktop settings to keep project data consistent. - Enhanced session store to manage sessions by directory and added new methods for session management. - Updated todo store to fetch session todos based on the directory context. - Refactored server code to validate and resolve project directories for various API endpoints. - Added project entry validation and sanitization to ensure data integrity. * feat(settings): migrate legacy project settings and update settings loading logic * feat: enhance project management with directory-aware settings and improved agent/command source handling * feat: enhance session and project management with directory-aware settings and improved configuration refresh logic * feat: enhance project management with worktree manager integration and project directory resolution * feat: enhance agent groups store with project directory resolution and loading logic * feat: add heartbeat management and global wrapping for SSE blocks in agent and chat providers * feat: refactor command and project handling in useCommandsStore - Replaced useDirectoryStore with useProjectsStore to manage project paths. - Introduced getRequestDirectory function to determine the active project directory. - Updated command fetching to respect project-level scoping. - Enhanced error handling and logging for command configuration fetching. - Improved command configuration saving and updating to utilize project directory context. feat: enhance project path normalization in useProjectsStore - Added resolveTildePath function to expand paths starting with ~. - Updated normalizeProjectPath to utilize home directory for path expansion. fix: update permission handling in useSessionStore - Changed Permission type to PermissionRequest for clarity. - Updated respondToPermission method to use requestId instead of permissionId. refactor: improve permission utilities - Introduced types for PermissionAction and PermissionRule. - Enhanced getAgentDefinition and resolveConfigStore functions for better type safety. - Added resolvePermissionAction to streamline permission resolution logic. feat: add agent configuration retrieval endpoint - Implemented new API endpoint to fetch agent configuration based on project directory. - Enhanced getAgentPermissionSource to prioritize project-level permissions. chore: update SDK version in package.json files - Bumped @opencode-ai/sdk version to ^1.1.1 across all relevant package.json files. refactor: streamline bridge message handling - Updated handleBridgeMessage to accept directory parameter for agent and command requests. - Improved local API request handling to extract directory from query parameters and headers. feat: enhance project configuration management - Added functions to retrieve and merge project configuration paths. - Improved handling of existing project configuration files for agents and commands. * feat: enhance VSCode integration and session management - Added support for a sticky sidebar header background in light and dark themes. - Introduced functions to read VSCode workspace directory and check if running in VSCode. - Implemented detailed logging for session loading and creation processes. - Enhanced session filtering based on directory structure and canonical paths. - Added a new method to reorder projects and prevent modifications in VSCode workspace. - Improved error handling and logging for app initialization and markdown file parsing. - Updated API checks and health checks to ensure readiness before proceeding. - Refactored code for better readability and maintainability across various modules. * feat: improve agent and branch selection logic, enhance session management, and update multi-run creation response * feat: add worktree management actions in agent group detail and sidebar, including delete and keep only options * fix(ui): share IME guard and cover multi-run * fix(session): reduce maximum visible sessions in group from 7 to 5
This commit is contained in:
committed by
GitHub
parent
8aa379e313
commit
18c5b4c7b5
@@ -2,8 +2,20 @@ import React from 'react';
|
||||
import { cn, getModifierLabel } from '@/lib/utils';
|
||||
import { SIDEBAR_SECTIONS } from '@/constants/sidebar';
|
||||
import type { SidebarSection } from '@/constants/sidebar';
|
||||
import { RiArrowLeftSLine, RiCloseLine } from '@remixicon/react';
|
||||
import { RiArrowDownSLine, RiArrowLeftSLine, RiCloseLine, RiFolderLine } from '@remixicon/react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useAgentsStore } from '@/stores/useAgentsStore';
|
||||
import { useCommandsStore } from '@/stores/useCommandsStore';
|
||||
import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import { AgentsSidebar } from '@/components/sections/agents/AgentsSidebar';
|
||||
import { AgentsPage } from '@/components/sections/agents/AgentsPage';
|
||||
import { CommandsSidebar } from '@/components/sections/commands/CommandsSidebar';
|
||||
@@ -99,8 +111,61 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const activeProjectId = useProjectsStore((state) => state.activeProjectId);
|
||||
const setActiveProject = useProjectsStore((state) => state.setActiveProject);
|
||||
|
||||
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]);
|
||||
|
||||
// Format project label: kebab-case/snake_case → Title Case
|
||||
const formatProjectLabel = React.useCallback((label: string): string => {
|
||||
return label
|
||||
.replace(/[-_]/g, ' ')
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}, []);
|
||||
|
||||
const activeProjectLabel = React.useMemo(() => {
|
||||
if (!activeProject) {
|
||||
return 'Project';
|
||||
}
|
||||
const rawLabel = activeProject.label && activeProject.label.trim().length > 0
|
||||
? activeProject.label
|
||||
: (activeProject.path.split('/').filter(Boolean).pop() || activeProject.path);
|
||||
return formatProjectLabel(rawLabel);
|
||||
}, [activeProject, formatProjectLabel]);
|
||||
|
||||
const showProjectSwitcher = sortedProjects.length > 0;
|
||||
|
||||
const showTabLabels = containerWidth === 0 || containerWidth >= TAB_LABELS_MIN_WIDTH;
|
||||
|
||||
React.useEffect(() => {
|
||||
// Force reload when activeProject changes to ensure scopes update
|
||||
if (activeTab === 'agents') {
|
||||
// Small delay to allow store state to propagate if needed
|
||||
setTimeout(() => void useAgentsStore.getState().loadAgents(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTab === 'commands') {
|
||||
setTimeout(() => void useCommandsStore.getState().loadCommands(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTab === 'skills') {
|
||||
void useSkillsStore.getState().loadSkills();
|
||||
void useSkillsCatalogStore.getState().loadCatalog();
|
||||
}
|
||||
}, [activeProjectId, activeTab]);
|
||||
|
||||
// Update proportional width on window resize (if not manually resized)
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -341,26 +406,79 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
})}
|
||||
</div>
|
||||
|
||||
{onClose && (
|
||||
<div className={cn('flex items-center', isMobile ? '' : 'pr-3')}>
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close settings"
|
||||
className={cn(
|
||||
'inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
!isMobile && 'app-region-no-drag'
|
||||
{(onClose || showProjectSwitcher) && (
|
||||
<div className={cn('flex items-center gap-2', isMobile ? '' : 'pr-3')}>
|
||||
{showProjectSwitcher && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{isMobile ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Switch project"
|
||||
title={activeProjectLabel}
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiFolderLine className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Switch project"
|
||||
title={activeProjectLabel}
|
||||
className={cn(
|
||||
'flex h-9 max-w-[18rem] items-center gap-1.5 bg-transparent px-2 text-foreground outline-none hover:text-foreground/80 focus-visible:ring-2 focus-visible:ring-ring',
|
||||
!isMobile && 'app-region-no-drag'
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate typography-ui-label font-medium">{activeProjectLabel}</span>
|
||||
<RiArrowDownSLine className="size-4 opacity-50" />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Close Settings ({shortcutKey}+,)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-auto">
|
||||
<DropdownMenuRadioGroup
|
||||
value={activeProject?.id ?? ''}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
setActiveProject(value);
|
||||
}}
|
||||
>
|
||||
{sortedProjects.map((project) => {
|
||||
const rawLabel = project.label?.trim()
|
||||
? project.label.trim()
|
||||
: (project.path.split('/').filter(Boolean).pop() || project.path);
|
||||
const label = formatProjectLabel(rawLabel);
|
||||
return (
|
||||
<DropdownMenuRadioItem key={project.id} value={project.id}>
|
||||
<span className="min-w-0 truncate typography-ui">{label}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{onClose && (
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close settings"
|
||||
className={cn(
|
||||
'inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
!isMobile && 'app-region-no-drag'
|
||||
)}
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Close Settings ({shortcutKey}+,)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user