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:
427
public/claude-ide/command-tracker.js
Normal file
427
public/claude-ide/command-tracker.js
Normal file
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* Command Execution Tracker
|
||||
* Monitors command lifecycle, detects behavioral anomalies,
|
||||
* and identifies patterns in command execution failures.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================
|
||||
// COMMAND TRACKER CLASS
|
||||
// ============================================================
|
||||
|
||||
class CommandTracker {
|
||||
constructor() {
|
||||
this.pendingCommands = new Map();
|
||||
this.executionHistory = [];
|
||||
this.maxHistorySize = 100;
|
||||
this.anomalyThreshold = 3; // Report if 3+ similar issues in 5 minutes
|
||||
this.anomalyWindow = 5 * 60 * 1000; // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique command ID
|
||||
* @returns {string} - Command ID
|
||||
*/
|
||||
generateId() {
|
||||
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking a command execution
|
||||
* @param {string} message - Command message
|
||||
* @param {string} mode - Chat mode (webcontainer, auto, native)
|
||||
* @param {string} sessionId - Session ID
|
||||
* @returns {string} - Command ID
|
||||
*/
|
||||
startCommand(message, mode, sessionId) {
|
||||
const commandId = this.generateId();
|
||||
|
||||
const commandData = {
|
||||
id: commandId,
|
||||
message: message,
|
||||
extractedCommand: this.extractActualCommand(message),
|
||||
mode: mode,
|
||||
sessionId: sessionId,
|
||||
startTime: Date.now(),
|
||||
state: 'pending',
|
||||
metadata: this.getCommandMetadata(message)
|
||||
};
|
||||
|
||||
this.pendingCommands.set(commandId, commandData);
|
||||
|
||||
console.log('[CommandTracker] Started tracking:', {
|
||||
id: commandId,
|
||||
command: message.substring(0, 50)
|
||||
});
|
||||
|
||||
return commandId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract actual command (strip conversational wrappers)
|
||||
* @param {string} message - Original message
|
||||
* @returns {string} - Extracted command
|
||||
*/
|
||||
extractActualCommand(message) {
|
||||
if (window.semanticValidator && typeof window.semanticValidator.extractCommand === 'function') {
|
||||
const extracted = window.semanticValidator.extractCommand(message);
|
||||
return extracted || message;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command metadata for analysis
|
||||
* @param {string} message - Command message
|
||||
* @returns {object} - Metadata
|
||||
*/
|
||||
getCommandMetadata(message) {
|
||||
const metadata = {
|
||||
length: message.length,
|
||||
wordCount: message.split(/\s+/).length,
|
||||
hasSpecialChars: /[\|>]/.test(message),
|
||||
hasPath: /^\//.test(message) || /\//.test(message),
|
||||
isConversational: false,
|
||||
isCommandRequest: false
|
||||
};
|
||||
|
||||
if (window.semanticValidator) {
|
||||
metadata.isConversational = window.semanticValidator.isConversational(message);
|
||||
metadata.isCommandRequest = message.match(/\b(run|execute|exec|do)\s+/i) !== null;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete command execution
|
||||
* @param {string} commandId - Command ID
|
||||
* @param {number|null} exitCode - Exit code
|
||||
* @param {string} output - Command output
|
||||
*/
|
||||
completeCommand(commandId, exitCode, output) {
|
||||
const command = this.pendingCommands.get(commandId);
|
||||
if (!command) {
|
||||
console.warn('[CommandTracker] Unknown command ID:', commandId);
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = Date.now() - command.startTime;
|
||||
const success = exitCode === 0;
|
||||
|
||||
// Update command data
|
||||
command.endTime = Date.now();
|
||||
command.exitCode = exitCode;
|
||||
command.output = output ? output.substring(0, 500) : '';
|
||||
command.duration = duration;
|
||||
command.state = success ? 'completed' : 'failed';
|
||||
command.success = success;
|
||||
|
||||
// Add to history
|
||||
this.addToHistory(command);
|
||||
|
||||
// Run semantic checks
|
||||
this.checkForSemanticErrors(command);
|
||||
|
||||
// Remove from pending
|
||||
this.pendingCommands.delete(commandId);
|
||||
|
||||
console.log('[CommandTracker] Completed:', {
|
||||
id: commandId,
|
||||
success: success,
|
||||
duration: duration,
|
||||
exitCode: exitCode
|
||||
});
|
||||
|
||||
// Periodically check for anomalies
|
||||
if (this.executionHistory.length % 10 === 0) {
|
||||
this.detectBehavioralAnomalies();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add completed command to history
|
||||
* @param {object} command - Command data
|
||||
*/
|
||||
addToHistory(command) {
|
||||
this.executionHistory.push(command);
|
||||
|
||||
// Trim history if too large
|
||||
if (this.executionHistory.length > this.maxHistorySize) {
|
||||
this.executionHistory = this.executionHistory.slice(-this.maxHistorySize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for semantic errors in command execution
|
||||
* @param {object} command - Command data
|
||||
*/
|
||||
checkForSemanticErrors(command) {
|
||||
// Check 1: Undefined exit code (UX issue)
|
||||
if (command.exitCode === undefined || command.exitCode === null) {
|
||||
this.reportSemanticError({
|
||||
type: 'ux_issue',
|
||||
subtype: 'undefined_exit_code',
|
||||
message: 'Command completed with undefined exit code',
|
||||
details: {
|
||||
command: command.message,
|
||||
extractedCommand: command.extractedCommand,
|
||||
output: command.output,
|
||||
suggestedFix: 'Ensure exit code is always returned from command execution'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check 2: Conversational message that failed as command
|
||||
if (command.metadata.isConversational && command.exitCode !== 0) {
|
||||
this.reportSemanticError({
|
||||
type: 'intent_error',
|
||||
subtype: 'conversational_failed_as_command',
|
||||
message: 'Conversational message failed when executed as shell command',
|
||||
details: {
|
||||
command: command.message,
|
||||
mode: command.mode,
|
||||
exitCode: command.exitCode,
|
||||
error: this.extractError(command.output),
|
||||
suggestedFix: 'Improve conversational pattern detection to prevent this'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check 3: Command request pattern with no actual command extracted
|
||||
if (command.metadata.isCommandRequest && command.extractedCommand === command.message) {
|
||||
// Command was not extracted properly
|
||||
if (command.exitCode !== 0) {
|
||||
this.reportSemanticError({
|
||||
type: 'intent_error',
|
||||
subtype: 'command_extraction_failed',
|
||||
message: 'Command request pattern detected but extraction may have failed',
|
||||
details: {
|
||||
command: command.message,
|
||||
exitCode: command.exitCode,
|
||||
error: this.extractError(command.output)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check 4: Long-running command (> 30 seconds)
|
||||
if (command.duration > 30000) {
|
||||
console.warn('[CommandTracker] Long-running command:', {
|
||||
command: command.extractedCommand,
|
||||
duration: command.duration
|
||||
});
|
||||
|
||||
this.reportSemanticError({
|
||||
type: 'performance_issue',
|
||||
subtype: 'long_running_command',
|
||||
message: 'Command took longer than 30 seconds to execute',
|
||||
details: {
|
||||
command: command.extractedCommand,
|
||||
duration: command.duration,
|
||||
suggestedFix: 'Consider timeout or progress indicator'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check 5: Empty output for successful command
|
||||
if (command.success && (!command.output || command.output.trim() === '')) {
|
||||
this.reportSemanticError({
|
||||
type: 'ux_issue',
|
||||
subtype: 'empty_success_output',
|
||||
message: 'Command succeeded but produced no output',
|
||||
details: {
|
||||
command: command.extractedCommand,
|
||||
suggestedFix: 'Show "Command completed successfully" message for empty output'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from output
|
||||
* @param {string} output - Command output
|
||||
* @returns {string} - Error message
|
||||
*/
|
||||
extractError(output) {
|
||||
if (!output) return 'No output';
|
||||
|
||||
// Try to extract first line of error
|
||||
const lines = output.split('\n');
|
||||
const errorLine = lines.find(line =>
|
||||
line.toLowerCase().includes('error') ||
|
||||
line.toLowerCase().includes('failed') ||
|
||||
line.toLowerCase().includes('cannot')
|
||||
);
|
||||
|
||||
return errorLine ? errorLine.substring(0, 100) : output.substring(0, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect behavioral anomalies in command history
|
||||
*/
|
||||
detectBehavioralAnomalies() {
|
||||
const now = Date.now();
|
||||
const recentWindow = this.executionHistory.filter(
|
||||
cmd => now - cmd.endTime < this.anomalyWindow
|
||||
);
|
||||
|
||||
// Anomaly 1: Multiple conversational messages failing as commands
|
||||
const conversationalFailures = recentWindow.filter(cmd =>
|
||||
cmd.metadata.isConversational && cmd.exitCode !== 0
|
||||
);
|
||||
|
||||
if (conversationalFailures.length >= this.anomalyThreshold) {
|
||||
this.reportSemanticError({
|
||||
type: 'behavioral_anomaly',
|
||||
subtype: 'repeated_conversational_failures',
|
||||
message: `Pattern detected: ${conversationalFailures.length} conversational messages failed as commands in last 5 minutes`,
|
||||
details: {
|
||||
count: conversationalFailures.length,
|
||||
examples: conversationalFailures.slice(0, 3).map(f => f.message),
|
||||
suggestedFix: 'Improve conversational detection or add user education'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Anomaly 2: High failure rate for specific command patterns
|
||||
const commandFailures = new Map();
|
||||
recentWindow.forEach(cmd => {
|
||||
if (!cmd.success) {
|
||||
const key = cmd.extractedCommand.split(' ')[0]; // First word (command name)
|
||||
commandFailures.set(key, (commandFailures.get(key) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
commandFailures.forEach((count, commandName) => {
|
||||
if (count >= this.anomalyThreshold) {
|
||||
this.reportSemanticError({
|
||||
type: 'behavioral_anomaly',
|
||||
subtype: 'high_failure_rate',
|
||||
message: `Command "${commandName}" failed ${count} times in last 5 minutes`,
|
||||
details: {
|
||||
command: commandName,
|
||||
failureCount: count,
|
||||
suggestedFix: 'Check if command exists or is being used correctly'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Anomaly 3: Commands with undefined exit codes
|
||||
const undefinedExitCodes = recentWindow.filter(cmd =>
|
||||
cmd.exitCode === undefined || cmd.exitCode === null
|
||||
);
|
||||
|
||||
if (undefinedExitCodes.length >= 5) {
|
||||
this.reportSemanticError({
|
||||
type: 'behavioral_anomaly',
|
||||
subtype: 'undefined_exit_code_pattern',
|
||||
message: `${undefinedExitCodes.length} commands completed with undefined exit code`,
|
||||
details: {
|
||||
count: undefinedExitCodes.length,
|
||||
affectedCommands: undefinedExitCodes.slice(0, 5).map(c => c.extractedCommand),
|
||||
suggestedFix: 'Fix command execution to always return exit code'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a semantic error
|
||||
* @param {object} errorData - Error details
|
||||
*/
|
||||
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,
|
||||
pendingCommands: this.pendingCommands.size,
|
||||
recentHistory: this.executionHistory.slice(-5).map(cmd => ({
|
||||
command: cmd.extractedCommand.substring(0, 30),
|
||||
success: cmd.success,
|
||||
timestamp: cmd.endTime
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
// 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, '📊', `Pattern: ${errorData.subtype}`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Report to server
|
||||
fetch('/claude/api/log-error', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(semanticError)
|
||||
}).catch(err => console.error('[CommandTracker] Failed to report error:', err));
|
||||
|
||||
console.log('[CommandTracker] Semantic error reported:', semanticError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about command execution
|
||||
* @returns {object} - Statistics
|
||||
*/
|
||||
getStatistics() {
|
||||
const total = this.executionHistory.length;
|
||||
const successful = this.executionHistory.filter(cmd => cmd.success).length;
|
||||
const failed = total - successful;
|
||||
const avgDuration = total > 0
|
||||
? this.executionHistory.reduce((sum, cmd) => sum + (cmd.duration || 0), 0) / total
|
||||
: 0;
|
||||
|
||||
return {
|
||||
total,
|
||||
successful,
|
||||
failed,
|
||||
successRate: total > 0 ? (successful / total * 100).toFixed(1) : 0,
|
||||
avgDuration: Math.round(avgDuration),
|
||||
pending: this.pendingCommands.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old history (older than 1 hour)
|
||||
*/
|
||||
clearOldHistory() {
|
||||
const oneHourAgo = Date.now() - (60 * 60 * 1000);
|
||||
this.executionHistory = this.executionHistory.filter(
|
||||
cmd => cmd.endTime > oneHourAgo
|
||||
);
|
||||
|
||||
console.log('[CommandTracker] Cleared old history, remaining:', this.executionHistory.length);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GLOBAL INSTANCE
|
||||
// ============================================================
|
||||
|
||||
// Create global instance
|
||||
window.commandTracker = new CommandTracker();
|
||||
|
||||
// Auto-clean old history every 10 minutes
|
||||
setInterval(() => {
|
||||
window.commandTracker.clearOldHistory();
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
// Expose statistics to console for debugging
|
||||
window.getCommandStats = () => window.commandTracker.getStatistics();
|
||||
|
||||
console.log('[CommandTracker] Command lifecycle tracking initialized');
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user