- 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>
523 lines
19 KiB
JavaScript
523 lines
19 KiB
JavaScript
/**
|
|
* 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');
|
|
})();
|