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
@@ -30,6 +30,7 @@ import { toast } from 'sonner';
|
||||
import { useFileStore } from '@/stores/fileStore';
|
||||
import { calculateEditPermissionUIState, type BashPermissionSetting } from '@/lib/permissions/editPermissionDefaults';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isIMECompositionEvent } from '@/lib/ime';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -47,14 +48,68 @@ interface ChatInputProps {
|
||||
|
||||
const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null;
|
||||
|
||||
/**
|
||||
* Detects if a keyboard event is part of IME composition.
|
||||
* Uses both isComposing and keyCode === 229 (MDN recommended).
|
||||
* WebKit may fire compositionend before keydown, causing isComposing to be false
|
||||
* while keyCode remains 229, so both checks are needed.
|
||||
*/
|
||||
const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => {
|
||||
return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
||||
type PermissionAction = 'allow' | 'ask' | 'deny';
|
||||
type PermissionRule = { permission: string; pattern: string; action: PermissionAction };
|
||||
|
||||
const asPermissionRuleset = (value: unknown): PermissionRule[] | null => {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const rules: PermissionRule[] = [];
|
||||
for (const entry of value) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
continue;
|
||||
}
|
||||
const candidate = entry as Partial<PermissionRule>;
|
||||
if (typeof candidate.permission !== 'string' || typeof candidate.pattern !== 'string' || typeof candidate.action !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (candidate.action !== 'allow' && candidate.action !== 'ask' && candidate.action !== 'deny') {
|
||||
continue;
|
||||
}
|
||||
rules.push({ permission: candidate.permission, pattern: candidate.pattern, action: candidate.action });
|
||||
}
|
||||
return rules;
|
||||
};
|
||||
|
||||
const resolveWildcardPermissionAction = (ruleset: unknown, permission: string): PermissionAction | undefined => {
|
||||
const rules = asPermissionRuleset(ruleset);
|
||||
if (!rules || rules.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (let i = rules.length - 1; i >= 0; i -= 1) {
|
||||
const rule = rules[i];
|
||||
if (rule.permission === permission && rule.pattern === '*') {
|
||||
return rule.action;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = rules.length - 1; i >= 0; i -= 1) {
|
||||
const rule = rules[i];
|
||||
if (rule.permission === '*' && rule.pattern === '*') {
|
||||
return rule.action;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildPermissionActionMap = (ruleset: unknown, permission: string): Record<string, PermissionAction | undefined> | undefined => {
|
||||
const rules = asPermissionRuleset(ruleset);
|
||||
if (!rules || rules.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const map: Record<string, PermissionAction | undefined> = {};
|
||||
for (const rule of rules) {
|
||||
if (rule.permission !== permission) {
|
||||
continue;
|
||||
}
|
||||
map[rule.pattern] = rule.action;
|
||||
}
|
||||
|
||||
return Object.keys(map).length > 0 ? map : undefined;
|
||||
};
|
||||
|
||||
export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBottom }) => {
|
||||
@@ -184,19 +239,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
}, [agents, currentAgentName]);
|
||||
|
||||
const agentDefaultEditMode = React.useMemo<EditPermissionMode>(() => {
|
||||
const agentPermissionRaw = currentAgent?.permission?.edit;
|
||||
let defaultMode: EditPermissionMode = 'ask';
|
||||
|
||||
if (agentPermissionRaw === 'allow' || agentPermissionRaw === 'ask' || agentPermissionRaw === 'deny' || agentPermissionRaw === 'full') {
|
||||
defaultMode = agentPermissionRaw;
|
||||
if (!currentAgent) {
|
||||
return 'deny';
|
||||
}
|
||||
|
||||
const editToolConfigured = currentAgent ? (currentAgent.tools?.['edit'] !== false) : false;
|
||||
if (!currentAgent || !editToolConfigured) {
|
||||
defaultMode = 'deny';
|
||||
}
|
||||
|
||||
return defaultMode;
|
||||
const action = resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'ask';
|
||||
return action;
|
||||
}, [currentAgent]);
|
||||
|
||||
const sessionAgentEditOverride = useSessionStore(
|
||||
@@ -209,8 +257,20 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
}, [currentSessionId, currentAgentName])
|
||||
);
|
||||
|
||||
const agentWebfetchPermission = currentAgent?.permission?.webfetch;
|
||||
const agentBashPermission = currentAgent?.permission?.bash as BashPermissionSetting | undefined;
|
||||
const agentWebfetchPermission = React.useMemo(() => {
|
||||
if (!currentAgent) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveWildcardPermissionAction(currentAgent.permission, 'webfetch');
|
||||
}, [currentAgent]);
|
||||
|
||||
const agentBashPermission = React.useMemo<BashPermissionSetting | undefined>(() => {
|
||||
if (!currentAgent) {
|
||||
return undefined;
|
||||
}
|
||||
const map = buildPermissionActionMap(currentAgent.permission, 'bash');
|
||||
return map ? (map as BashPermissionSetting) : undefined;
|
||||
}, [currentAgent]);
|
||||
|
||||
const permissionUiState = React.useMemo(() => calculateEditPermissionUIState({
|
||||
agentDefaultEditMode,
|
||||
@@ -486,8 +546,8 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
}, [sessionPhase, queuedMessages.length, currentSessionId, currentProviderId, currentModelId, sessionAbortFlags]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Early return during IME composition to prevent interference with autocomplete
|
||||
// Uses keyCode === 229 fallback for WebKit where compositionend fires before keydown
|
||||
// Early return during IME composition to prevent interference with autocomplete.
|
||||
// Uses keyCode === 229 fallback for WebKit where compositionend fires before keydown.
|
||||
if (isIMECompositionEvent(e)) return;
|
||||
|
||||
if (showCommandAutocomplete && commandRef.current) {
|
||||
@@ -521,7 +581,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
}
|
||||
|
||||
// Handle Enter/Ctrl+Enter based on queue mode
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isMobile && !isIMECompositionEvent(e)) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !isMobile) {
|
||||
e.preventDefault();
|
||||
|
||||
const isCtrlEnter = e.ctrlKey || e.metaKey;
|
||||
|
||||
Reference in New Issue
Block a user