mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-09 19:09:45 +00:00
feat: Multiple features, improvements, and bug fixes (#208)
* feat: Add token budget tracking and multiple improvements ## Features - **Token Budget Visualization**: Added real-time token usage tracking with pie chart display showing percentage used (blue < 50%, orange < 75%, red ≥ 75%) - **Show Thinking Toggle**: Added quick settings option to show/hide reasoning sections in messages - **Cache Clearing Utility**: Added `/clear-cache.html` page for clearing service workers, caches, and storage ## Improvements - **Package Upgrades**: Migrated from deprecated `xterm` to `@xterm/*` scoped packages - **Testing Setup**: Added Playwright for end-to-end testing - **Build Optimization**: Implemented code splitting for React, CodeMirror, and XTerm vendors to improve initial load time - **Deployment Scripts**: Added `scripts/start.sh` and `scripts/stop.sh` for cleaner server management with automatic port conflict resolution - **Vite Update**: Upgraded Vite from 7.0.5 to 7.1.8 ## Bug Fixes - Fixed static file serving to properly handle routes vs assets - Fixed session state reset to preserve token budget on initial load - Updated default Vite dev server port to 5173 (Vite's standard) ## Technical Details - Token budget is parsed from Claude CLI `modelUsage` field in result messages - Budget updates are sent via WebSocket as `token-budget` events - Calculation includes input, output, cache read, and cache creation tokens - Token budget state persists during active sessions but resets on session switch * feat: Add session processing state persistence Fixes issue where "Thinking..." banner and stop button disappear when switching between sessions. Users can now navigate freely while Claude is processing without losing the ability to monitor or stop the session. Features: - Processing state tracked in processingSessions Set (App.jsx) - Backend session status queries via check-session-status WebSocket message - UI state (banner + stop button) restored when returning to processing sessions - Works after page reload by querying backend's authoritative process maps - Proper cleanup when sessions complete in background Backend Changes: - Added sessionId to claude-complete, cursor-result, session-aborted messages - Exported isClaudeSessionActive, isCursorSessionActive helper functions - Exported getActiveClaudeSessions, getActiveCursorSessions for status queries - Added check-session-status and get-active-sessions WebSocket handlers Frontend Changes: - processingSessions state tracking in App.jsx - onSessionProcessing/onSessionNotProcessing callbacks - Session status check on session load and switch - Completion handlers only update UI if message is for current session - Always clean up processing state regardless of which session is active * feat: Make context window size configurable via environment variables Removes hardcoded 160k token limit and makes it configurable through environment variables. This allows easier adjustment for different Claude models or use cases. Changes: - Added CONTEXT_WINDOW env var for backend (default: 160000) - Added VITE_CONTEXT_WINDOW env var for frontend (default: 160000) - Updated .env.example with documentation - Replaced hardcoded values in token usage calculations - Replaced hardcoded values in pie chart display Why 160k? Claude Code reserves ~40k tokens for auto-compact feature, leaving 160k available for actual usage from the 200k context window. * fix: Decode HTML entities in chat message display HTML entities like < and > were showing as-is instead of being decoded to < and > characters. Added decodeHtmlEntities helper function to properly display angle brackets and other special characters. Applied to: - Regular message content - Streaming content deltas - Session history loading - Both string and array content types * refactor: Align package.json with main branch standards - Revert to main branch's package.json scripts structure - Remove custom scripts/start.sh and scripts/stop.sh - Update xterm dependencies to scoped @xterm packages (required for code compatibility) - Replace xterm with @xterm/xterm - Replace xterm-addon-fit with @xterm/addon-fit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Replace CLI implementation with Claude Agents SDK This commit completes the migration to the Claude Agents SDK, removing the legacy CLI-based implementation and making the SDK the exclusive integration method. Changes: - Remove claude-cli.js legacy implementation - Add claude-sdk.js with full SDK integration - Remove CLAUDE_USE_SDK feature flag (SDK is now always used) - Update server/index.js to use SDK functions directly - Add .serena/ to .gitignore for AI assistant cache Benefits: - Better performance (no child process overhead) - Native session management with interrupt support - Cleaner codebase without CLI/SDK branching - Full feature parity with previous CLI implementation - Maintains compatibility with Cursor integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update server/claude-sdk.js Whoops. This is correct. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update server/index.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/components/ChatInterface.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/components/ChatInterface.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/components/ChatInterface.jsx Left my test code in, but that's fixed. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Prevent stale token-usage data from updating state on session switch - Add AbortController to cancel in-flight token-usage requests when session/project changes - Capture session/project IDs before fetch and verify they match before updating state - Handle AbortError gracefully without logging as error - Prevents race condition where old session data overwrites current session's token budget 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/components/TokenUsagePie.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: viper151 <simosmik@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
134
server/index.js
134
server/index.js
@@ -28,18 +28,18 @@ console.log('PORT from env:', process.env.PORT);
|
||||
|
||||
import express from 'express';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import os from 'os';
|
||||
import http from 'http';
|
||||
import cors from 'cors';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import os from 'os';
|
||||
import pty from 'node-pty';
|
||||
import fetch from 'node-fetch';
|
||||
import mime from 'mime-types';
|
||||
|
||||
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
||||
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
|
||||
import { spawnCursor, abortCursorSession } from './cursor-cli.js';
|
||||
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
|
||||
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
||||
import gitRoutes from './routes/git.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import mcpRoutes from './routes/mcp.js';
|
||||
@@ -550,7 +550,9 @@ function handleChatConnection(ws) {
|
||||
console.log('💬 User message:', data.command || '[Continue/Resume]');
|
||||
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
|
||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||
await spawnClaude(data.command, data.options, ws);
|
||||
|
||||
// Use Claude Agents SDK
|
||||
await queryClaudeSDK(data.command, data.options, ws);
|
||||
} else if (data.type === 'cursor-command') {
|
||||
console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
|
||||
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
||||
@@ -568,9 +570,15 @@ function handleChatConnection(ws) {
|
||||
} else if (data.type === 'abort-session') {
|
||||
console.log('🛑 Abort session request:', data.sessionId);
|
||||
const provider = data.provider || 'claude';
|
||||
const success = provider === 'cursor'
|
||||
? abortCursorSession(data.sessionId)
|
||||
: abortClaudeSession(data.sessionId);
|
||||
let success;
|
||||
|
||||
if (provider === 'cursor') {
|
||||
success = abortCursorSession(data.sessionId);
|
||||
} else {
|
||||
// Use Claude Agents SDK
|
||||
success = await abortClaudeSDKSession(data.sessionId);
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-aborted',
|
||||
sessionId: data.sessionId,
|
||||
@@ -586,6 +594,35 @@ function handleChatConnection(ws) {
|
||||
provider: 'cursor',
|
||||
success
|
||||
}));
|
||||
} else if (data.type === 'check-session-status') {
|
||||
// Check if a specific session is currently processing
|
||||
const provider = data.provider || 'claude';
|
||||
const sessionId = data.sessionId;
|
||||
let isActive;
|
||||
|
||||
if (provider === 'cursor') {
|
||||
isActive = isCursorSessionActive(sessionId);
|
||||
} else {
|
||||
// Use Claude Agents SDK
|
||||
isActive = isClaudeSDKSessionActive(sessionId);
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-status',
|
||||
sessionId,
|
||||
provider,
|
||||
isProcessing: isActive
|
||||
}));
|
||||
} else if (data.type === 'get-active-sessions') {
|
||||
// Get all currently active sessions
|
||||
const activeSessions = {
|
||||
claude: getActiveClaudeSDKSessions(),
|
||||
cursor: getActiveCursorSessions()
|
||||
};
|
||||
ws.send(JSON.stringify({
|
||||
type: 'active-sessions',
|
||||
sessions: activeSessions
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Chat WebSocket error:', error.message);
|
||||
@@ -1053,13 +1090,87 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
||||
}
|
||||
});
|
||||
|
||||
// Serve React app for all other routes
|
||||
// API endpoint to get token usage for a session by reading its JSONL file
|
||||
app.get('/api/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { projectPath } = req.query;
|
||||
|
||||
if (!projectPath) {
|
||||
return res.status(400).json({ error: 'projectPath query parameter is required' });
|
||||
}
|
||||
|
||||
// Construct the JSONL file path
|
||||
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
||||
// The encoding replaces /, spaces, ~, and _ with -
|
||||
const homeDir = os.homedir();
|
||||
const encodedPath = projectPath.replace(/[\/\s~_]/g, '-');
|
||||
const jsonlPath = path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(jsonlPath)) {
|
||||
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
||||
}
|
||||
|
||||
// Read and parse the JSONL file
|
||||
const fileContent = fs.readFileSync(jsonlPath, 'utf8');
|
||||
const lines = fileContent.trim().split('\n');
|
||||
|
||||
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
||||
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
||||
let inputTokens = 0;
|
||||
let cacheCreationTokens = 0;
|
||||
let cacheReadTokens = 0;
|
||||
|
||||
// Find the latest assistant message with usage data (scan from end)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
|
||||
// Only count assistant messages which have usage data
|
||||
if (entry.type === 'assistant' && entry.message?.usage) {
|
||||
const usage = entry.message.usage;
|
||||
|
||||
// Use token counts from latest assistant message only
|
||||
inputTokens = usage.input_tokens || 0;
|
||||
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
||||
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
||||
|
||||
break; // Stop after finding the latest assistant message
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip lines that can't be parsed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total context usage (excluding output_tokens, as per ccusage)
|
||||
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
||||
|
||||
res.json({
|
||||
used: totalUsed,
|
||||
total: contextWindow,
|
||||
breakdown: {
|
||||
input: inputTokens,
|
||||
cacheCreation: cacheCreationTokens,
|
||||
cacheRead: cacheReadTokens
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading session token usage:', error);
|
||||
res.status(500).json({ error: 'Failed to read session token usage' });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve React app for all other routes (excluding static files)
|
||||
app.get('*', (req, res) => {
|
||||
// Only serve index.html for HTML routes, not for static assets
|
||||
// Static assets should already be handled by express.static middleware above
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
||||
} else {
|
||||
// In development, redirect to Vite dev server
|
||||
res.redirect(`http://localhost:${process.env.VITE_PORT || 3001}`);
|
||||
res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1153,11 +1264,14 @@ async function startServer() {
|
||||
await initializeDatabase();
|
||||
console.log('✅ Database initialization skipped (testing)');
|
||||
|
||||
// Log Claude implementation mode
|
||||
console.log('🚀 Using Claude Agents SDK for Claude integration');
|
||||
|
||||
server.listen(PORT, '0.0.0.0', async () => {
|
||||
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
|
||||
|
||||
// Start watching the projects folder for changes
|
||||
await setupProjectsWatcher();
|
||||
await setupProjectsWatcher();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
|
||||
Reference in New Issue
Block a user