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

@@ -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');
})();