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:
uroma
2026-01-21 13:14:43 +00:00
Unverified
parent efb3ecfb19
commit 153e365c7b
4 changed files with 1424 additions and 58 deletions

View File

@@ -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);