Refactor permission management across UI components and stores (#221)

* refactor: normalize permission rules and improve command draft handling

Add normalized rule set construction for permissions in Agents pages
Track initial command draft state to detect unsaved changes
Prefill draft fields when duplicating or selecting existing commands

* feat: add global session-level edit mode for agent permissions
This commit is contained in:
Bohdan Triapitsyn
2026-01-26 16:09:46 +02:00
committed by GitHub
parent 74511abfda
commit a9952fd8da
8 changed files with 785 additions and 898 deletions
+24 -54
View File
@@ -29,7 +29,6 @@ import { useAssistantStatus } from '@/hooks/useAssistantStatus';
import { useCurrentSessionActivity } from '@/hooks/useSessionActivity';
import { toast } from '@/components/ui';
import { useFileStore } from '@/stores/fileStore';
import { calculateEditPermissionUIState, type BashPermissionSetting } from '@/lib/permissions/editPermissionDefaults';
import { isVSCodeRuntime } from '@/lib/desktop';
import { isIMECompositionEvent } from '@/lib/ime';
import {
@@ -38,6 +37,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useContextStore } from '@/stores/contextStore';
const MAX_VISIBLE_TEXTAREA_LINES = 8;
const EMPTY_QUEUE: QueuedMessage[] = [];
@@ -96,23 +96,6 @@ const resolveWildcardPermissionAction = (ruleset: unknown, permission: string):
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 }) => {
const [message, setMessage] = React.useState('');
const [isDragging, setIsDragging] = React.useState(false);
@@ -236,60 +219,47 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}, [pendingInputText, consumePendingInputText]);
const currentAgent = React.useMemo(() => {
if (!currentAgentName) {
const selectedName = currentSessionId
? (useContextStore.getState().getSessionAgentSelection(currentSessionId) || currentAgentName)
: currentAgentName;
if (!selectedName) {
return undefined;
}
return agents.find((agent) => agent.name === currentAgentName);
}, [agents, currentAgentName]);
return agents.find((agent) => agent.name === selectedName);
}, [agents, currentAgentName, currentSessionId]);
const agentDefaultEditMode = React.useMemo<EditPermissionMode>(() => {
const agentEditAction = React.useMemo<EditPermissionMode>(() => {
if (!currentAgent) {
return 'deny';
}
const action = resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'ask';
return action;
return resolveWildcardPermissionAction(currentAgent.permission, 'edit') ?? 'allow';
}, [currentAgent]);
const sessionAgentEditOverride = useSessionStore(
const sessionEditMode = useContextStore(
React.useCallback((state) => {
if (!currentSessionId || !currentAgentName) {
if (!currentAgentName) {
return undefined;
}
const sessionMap = state.sessionAgentEditModes.get(currentSessionId);
return sessionMap?.get(currentAgentName);
}, [currentSessionId, currentAgentName])
const sessionId = currentSessionId ?? '__global__';
return state.getSessionAgentEditMode(sessionId, currentAgentName, 'ask');
}, [currentAgentName, currentSessionId])
);
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,
webfetchPermission: agentWebfetchPermission,
bashPermission: agentBashPermission,
}), [agentDefaultEditMode, agentWebfetchPermission, agentBashPermission]);
const selectionContextReady = Boolean(currentSessionId && currentAgentName);
const effectiveEditPermission = React.useMemo<EditPermissionMode>(() => {
if (selectionContextReady && sessionAgentEditOverride && permissionUiState.modeAvailability[sessionAgentEditOverride]) {
return sessionAgentEditOverride;
// Only show accent when edits are effectively allowed.
if (agentEditAction === 'allow') {
return 'allow';
}
return permissionUiState.cascadeDefaultMode;
}, [permissionUiState, selectionContextReady, sessionAgentEditOverride]);
if (agentEditAction !== 'ask') {
return 'ask';
}
const sessionMode = selectionContextReady ? (sessionEditMode ?? 'ask') : 'ask';
return (sessionMode === 'allow' || sessionMode === 'full') ? 'allow' : 'ask';
}, [agentEditAction, selectionContextReady, sessionEditMode]);
const chatInputAccent = React.useMemo(() => getEditModeColors(effectiveEditPermission), [effectiveEditPermission]);