Fix command truncation and prepare for approval UI
- Fix regex pattern in semantic-validator.js that was truncating domain names (google.com -> google) - Remove unnecessary 'info' type error reporting to bug tracker - Only add bug tracker activity when errorId is valid - Add terminal approval UI design document Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
522
public/claude-ide/semantic-validator.js
Normal file
522
public/claude-ide/semantic-validator.js
Normal file
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* Semantic Error Detection Layer
|
||||
* Detects intent/behavior mismatches, conversational messages executed as commands,
|
||||
* and confusing UX messages in the chat interface.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================
|
||||
// CONVERSATIONAL PATTERN DETECTION
|
||||
// ============================================================
|
||||
|
||||
const CONVERSATIONAL_PATTERNS = [
|
||||
// Question words (strong indicators)
|
||||
/^(if|when|where|what|how|why|who|which|whose|can|could|would|should|will|do|does|did|is|are|was|were|am|have|has|had)\s/i,
|
||||
// Pronouns (very strong conversational indicators)
|
||||
/^(i|you|he|she|it|we|they)\s/i,
|
||||
// Agreement/disagreement phrases
|
||||
/^(yes|no|yeah|yep|nope|maybe|ok|okay|sure|alright|fine|go ahead)\s/i,
|
||||
// Politeness markers
|
||||
/^(please|thank|thanks|hey|hello|hi|greetings)\s/i,
|
||||
// Ellipsis (conversational pause)
|
||||
/^\.\.\./,
|
||||
// Conversational transitions with commas
|
||||
/^[a-z]+,\s/i,
|
||||
// Thinking/believing/wanting verbs (indicate opinion, not command)
|
||||
/\b(think|believe|want|need|like|prefer|mean|wish|hope|feel)\b/i,
|
||||
// Context markers (explanatory, not imperative)
|
||||
/\b(because|although|however|therefore|unless|until|since|while)\b/i,
|
||||
// Second-person references to AI's actions
|
||||
/\byou\b.*(ask|tell|want|need|like|prefer|said|mentioned)/i,
|
||||
// First-person expressions
|
||||
/\b(I'm|i am|i'd|i will|i can|i should|i would)\s/i
|
||||
];
|
||||
|
||||
const APPROVAL_PATTERNS = [
|
||||
// Direct approval
|
||||
/^(yes|yeah|yep|sure|okay|ok|go ahead|please do|do it)\b/i,
|
||||
// Direct rejection
|
||||
/^(no|nope|don't|do not|stop|wait)\b/i,
|
||||
// Polite approval
|
||||
/^please\s+(go ahead|proceed|continue|do it)/i,
|
||||
// Casual approval
|
||||
/^(cool|great|awesome|perfect|good|fine)\b/i
|
||||
];
|
||||
|
||||
const CONTEXTUAL_CONVERSATIONAL_PATTERNS = [
|
||||
// Conditional statements about commands
|
||||
/if (i|you|we) asked/i,
|
||||
/when (i|you|we) said/i,
|
||||
/as (i|you|we) mentioned/i,
|
||||
/since (i|you|we) asked/i,
|
||||
// References to previous conversation
|
||||
/like (i|you) said/i,
|
||||
/as (i|you) requested/i
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// COMMAND PATTERN DETECTION (Existing patterns + enhancements)
|
||||
// ============================================================
|
||||
|
||||
const COMMAND_PATTERNS = [
|
||||
// Shell built-ins
|
||||
/^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|rmdir|touch|chmod|chown|ln|head|tail|less|more|sort|uniq|wc|tar|zip|unzip|gzip|gunzip|df|du|ps|top|kill|killall|nohup|bg|fg|jobs|export|unset|env|source|\.|alias|unalias|history|clear|reset|ping|ssh|scp|rsync|curl|wget|file|which|whereis|man|info|traceroute|nslookup|dig|host|netstat|ifconfig|ip)(\s|$)/,
|
||||
// Package managers
|
||||
/^(npm|yarn|pnpm|pip|pip3|conda|brew|apt|apt-get|yum|dnf|pacman)(\s|$)/,
|
||||
// Node.js commands
|
||||
/^(node|npx)(\s|$)/,
|
||||
// Python commands
|
||||
/^(python|python3|pip|pip3|python3-m)(\s|$)/,
|
||||
// Git commands
|
||||
/^git(\s|$)/,
|
||||
// Docker commands
|
||||
/^docker(\s|$)/,
|
||||
// File operations with pipes or redirects (must look technical)
|
||||
/^[a-z0-9_\-./]+\s*[\|>]/,
|
||||
// Build tools
|
||||
/^(make|cmake|cargo|go|rust|ruby|gem|java|javac|gradle|maven|mvn|gcc|g\+\+|clang)(\s|$)/,
|
||||
// Absolute paths
|
||||
/^\//,
|
||||
// Scripts
|
||||
/^(sh|bash|zsh|fish|powershell|pwsh)(\s|$)/
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// CONFUSING UX MESSAGE PATTERNS
|
||||
// ============================================================
|
||||
|
||||
const CONFUSING_OUTPUT_PATTERNS = [
|
||||
{
|
||||
pattern: /exited with code (undefined|null)/i,
|
||||
issue: 'Exit code is undefined/null',
|
||||
suggestion: 'Show actual exit code or success/failure message'
|
||||
},
|
||||
{
|
||||
pattern: /command (succeeded|failed) but output is (empty|undefined|null)/i,
|
||||
issue: 'No output shown to user',
|
||||
suggestion: 'Always show command output or "No output" message'
|
||||
},
|
||||
{
|
||||
pattern: /error:.*undefined/i,
|
||||
issue: 'Generic undefined error',
|
||||
suggestion: 'Provide specific error message'
|
||||
},
|
||||
{
|
||||
pattern: /null.*error/i,
|
||||
issue: 'Null error reference',
|
||||
suggestion: 'Provide meaningful error details'
|
||||
},
|
||||
{
|
||||
pattern: /cannot (read|access).*undefined/i,
|
||||
issue: 'Undefined property access',
|
||||
suggestion: 'Validate properties before access'
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// CONVERSATION STATE TRACKING
|
||||
// ============================================================
|
||||
|
||||
const conversationState = {
|
||||
lastAssistantMessages: [],
|
||||
lastUserMessage: null,
|
||||
intentState: 'IDLE', // IDLE, AWAITING_APPROVAL, EXECUTING_COMMAND, CONVERSING
|
||||
|
||||
addAssistantMessage(message) {
|
||||
this.lastAssistantMessages.push({
|
||||
content: message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
// Keep only last 5 messages
|
||||
if (this.lastAssistantMessages.length > 5) {
|
||||
this.lastAssistantMessages.shift();
|
||||
}
|
||||
this.updateIntentState();
|
||||
},
|
||||
|
||||
addUserMessage(message) {
|
||||
this.lastUserMessage = {
|
||||
content: message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
},
|
||||
|
||||
updateIntentState() {
|
||||
const lastMsg = this.getLastAssistantMessage();
|
||||
if (!lastMsg) {
|
||||
this.intentState = 'IDLE';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if AI is asking for approval
|
||||
const approvalRequest = /\byou want me to|shall i|should i|do you want|would you like|shall i proceed|should i run/i.test(lastMsg.content);
|
||||
if (approvalRequest) {
|
||||
this.intentState = 'AWAITING_APPROVAL';
|
||||
return;
|
||||
}
|
||||
|
||||
this.intentState = 'CONVERSING';
|
||||
},
|
||||
|
||||
getLastAssistantMessage() {
|
||||
return this.lastAssistantMessages[this.lastAssistantMessages.length - 1];
|
||||
},
|
||||
|
||||
getLastNMessages(n) {
|
||||
return this.lastAssistantMessages.slice(-n);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// PUBLIC API
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Check if a message appears to be conversational (not a shell command)
|
||||
* @param {string} message - User's message
|
||||
* @returns {boolean} - True if conversational
|
||||
*/
|
||||
function isConversational(message) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
// Check all conversational patterns
|
||||
return CONVERSATIONAL_PATTERNS.some(pattern => pattern.test(lower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message is an approval response (yes/no/sure/etc)
|
||||
* @param {string} message - User's message
|
||||
* @returns {boolean} - True if approval response
|
||||
*/
|
||||
function isApprovalResponse(message) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
return APPROVAL_PATTERNS.some(pattern => pattern.test(lower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message looks like a contextual conversational message
|
||||
* (e.g., "if I asked you to ping google.com...")
|
||||
* @param {string} message - User's message
|
||||
* @returns {boolean} - True if contextual conversational
|
||||
*/
|
||||
function isContextualConversational(message) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
return CONTEXTUAL_CONVERSATIONAL_PATTERNS.some(pattern => pattern.test(lower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced shell command detection
|
||||
* @param {string} message - User's message
|
||||
* @returns {boolean} - True if it looks like a shell command
|
||||
*/
|
||||
function isShellCommand(message) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return false;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
// FIRST: Check for command requests in conversational language
|
||||
// Pattern: "run ping google.com", "execute ls -la", "can you run npm install?"
|
||||
const commandRequestPatterns = [
|
||||
/\b(run|execute|exec|do|can you run|please run|execute this|run this)\s+(.+?)\b/i,
|
||||
/\b(start|launch|begin|kick off)\s+(.+?)\b/i,
|
||||
/\b(go ahead and\s+)?(run|execute)\b/i
|
||||
];
|
||||
|
||||
for (const pattern of commandRequestPatterns) {
|
||||
const match = lower.match(pattern);
|
||||
if (match && match[2]) {
|
||||
// Extract the command and check if it's valid
|
||||
const extractedCommand = match[2].trim();
|
||||
if (extractedCommand && COMMAND_PATTERNS.some(p => p.test(extractedCommand))) {
|
||||
console.log('[SemanticValidator] Detected command request:', extractedCommand);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND: Check if it's conversational (catch intent errors)
|
||||
if (isConversational(lower)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// THIRD: Check for contextual conversational patterns
|
||||
if (isContextualConversational(lower)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FOURTH: Check if it matches command patterns
|
||||
return COMMAND_PATTERNS.some(pattern => pattern.test(lower));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract actual command from conversational request
|
||||
* @param {string} message - User's message
|
||||
* @returns {string|null} - Extracted command or null
|
||||
*/
|
||||
function extractCommand(message) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
|
||||
// Pattern: "run ping google.com" → "ping google.com"
|
||||
// Pattern: "execute ls -la" → "ls -la"
|
||||
// FIXED: Capture everything after command verb, then trim only trailing sentence punctuation
|
||||
const commandRequestPatterns = [
|
||||
// Match "run/execute [command]" - capture everything to end, trim punctuation later
|
||||
/\b(run|execute|exec|do|can you run|please run|execute this|run this)\s+(.+)/i,
|
||||
/\b(start|launch|begin|kick off)\s+(.+)/i
|
||||
];
|
||||
|
||||
for (const pattern of commandRequestPatterns) {
|
||||
const match = lower.match(pattern);
|
||||
if (match && match[2]) {
|
||||
let extractedCommand = match[2].trim();
|
||||
|
||||
// Remove trailing sentence punctuation (. ! ?) ONLY if at end (not in middle like .com)
|
||||
// Only remove if the punctuation is at the very end after trimming
|
||||
extractedCommand = extractedCommand.replace(/[.!?]+$/g, '').trim();
|
||||
|
||||
// Validate that it's actually a command (check first word)
|
||||
const firstWord = extractedCommand.split(/\s+/)[0];
|
||||
if (firstWord && COMMAND_PATTERNS.some(p => p.test(firstWord))) {
|
||||
// Preserve original case for the command
|
||||
const originalMatch = trimmed.match(pattern);
|
||||
if (originalMatch && originalMatch[2]) {
|
||||
let originalCommand = originalMatch[2].trim();
|
||||
// Remove trailing punctuation from original too
|
||||
originalCommand = originalCommand.replace(/[.!?]+$/, '').trim();
|
||||
console.log('[SemanticValidator] Extracted command:', originalCommand, 'from:', trimmed);
|
||||
return originalCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no command request pattern found, return original message
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect intent mismatch when user responds to approval request
|
||||
* @param {string} userMessage - User's message
|
||||
* @param {string} currentMode - Current chat mode
|
||||
* @returns {object|null} - Error object if mismatch detected, null otherwise
|
||||
*/
|
||||
function detectApprovalIntentMismatch(userMessage, currentMode) {
|
||||
// Only check in Terminal/WebContainer mode
|
||||
if (currentMode !== 'webcontainer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're awaiting approval
|
||||
if (conversationState.intentState !== 'AWAITING_APPROVAL') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if user message is an approval response
|
||||
if (isApprovalResponse(userMessage)) {
|
||||
return {
|
||||
type: 'intent_error',
|
||||
subtype: 'approval_loop',
|
||||
message: 'User responded with approval in Terminal mode, but system may execute it as a command',
|
||||
details: {
|
||||
lastAssistantMessage: conversationState.getLastAssistantMessage()?.content?.substring(0, 100) || '',
|
||||
userMessage: userMessage,
|
||||
mode: currentMode,
|
||||
suggestedAction: 'Interpret approval responses contextually instead of executing as commands'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect conversational messages being sent to Terminal mode
|
||||
* @param {string} message - User's message
|
||||
* @param {string} mode - Current chat mode
|
||||
* @returns {object|null} - Error object if detected, null otherwise
|
||||
*/
|
||||
function detectConversationalCommand(message, mode) {
|
||||
// Only check in Terminal/WebContainer mode
|
||||
if (mode !== 'webcontainer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isConversational(message) || isContextualConversational(message)) {
|
||||
return {
|
||||
type: 'intent_error',
|
||||
subtype: 'conversational_as_command',
|
||||
message: 'Conversational message sent to Terminal mode',
|
||||
details: {
|
||||
userMessage: message,
|
||||
mode: mode,
|
||||
suggestedMode: 'chat',
|
||||
suggestedAction: 'Switch to Chat mode or rephrase as shell command'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect confusing UX messages in command output
|
||||
* @param {string} output - Command output
|
||||
* @returns {object|null} - Error object if detected, null otherwise
|
||||
*/
|
||||
function detectConfusingOutput(output) {
|
||||
if (!output || typeof output !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const confusing of CONFUSING_OUTPUT_PATTERNS) {
|
||||
if (confusing.pattern.test(output)) {
|
||||
return {
|
||||
type: 'ux_issue',
|
||||
subtype: 'confusing_message',
|
||||
message: 'Confusing error message displayed to user',
|
||||
details: {
|
||||
originalOutput: output.substring(0, 200),
|
||||
issue: confusing.issue,
|
||||
suggestedImprovement: confusing.suggestion
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user intent before executing command
|
||||
* @param {string} message - User's message
|
||||
* @param {string} mode - Current chat mode
|
||||
* @returns {object} - Validation result {valid: boolean, error: object|null}
|
||||
*/
|
||||
function validateIntentBeforeExecution(message, mode) {
|
||||
// Check for approval intent mismatch
|
||||
const approvalMismatch = detectApprovalIntentMismatch(message, mode);
|
||||
if (approvalMismatch) {
|
||||
return {
|
||||
valid: false,
|
||||
error: approvalMismatch
|
||||
};
|
||||
}
|
||||
|
||||
// Check for conversational message in command mode
|
||||
const conversationalCmd = detectConversationalCommand(message, mode);
|
||||
if (conversationalCmd) {
|
||||
return {
|
||||
valid: false,
|
||||
error: conversationalCmd
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Track assistant message for context
|
||||
* @param {string} message - Assistant's message
|
||||
*/
|
||||
function trackAssistantMessage(message) {
|
||||
conversationState.addAssistantMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track user message for context
|
||||
* @param {string} message - User's message
|
||||
*/
|
||||
function trackUserMessage(message) {
|
||||
conversationState.addUserMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current conversation state
|
||||
* @returns {string} - Current intent state
|
||||
*/
|
||||
function getIntentState() {
|
||||
return conversationState.intentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a semantic error to the bug tracker
|
||||
* @param {object} errorData - Error details
|
||||
*/
|
||||
function reportSemanticError(errorData) {
|
||||
const semanticError = {
|
||||
type: 'semantic',
|
||||
subtype: errorData.subtype || errorData.type,
|
||||
message: errorData.message,
|
||||
details: errorData.details || {},
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
context: {
|
||||
chatMode: window.currentChatMode || 'unknown',
|
||||
sessionId: window.attachedSessionId || window.chatSessionId,
|
||||
intentState: conversationState.intentState,
|
||||
recentMessages: conversationState.getLastNMessages(3).map(m => ({
|
||||
content: m.content?.substring(0, 50),
|
||||
timestamp: m.timestamp
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
// Report to bug tracker
|
||||
if (window.bugTracker) {
|
||||
const errorId = window.bugTracker.addError(semanticError);
|
||||
// Only add activity if error was actually added (not skipped)
|
||||
if (errorId) {
|
||||
window.bugTracker.addActivity(errorId, '🔍', `Semantic: ${errorData.subtype}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Report to server for auto-fixer
|
||||
fetch('/claude/api/log-error', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(semanticError)
|
||||
}).catch(err => console.error('[SemanticError] Failed to report:', err));
|
||||
|
||||
// Log for debugging
|
||||
console.log('[SemanticError] Detected:', semanticError);
|
||||
|
||||
return semanticError;
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.semanticValidator = {
|
||||
isConversational,
|
||||
isApprovalResponse,
|
||||
isContextualConversational,
|
||||
isShellCommand,
|
||||
extractCommand,
|
||||
detectApprovalIntentMismatch,
|
||||
detectConversationalCommand,
|
||||
detectConfusingOutput,
|
||||
validateIntentBeforeExecution,
|
||||
trackAssistantMessage,
|
||||
trackUserMessage,
|
||||
getIntentState,
|
||||
reportSemanticError
|
||||
};
|
||||
|
||||
console.log('[SemanticValidator] Initialized with enhanced error detection');
|
||||
})();
|
||||
Reference in New Issue
Block a user