Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion bin/cc-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ program
.option('--cert <path>', 'path to SSL certificate file')
.option('--key <path>', 'path to SSL private key file')
.option('--dev', 'development mode with additional logging')
.option('--cwd <path>', 'working directory for Claude sessions (default: current directory)')
.option('--no-usage', 'disable usage tracking (reduces memory usage)')
.option('--plan <type>', 'subscription plan (pro, max5, max20)', 'max20')
.option('--claude-alias <name>', 'display alias for Claude (default: env CLAUDE_ALIAS or "Claude")')
.option('--codex-alias <name>', 'display alias for Codex (default: env CODEX_ALIAS or "Codex")')
Expand Down Expand Up @@ -62,6 +64,22 @@ async function main() {
}
}

// Resolve and validate working directory if provided
let cwd = process.cwd();
if (options.cwd) {
const resolvedCwd = path.resolve(options.cwd);
const fs = require('fs');
if (!fs.existsSync(resolvedCwd)) {
console.error(`Error: Working directory does not exist: ${resolvedCwd}`);
process.exit(1);
}
if (!fs.statSync(resolvedCwd).isDirectory()) {
console.error(`Error: Path is not a directory: ${resolvedCwd}`);
process.exit(1);
}
cwd = resolvedCwd;
}

const serverOptions = {
port,
auth: authToken,
Expand All @@ -70,6 +88,8 @@ async function main() {
cert: options.cert,
key: options.key,
dev: options.dev,
cwd: cwd,
usageTracking: options.usage !== false, // --no-usage sets this to false
plan: options.plan,
// UI aliases for assistants
claudeAlias: options.claudeAlias || process.env.CLAUDE_ALIAS || 'Claude',
Expand All @@ -80,7 +100,7 @@ async function main() {

console.log('Starting Claude Code Web Interface...');
console.log(`Port: ${port}`);
console.log('Mode: Folder selection mode');
console.log(`Working directory: ${cwd}`);
console.log(`Plan: ${options.plan}`);
console.log(`Aliases: Claude → "${serverOptions.claudeAlias}", Codex → "${serverOptions.codexAlias}", Agent → "${serverOptions.agentAlias}"`);

Expand Down
68 changes: 59 additions & 9 deletions src/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class ClaudeCodeWebInterface {
this.sessionStats = null;
this.sessionTimer = null;
this.sessionTimerInterval = null;

this.usageTrackingEnabled = true; // Server-side usage tracking state

this.splitContainer = null;
this.init();
}
Expand Down Expand Up @@ -614,8 +615,11 @@ class ClaudeCodeWebInterface {
switch (message.type) {
case 'connected':
this.connectionId = message.connectionId;
// Store server-side usage tracking state
this.usageTrackingEnabled = message.usageTracking !== false;
this.updateUsageTrackingUI();
break;

case 'session_created':
this.currentClaudeSessionId = message.sessionId;
this.currentClaudeSessionName = message.sessionName;
Expand Down Expand Up @@ -816,16 +820,29 @@ class ClaudeCodeWebInterface {

case 'usage_update':
this.updateUsageDisplay(
message.sessionStats,
message.dailyStats,
message.sessionStats,
message.dailyStats,
message.sessionTimer,
message.analytics,
message.burnRate,
message.plan,
message.limits
);
break;


case 'usage_stats':
// Handle usage_stats response (sent when tracking is disabled)
if (message.disabled) {
this.usageTrackingEnabled = false;
this.updateUsageTrackingUI();
// Stop polling since it's disabled
if (this.usageUpdateTimer) {
clearInterval(this.usageUpdateTimer);
this.usageUpdateTimer = null;
}
}
break;

default:
console.log('Unknown message type:', message.type);
}
Expand Down Expand Up @@ -985,18 +1002,21 @@ class ClaudeCodeWebInterface {
showSettings() {
const modal = document.getElementById('settingsModal');
modal.classList.add('active');

// Prevent body scroll on mobile when modal is open
if (this.isMobile) {
document.body.style.overflow = 'hidden';
}

const settings = this.loadSettings();
document.getElementById('fontSize').value = settings.fontSize;
document.getElementById('fontSizeValue').textContent = settings.fontSize + 'px';
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) themeSelect.value = settings.theme === 'light' ? 'light' : 'dark';
document.getElementById('showTokenStats').checked = settings.showTokenStats;

// Update usage tracking UI state (hide checkbox if disabled server-side)
this.updateUsageTrackingUI();
}

hideSettings() {
Expand Down Expand Up @@ -1828,15 +1848,45 @@ class ClaudeCodeWebInterface {
}

requestUsageStats() {
// Skip if usage tracking is disabled server-side
if (!this.usageTrackingEnabled) {
return;
}

if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type: 'get_usage' }));
}

// Start periodic updates if not already running
if (!this.usageUpdateTimer) {
this.usageUpdateTimer = setInterval(() => {
this.requestUsageStats();
}, 10000); // Update every 10 seconds for more real-time stats
}, 60000); // Update every 60 seconds
}
}

updateUsageTrackingUI() {
// Update the "Show Token Stats" checkbox in settings based on server-side usage tracking state
const checkbox = document.getElementById('showTokenStats');
const settingGroup = checkbox?.closest('.setting-group');

if (!this.usageTrackingEnabled) {
// Disable and hide the setting when usage tracking is disabled server-side
if (checkbox) {
checkbox.disabled = true;
checkbox.checked = false;
}
if (settingGroup) {
settingGroup.style.display = 'none';
}
} else {
// Re-enable if usage tracking becomes available
if (checkbox) {
checkbox.disabled = false;
}
if (settingGroup) {
settingGroup.style.display = '';
}
}
}

Expand Down
25 changes: 16 additions & 9 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@ class ClaudeCodeWebServer {
this.keyFile = options.key;
this.folderMode = options.folderMode !== false; // Default to true
this.selectedWorkingDir = null;
this.baseFolder = process.cwd(); // The folder where the app runs from
this.baseFolder = options.cwd || process.cwd(); // The folder where the app runs from (or specified via --cwd)
this.usageTracking = options.usageTracking !== false; // Default to true, --no-usage disables
// Session duration in hours (default to 5 hours from first message)
this.sessionDurationHours = parseFloat(process.env.CLAUDE_SESSION_HOURS || options.sessionHours || 5);

this.app = express();
this.claudeSessions = new Map(); // Persistent sessions (claude, codex, or agent)
this.webSocketConnections = new Map(); // Maps WebSocket connection ID to session info
this.claudeBridge = new ClaudeBridge();
this.codexBridge = new CodexBridge();
this.agentBridge = new AgentBridge();
this.sessionStore = new SessionStore();
this.usageReader = new UsageReader(this.sessionDurationHours);
this.usageAnalytics = new UsageAnalytics({
// Only create usage tracking objects if enabled (they parse large JSONL files)
this.usageReader = this.usageTracking ? new UsageReader(this.sessionDurationHours) : null;
this.usageAnalytics = this.usageTracking ? new UsageAnalytics({
sessionDurationHours: this.sessionDurationHours,
plan: options.plan || process.env.CLAUDE_PLAN || 'max20',
customCostLimit: parseFloat(process.env.CLAUDE_COST_LIMIT || options.customCostLimit || 50.00)
});
}) : null;
this.autoSaveInterval = null;
this.startTime = Date.now(); // Track server start time
this.isShuttingDown = false; // Flag to prevent duplicate shutdown
Expand All @@ -51,7 +53,7 @@ class ClaudeCodeWebServer {
codex: options.codexAlias || process.env.CODEX_ALIAS || 'Codex',
agent: options.agentAlias || process.env.AGENT_ALIAS || 'Cursor'
};

this.setupExpress();
this.loadPersistedSessions();
this.setupAutoSave();
Expand Down Expand Up @@ -629,10 +631,11 @@ class ClaudeCodeWebServer {
this.cleanupWebSocketConnection(wsId);
});

// Send initial connection message
// Send initial connection message with server configuration
this.sendToWebSocket(ws, {
type: 'connected',
connectionId: wsId
connectionId: wsId,
usageTracking: this.usageTracking
});

// If sessionId provided, auto-join that session
Expand Down Expand Up @@ -745,7 +748,11 @@ class ClaudeCodeWebServer {
break;

case 'get_usage':
this.handleGetUsage(wsInfo);
if (this.usageTracking) {
this.handleGetUsage(wsInfo);
} else {
this.sendToWebSocket(wsInfo.ws, { type: 'usage_stats', disabled: true });
}
break;

default:
Expand Down