diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 5d86001..37943c6 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -236,6 +236,102 @@ const safeLocalStorage = { } }; +const CLAUDE_SETTINGS_KEY = 'claude-settings'; +const TOOL_PERMISSION_ERROR_REGEX = /requested permissions? to use\s+([^.,\n]+)/i; + +function safeJsonParse(value) { + if (!value || typeof value !== 'string') return null; + try { + return JSON.parse(value); + } catch { + return null; + } +} + +function getClaudeSettings() { + const raw = safeLocalStorage.getItem(CLAUDE_SETTINGS_KEY); + if (!raw) { + return { + allowedTools: [], + disallowedTools: [], + skipPermissions: false, + projectSortOrder: 'name' + }; + } + + try { + const parsed = JSON.parse(raw); + return { + ...parsed, + allowedTools: Array.isArray(parsed.allowedTools) ? parsed.allowedTools : [], + disallowedTools: Array.isArray(parsed.disallowedTools) ? parsed.disallowedTools : [], + skipPermissions: Boolean(parsed.skipPermissions), + projectSortOrder: parsed.projectSortOrder || 'name' + }; + } catch { + return { + allowedTools: [], + disallowedTools: [], + skipPermissions: false, + projectSortOrder: 'name' + }; + } +} + +function buildClaudeToolPermissionEntry(toolName, toolInput) { + if (!toolName) return null; + if (toolName !== 'Bash') return toolName; + + const parsed = safeJsonParse(toolInput); + const command = typeof parsed?.command === 'string' ? parsed.command.trim() : ''; + if (!command) return toolName; + + const tokens = command.split(/\s+/); + if (tokens.length === 0) return toolName; + + // For Bash, allow the command family instead of every Bash invocation. + if (tokens[0] === 'git' && tokens[1]) { + return `Bash(${tokens[0]} ${tokens[1]}:*)`; + } + return `Bash(${tokens[0]}:*)`; +} + +function getClaudePermissionSuggestion(message, provider) { + if (provider !== 'claude') return null; + if (!message?.toolResult?.isError) return null; + + const content = String(message.toolResult.content || ''); + if (!TOOL_PERMISSION_ERROR_REGEX.test(content)) return null; + + const match = content.match(TOOL_PERMISSION_ERROR_REGEX); + const requestedTool = match?.[1]?.trim(); + const toolName = requestedTool || message.toolName; + const entry = buildClaudeToolPermissionEntry(toolName, message.toolInput); + if (!entry) return null; + + const settings = getClaudeSettings(); + const isAllowed = settings.allowedTools.includes(entry); + return { toolName: toolName || entry, entry, isAllowed }; +} + +function grantClaudeToolPermission(entry) { + if (!entry) return { success: false }; + + const settings = getClaudeSettings(); + const alreadyAllowed = settings.allowedTools.includes(entry); + const nextAllowed = alreadyAllowed ? settings.allowedTools : [...settings.allowedTools, entry]; + const nextDisallowed = settings.disallowedTools.filter(tool => tool !== entry); + const updatedSettings = { + ...settings, + allowedTools: nextAllowed, + disallowedTools: nextDisallowed, + lastUpdated: new Date().toISOString() + }; + + safeLocalStorage.setItem(CLAUDE_SETTINGS_KEY, JSON.stringify(updatedSettings)); + return { success: true, alreadyAllowed, updatedSettings }; +} + // Common markdown components to ensure consistent rendering (tables, inline code, links, etc.) const markdownComponents = { code: ({ node, inline, className, children, ...props }) => { @@ -356,7 +452,7 @@ const markdownComponents = { }; // Memoized message component to prevent unnecessary re-renders -const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters, showThinking, selectedProject }) => { +const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }) => { const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || (prevMessage.type === 'user') || @@ -364,6 +460,13 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile (prevMessage.type === 'error')); const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); + const permissionSuggestion = getClaudePermissionSuggestion(message, provider); + const [permissionGrantState, setPermissionGrantState] = React.useState('idle'); + + React.useEffect(() => { + setPermissionGrantState('idle'); + }, [permissionSuggestion?.entry, message.toolId]); + React.useEffect(() => { if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; @@ -1358,6 +1461,59 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ); })()} + {permissionSuggestion && ( +