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:
@@ -530,6 +530,131 @@ async function sendChatMessage() {
|
||||
|
||||
if (!message) return;
|
||||
|
||||
// ============================================================
|
||||
// SEMANTIC VALIDATION - Detect intent/behavior mismatches
|
||||
// ============================================================
|
||||
if (window.semanticValidator) {
|
||||
// Track user message for context
|
||||
window.semanticValidator.trackUserMessage(message);
|
||||
|
||||
// Get the mode BEFORE any validation
|
||||
const selectedMode = currentChatMode || 'auto';
|
||||
|
||||
// IMPORTANT: In Terminal/WebContainer mode, check if this is a command request first
|
||||
// If user says "run ping google.com", we should EXECUTE it, not block it!
|
||||
if (selectedMode === 'webcontainer') {
|
||||
const extractedCommand = window.semanticValidator.extractCommand(message);
|
||||
|
||||
// If command was extracted from conversational language, allow it through
|
||||
if (extractedCommand !== message) {
|
||||
console.log('[sendChatMessage] Command request detected, allowing execution:', extractedCommand);
|
||||
// Don't return - let the command execute
|
||||
} else {
|
||||
// No extraction, run normal validation
|
||||
const validation = window.semanticValidator.validateIntentBeforeExecution(message, selectedMode);
|
||||
|
||||
if (!validation.valid && validation.error) {
|
||||
// Report semantic error to bug tracker
|
||||
window.semanticValidator.reportSemanticError(validation.error);
|
||||
|
||||
// Show user-friendly message based on error type
|
||||
if (validation.error.subtype === 'conversational_as_command') {
|
||||
appendSystemMessage(`💬 This looks like a conversational message, not a shell command.
|
||||
|
||||
You're currently in <strong>Terminal mode</strong> which executes shell commands.
|
||||
|
||||
<strong>Options:</strong>
|
||||
1. Switch to Chat mode (click "Auto" or "Native" button above)
|
||||
2. Rephrase as a shell command (e.g., <code>ls -la</code>, <code>npm install</code>)
|
||||
|
||||
<strong>Your message:</strong> "${escapeHtml(message.substring(0, 50))}${message.length > 50 ? '...' : ''}"`);
|
||||
|
||||
// Auto-switch to Chat mode after delay
|
||||
setTimeout(() => {
|
||||
if (currentChatMode === 'webcontainer') {
|
||||
setChatMode('auto');
|
||||
appendSystemMessage('✅ Switched to Chat mode. You can continue your conversation.');
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
clearInput();
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation.error.subtype === 'approval_loop') {
|
||||
appendSystemMessage(`⚠️ <strong>Intent Mismatch Detected</strong>
|
||||
|
||||
The AI assistant asked for your approval, but you responded in <strong>Terminal mode</strong> which executes commands.
|
||||
|
||||
<strong>What happened:</strong>
|
||||
• AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}"
|
||||
• You: "${escapeHtml(message)}"
|
||||
• System: Tried to execute "${escapeHtml(message)}" as a command
|
||||
|
||||
<strong>Suggested fix:</strong> Switch to Chat mode for conversational interactions.`);
|
||||
clearInput();
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In Chat/Auto mode, run normal validation
|
||||
const validation = window.semanticValidator.validateIntentBeforeExecution(message, selectedMode);
|
||||
|
||||
if (!validation.valid && validation.error) {
|
||||
// Report semantic error to bug tracker
|
||||
window.semanticValidator.reportSemanticError(validation.error);
|
||||
|
||||
// Show user-friendly message based on error type
|
||||
if (validation.error.subtype === 'conversational_as_command') {
|
||||
appendSystemMessage(`💬 This looks like a conversational message, not a shell command.
|
||||
|
||||
You're currently in <strong>Terminal mode</strong> which executes shell commands.
|
||||
|
||||
<strong>Options:</strong>
|
||||
1. Switch to Chat mode (click "Auto" or "Native" button above)
|
||||
2. Rephrase as a shell command (e.g., <code>ls -la</code>, <code>npm install</code>)
|
||||
|
||||
<strong>Your message:</strong> "${escapeHtml(message.substring(0, 50))}${message.length > 50 ? '...' : ''}"`);
|
||||
|
||||
// Auto-switch to Chat mode after delay
|
||||
setTimeout(() => {
|
||||
if (currentChatMode === 'webcontainer') {
|
||||
setChatMode('auto');
|
||||
appendSystemMessage('✅ Switched to Chat mode. You can continue your conversation.');
|
||||
}
|
||||
}, 4000);
|
||||
|
||||
clearInput();
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation.error.subtype === 'approval_loop') {
|
||||
appendSystemMessage(`⚠️ <strong>Intent Mismatch Detected</strong>
|
||||
|
||||
The AI assistant asked for your approval, but you responded in <strong>Terminal mode</strong> which executes commands.
|
||||
|
||||
<strong>What happened:</strong>
|
||||
• AI: "${validation.error.details.lastAssistantMessage || 'Asked for permission'}"
|
||||
• You: "${escapeHtml(message)}"
|
||||
• System: Tried to execute "${escapeHtml(message)}" as a command
|
||||
|
||||
<strong>Suggested fix:</strong> Switch to Chat mode for conversational interactions.`);
|
||||
clearInput();
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-create session if none exists (OpenCode/CodeNomad hybrid approach)
|
||||
if (!attachedSessionId) {
|
||||
console.log('[sendChatMessage] No session attached, auto-creating...');
|
||||
@@ -801,57 +926,62 @@ async function initializeWebContainer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Detect if message is a shell command
|
||||
// Detect if message is a shell command (using semantic validator)
|
||||
function isShellCommand(message) {
|
||||
if (window.semanticValidator) {
|
||||
return window.semanticValidator.isShellCommand(message);
|
||||
}
|
||||
|
||||
// Fallback to basic detection if validator not loaded
|
||||
console.warn('[SemanticValidator] Not loaded, using basic command detection');
|
||||
const trimmed = message.trim().toLowerCase();
|
||||
|
||||
// Check for common shell command patterns
|
||||
const commandPatterns = [
|
||||
// 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)(\s|$)/,
|
||||
// Package managers
|
||||
/^(npm|yarn|pnpm|pip|pip3|conda|brew|apt|apt-get|yum|dnf|pacman|curl|wget)(\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 paths
|
||||
/^[a-z0-9_\-./]+\s*[\|>]/,
|
||||
// Commands with arguments
|
||||
/^[a-z][a-z0-9_\-]*\s+/,
|
||||
// Absolute paths
|
||||
/^\//,
|
||||
// Scripts
|
||||
/^(sh|bash|zsh|fish|powershell|pwsh)(\s|$)/
|
||||
// Basic conversational check
|
||||
const conversationalPatterns = [
|
||||
/^(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|please|thank|hey|hello|hi)\s/i,
|
||||
/^(i|you|he|she|it|we|they)\s/i,
|
||||
/^(yes|no|maybe|ok|okay|sure|alright)\s/i
|
||||
];
|
||||
|
||||
return commandPatterns.some(pattern => pattern.test(trimmed));
|
||||
if (conversationalPatterns.some(pattern => pattern.test(trimmed))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic command patterns
|
||||
const basicPatterns = [
|
||||
/^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|npm|git|ping|curl|wget|node|python)(\s|$)/,
|
||||
/^\//,
|
||||
/^[a-z0-9_\-./]+\s*[\|>]/
|
||||
];
|
||||
|
||||
return basicPatterns.some(pattern => pattern.test(trimmed));
|
||||
}
|
||||
|
||||
// Send shell command to active Claude CLI session
|
||||
// Send shell command to active Claude CLI session via WebSocket
|
||||
async function sendShellCommand(sessionId, command) {
|
||||
try {
|
||||
const response = await fetch('/claude/api/shell-command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId, command })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
// Use WebSocket to send shell command (backend will execute through native mode)
|
||||
if (!window.ws || window.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket not connected');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Send command via WebSocket
|
||||
window.ws.send(JSON.stringify({
|
||||
type: 'command',
|
||||
sessionId: sessionId,
|
||||
command: command,
|
||||
metadata: {
|
||||
executionMode: 'native',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}));
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Command execution failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
// Return a promise that will be resolved when the command completes
|
||||
// Note: The actual output will come through WebSocket messages
|
||||
return {
|
||||
success: true,
|
||||
message: 'Command sent via WebSocket'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Shell Command] Error:', error);
|
||||
throw error;
|
||||
@@ -894,30 +1024,35 @@ Would you like to:
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract actual command if embedded in conversational language
|
||||
let actualCommand = message;
|
||||
if (window.semanticValidator && typeof window.semanticValidator.extractCommand === 'function') {
|
||||
const extracted = window.semanticValidator.extractCommand(message);
|
||||
if (extracted && extracted !== message) {
|
||||
actualCommand = extracted;
|
||||
appendSystemMessage(`🎯 Detected command request: "${escapeHtml(actualCommand)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Track command execution
|
||||
const commandId = window.commandTracker ?
|
||||
window.commandTracker.startCommand(message, currentChatMode, sessionId) :
|
||||
null;
|
||||
|
||||
try {
|
||||
appendSystemMessage(`💻 Executing in session: <code>${message}</code>`);
|
||||
appendSystemMessage(`💻 Executing in session: <code>${escapeHtml(actualCommand)}</code>`);
|
||||
|
||||
// Send shell command to the active Claude CLI session
|
||||
const result = await sendShellCommand(sessionId, message);
|
||||
// Send shell command to the active Claude CLI session via WebSocket
|
||||
// Note: Output will be received asynchronously via WebSocket messages
|
||||
await sendShellCommand(sessionId, actualCommand);
|
||||
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
|
||||
// Show command output
|
||||
if (result.stdout) {
|
||||
appendMessage('assistant', result.stdout);
|
||||
// Store command ID for later completion
|
||||
if (commandId) {
|
||||
window._pendingCommandId = commandId;
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
appendMessage('assistant', `<error>${result.stderr}</error>`);
|
||||
}
|
||||
|
||||
// Show completion status
|
||||
if (result.exitCode === 0) {
|
||||
appendSystemMessage(`✅ Command completed successfully`);
|
||||
} else {
|
||||
appendSystemMessage(`⚠️ Command exited with code ${result.exitCode}`);
|
||||
}
|
||||
// Don't hide indicators yet - wait for actual output via WebSocket
|
||||
// The output will be displayed through handleSessionOutput()
|
||||
|
||||
// Record tool usage
|
||||
if (typeof contextPanel !== 'undefined' && contextPanel) {
|
||||
@@ -926,6 +1061,12 @@ Would you like to:
|
||||
|
||||
} catch (error) {
|
||||
console.error('Shell command execution failed:', error);
|
||||
|
||||
// Mark command as failed in tracker
|
||||
if (commandId && window.commandTracker) {
|
||||
window.commandTracker.completeCommand(commandId, null, error.message);
|
||||
}
|
||||
|
||||
hideStreamingIndicator();
|
||||
setGeneratingState(false);
|
||||
appendSystemMessage('❌ Failed to execute command: ' + error.message);
|
||||
|
||||
Reference in New Issue
Block a user