mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-12 00:21:30 +00:00
Compare commits
2 Commits
refactor/r
...
fix/replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bbd56e8e9 | ||
|
|
11733918e5 |
@@ -1,4 +1,4 @@
|
|||||||
# CloudCLI — Docker Sandbox Templates
|
# Claude Code UI — Docker Sandbox Templates
|
||||||
|
|
||||||
Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/).
|
Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/).
|
||||||
|
|
||||||
@@ -62,11 +62,11 @@ docker build -f docker/gemini/Dockerfile -t cloudcli-sandbox:gemini docker/
|
|||||||
|
|
||||||
Each template extends Docker's official sandbox base image and adds:
|
Each template extends Docker's official sandbox base image and adds:
|
||||||
|
|
||||||
1. **Node.js 22** — Runtime for CloudCLI
|
1. **Node.js 22** — Runtime for Claude Code UI
|
||||||
2. **CloudCLI** — Installed globally via `npm install -g @cloudcli-ai/cloudcli`
|
2. **Claude Code UI** — Installed globally via `npm install -g @cloudcli-ai/cloudcli`
|
||||||
3. **Auto-start** — The UI server starts in the background when the sandbox shell opens (port 3001)
|
3. **Auto-start** — The UI server starts in the background when the sandbox shell opens (port 3001)
|
||||||
|
|
||||||
The agent (Claude Code, Codex, or Gemini) comes from the base image. CloudCLI connects to it and provides the web interface on top.
|
The agent (Claude Code, Codex, or Gemini) comes from the base image. Claude Code UI connects to it and provides the web interface on top.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -86,4 +86,4 @@ sbx policy allow network "localhost:3001"
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
These templates are free and open-source under the same license as CloudCLI (AGPL-3.0-or-later).
|
These templates are free and open-source under the same license as Claude Code UI (AGPL-3.0-or-later).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Auto-start CloudCLI server in background if not already running.
|
# Auto-start Claude Code UI server in background if not already running.
|
||||||
# This script is sourced from ~/.bashrc on sandbox shell open.
|
# This script is sourced from ~/.bashrc on sandbox shell open.
|
||||||
|
|
||||||
if ! pgrep -f "server/index.js" > /dev/null 2>&1; then
|
if ! pgrep -f "server/index.js" > /dev/null 2>&1; then
|
||||||
@@ -13,7 +13,7 @@ if ! pgrep -f "server/index.js" > /dev/null 2>&1; then
|
|||||||
disown
|
disown
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " CloudCLI is starting on port 3001..."
|
echo " Claude Code UI is starting on port 3001..."
|
||||||
echo ""
|
echo ""
|
||||||
echo " To access the web UI, forward the port:"
|
echo " To access the web UI, forward the port:"
|
||||||
echo " sbx ports \$(hostname) --publish 3001:3001"
|
echo " sbx ports \$(hostname) --publish 3001:3001"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CloudCLI - API Documentation</title>
|
<title>Claude Code UI - API Documentation</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<h1>CloudCLI</h1>
|
<h1>Claude Code UI</h1>
|
||||||
<div class="subtitle">API Documentation</div>
|
<div class="subtitle">API Documentation</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,7 +585,7 @@
|
|||||||
<p>Server-sent events (SSE) format with real-time updates. Content-Type: <code>text/event-stream</code></p>
|
<p>Server-sent events (SSE) format with real-time updates. Content-Type: <code>text/event-stream</code></p>
|
||||||
|
|
||||||
<h4>Response (Non-Streaming)</h4>
|
<h4>Response (Non-Streaming)</h4>
|
||||||
<p>JSON object containing session details and assistant messages only (filtered). Content-Type: <code>application/json</code></p>
|
<p>JSON object containing session details, assistant messages only (filtered), and token usage summary. Content-Type: <code>application/json</code></p>
|
||||||
|
|
||||||
<h4>Error Response</h4>
|
<h4>Error Response</h4>
|
||||||
<p>Returns error details with appropriate HTTP status code.</p>
|
<p>Returns error details with appropriate HTTP status code.</p>
|
||||||
@@ -674,10 +674,21 @@ data: {"type":"done"}</code></pre>
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "I've completed the task..."
|
"text": "I've completed the task..."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 150,
|
||||||
|
"output_tokens": 50
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"tokens": {
|
||||||
|
"inputTokens": 150,
|
||||||
|
"outputTokens": 50,
|
||||||
|
"cacheReadTokens": 0,
|
||||||
|
"cacheCreationTokens": 0,
|
||||||
|
"totalTokens": 200
|
||||||
|
},
|
||||||
"projectPath": "/path/to/project",
|
"projectPath": "/path/to/project",
|
||||||
"branch": {
|
"branch": {
|
||||||
"name": "fix-authentication-bug-abc123",
|
"name": "fix-authentication-bug-abc123",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Service Worker for CloudCLI PWA
|
// Service Worker for Claude Code UI PWA
|
||||||
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
|
// Cache only manifest (needed for PWA install). HTML and JS are never pre-cached
|
||||||
// so a rebuild + refresh always picks up the latest assets.
|
// so a rebuild + refresh always picks up the latest assets.
|
||||||
const CACHE_NAME = 'claude-ui-v2';
|
const CACHE_NAME = 'claude-ui-v2';
|
||||||
@@ -79,7 +79,7 @@ self.addEventListener('push', event => {
|
|||||||
try {
|
try {
|
||||||
payload = event.data.json();
|
payload = event.data.json();
|
||||||
} catch {
|
} catch {
|
||||||
payload = { title: 'CloudCLI', body: event.data.text() };
|
payload = { title: 'Claude Code UI', body: event.data.text() };
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -92,7 +92,7 @@ self.addEventListener('push', event => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.registration.showNotification(payload.title || 'CloudCLI', options)
|
self.registration.showNotification(payload.title || 'Claude Code UI', options)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,46 @@ function transformMessage(sdkMessage) {
|
|||||||
return sdkMessage;
|
return sdkMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts token usage from SDK result messages
|
||||||
|
* @param {Object} resultMessage - SDK result message
|
||||||
|
* @returns {Object|null} Token budget object or null
|
||||||
|
*/
|
||||||
|
function extractTokenBudget(resultMessage) {
|
||||||
|
if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first model's usage data
|
||||||
|
const modelKey = Object.keys(resultMessage.modelUsage)[0];
|
||||||
|
const modelData = resultMessage.modelUsage[modelKey];
|
||||||
|
|
||||||
|
if (!modelData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cumulative tokens if available (tracks total for the session)
|
||||||
|
// Otherwise fall back to per-request tokens
|
||||||
|
const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
|
||||||
|
const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
|
||||||
|
const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
|
||||||
|
const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
|
||||||
|
|
||||||
|
// Total used = input + output + cache tokens
|
||||||
|
const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
|
||||||
|
|
||||||
|
// Use configured context window budget from environment (default 160000)
|
||||||
|
// This is the user's budget limit, not the model's context window
|
||||||
|
const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
|
||||||
|
|
||||||
|
// Token calc logged via token-budget WS event
|
||||||
|
|
||||||
|
return {
|
||||||
|
used: totalUsed,
|
||||||
|
total: contextWindow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles image processing for SDK queries
|
* Handles image processing for SDK queries
|
||||||
* Saves base64 images to temporary files and returns modified prompt with file paths
|
* Saves base64 images to temporary files and returns modified prompt with file paths
|
||||||
@@ -617,6 +657,18 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
}
|
}
|
||||||
ws.send(msg);
|
ws.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract and send token budget updates from result messages
|
||||||
|
if (message.type === 'result') {
|
||||||
|
const models = Object.keys(message.modelUsage || {});
|
||||||
|
if (models.length > 0) {
|
||||||
|
// Model info available in result message
|
||||||
|
}
|
||||||
|
const tokenBudgetData = extractTokenBudget(message);
|
||||||
|
if (tokenBudgetData) {
|
||||||
|
ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up session on completion
|
// Clean up session on completion
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* CloudCLI CLI
|
* Claude Code UI CLI
|
||||||
*
|
*
|
||||||
* Provides command-line utilities for managing CloudCLI
|
* Provides command-line utilities for managing Claude Code UI
|
||||||
*
|
*
|
||||||
* Commands:
|
* Commands:
|
||||||
* (no args) - Start the server (default)
|
* (no args) - Start the server (default)
|
||||||
@@ -84,7 +84,7 @@ function getInstallDir() {
|
|||||||
|
|
||||||
// Show status command
|
// Show status command
|
||||||
function showStatus() {
|
function showStatus() {
|
||||||
console.log(`\n${c.bright('CloudCLI UI - Status')}\n`);
|
console.log(`\n${c.bright('Claude Code UI - Status')}\n`);
|
||||||
console.log(c.dim('═'.repeat(60)));
|
console.log(c.dim('═'.repeat(60)));
|
||||||
|
|
||||||
// Version info
|
// Version info
|
||||||
@@ -141,7 +141,7 @@ function showStatus() {
|
|||||||
function showHelp() {
|
function showHelp() {
|
||||||
console.log(`
|
console.log(`
|
||||||
╔═══════════════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════════════╗
|
||||||
║ CloudCLI - Command Line Tool ║
|
║ Claude Code UI - Command Line Tool ║
|
||||||
╚═══════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -149,7 +149,7 @@ Usage:
|
|||||||
cloudcli [command] [options]
|
cloudcli [command] [options]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
start Start the CloudCLI server (default)
|
start Start the Claude Code UI server (default)
|
||||||
status Show configuration and data locations
|
status Show configuration and data locations
|
||||||
update Update to the latest version
|
update Update to the latest version
|
||||||
help Show this help information
|
help Show this help information
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import crossSpawn from 'cross-spawn';
|
|
||||||
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
||||||
import { cursorAdapter } from './providers/cursor/adapter.js';
|
import { cursorAdapter } from './providers/cursor/adapter.js';
|
||||||
import { createNormalizedMessage } from './providers/types.js';
|
import { createNormalizedMessage } from './providers/types.js';
|
||||||
|
|
||||||
// Use cross-spawn on Windows for better command execution
|
|
||||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
||||||
|
|
||||||
let activeCursorProcesses = new Map(); // Track active processes by session ID
|
let activeCursorProcesses = new Map(); // Track active processes by session ID
|
||||||
|
|
||||||
const WORKSPACE_TRUST_PATTERNS = [
|
const WORKSPACE_TRUST_PATTERNS = [
|
||||||
@@ -122,7 +118,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
console.log('Working directory:', workingDir);
|
console.log('Working directory:', workingDir);
|
||||||
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
||||||
|
|
||||||
const cursorProcess = spawnFunction('cursor-agent', args, {
|
const cursorProcess = spawn('cursor-agent', args, {
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
env: { ...process.env } // Inherit all environment variables
|
env: { ...process.env } // Inherit all environment variables
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import crossSpawn from 'cross-spawn';
|
|
||||||
|
|
||||||
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
||||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
@@ -168,7 +164,7 @@ async function spawnGemini(command, options = {}, ws) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
|
const geminiProcess = spawn(spawnCmd, spawnArgs, {
|
||||||
cwd: workingDir,
|
cwd: workingDir,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
env: { ...process.env } // Inherit all environment variables
|
env: { ...process.env } // Inherit all environment variables
|
||||||
|
|||||||
192
server/index.js
192
server/index.js
@@ -39,7 +39,7 @@ import os from 'os';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import pty from 'node-pty';
|
import pty from 'node-pty';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
@@ -2218,6 +2218,194 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get token usage for a specific session
|
||||||
|
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { projectName, sessionId } = req.params;
|
||||||
|
const { provider = 'claude' } = req.query;
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
|
||||||
|
// Allow only safe characters in sessionId
|
||||||
|
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
||||||
|
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid sessionId' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
||||||
|
if (provider === 'cursor') {
|
||||||
|
return res.json({
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
||||||
|
unsupported: true,
|
||||||
|
message: 'Token usage tracking not available for Cursor sessions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Gemini sessions - they are raw logs in our current setup
|
||||||
|
if (provider === 'gemini') {
|
||||||
|
return res.json({
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
||||||
|
unsupported: true,
|
||||||
|
message: 'Token usage tracking not available for Gemini sessions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Codex sessions
|
||||||
|
if (provider === 'codex') {
|
||||||
|
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
||||||
|
|
||||||
|
// Find the session file by searching for the session ID
|
||||||
|
const findSessionFile = async (dir) => {
|
||||||
|
try {
|
||||||
|
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const found = await findSessionFile(fullPath);
|
||||||
|
if (found) return found;
|
||||||
|
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Skip directories we can't read
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
||||||
|
|
||||||
|
if (!sessionFilePath) {
|
||||||
|
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse the Codex JSONL file
|
||||||
|
let fileContent;
|
||||||
|
try {
|
||||||
|
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const lines = fileContent.trim().split('\n');
|
||||||
|
let totalTokens = 0;
|
||||||
|
let contextWindow = 200000; // Default for Codex/OpenAI
|
||||||
|
|
||||||
|
// Find the latest token_count event with info (scan from end)
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
const entry = JSON.parse(lines[i]);
|
||||||
|
|
||||||
|
// Codex stores token info in event_msg with type: "token_count"
|
||||||
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
||||||
|
const tokenInfo = entry.payload.info;
|
||||||
|
if (tokenInfo.total_token_usage) {
|
||||||
|
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
||||||
|
}
|
||||||
|
if (tokenInfo.model_context_window) {
|
||||||
|
contextWindow = tokenInfo.model_context_window;
|
||||||
|
}
|
||||||
|
break; // Stop after finding the latest token count
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
// Skip lines that can't be parsed
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
used: totalTokens,
|
||||||
|
total: contextWindow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Claude sessions (default)
|
||||||
|
// Extract actual project path
|
||||||
|
let projectPath;
|
||||||
|
try {
|
||||||
|
projectPath = await extractProjectDirectory(projectName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting project directory:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to determine project path' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the JSONL file path
|
||||||
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
||||||
|
// The encoding replaces any non-alphanumeric character (except -) with -
|
||||||
|
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||||
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
||||||
|
|
||||||
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
||||||
|
|
||||||
|
// Constrain to projectDir
|
||||||
|
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
||||||
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid path' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse the JSONL file
|
||||||
|
let fileContent;
|
||||||
|
try {
|
||||||
|
fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
||||||
|
}
|
||||||
|
throw error; // Re-throw other errors to be caught by outer try-catch
|
||||||
|
}
|
||||||
|
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)
|
// Serve React app for all other routes (excluding static files)
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
// Skip requests for static assets (files with extensions)
|
// Skip requests for static assets (files with extensions)
|
||||||
@@ -2360,7 +2548,7 @@ async function startServer() {
|
|||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(c.dim('═'.repeat(63)));
|
console.log(c.dim('═'.repeat(63)));
|
||||||
console.log(` ${c.bright('CloudCLI Server - Ready')}`);
|
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
|
||||||
console.log(c.dim('═'.repeat(63)));
|
console.log(c.dim('═'.repeat(63)));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
|
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + DISPLAY_HOST + ':' + SERVER_PORT)}`);
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ function transformCodexEvent(event) {
|
|||||||
|
|
||||||
case 'turn.completed':
|
case 'turn.completed':
|
||||||
return {
|
return {
|
||||||
type: 'turn_complete'
|
type: 'turn_complete',
|
||||||
|
usage: event.usage
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'turn.failed':
|
case 'turn.failed':
|
||||||
@@ -278,6 +279,12 @@ export async function queryCodex(command, options = {}, ws) {
|
|||||||
error: terminalFailure
|
error: terminalFailure
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract and send token usage if available (normalized to match Claude format)
|
||||||
|
if (event.type === 'turn.completed' && event.usage) {
|
||||||
|
const totalTokens = (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0);
|
||||||
|
sendMessage(ws, createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: { used: totalTokens, total: 200000 }, sessionId: currentSessionId, provider: 'codex' }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send completion event
|
// Send completion event
|
||||||
|
|||||||
@@ -1618,6 +1618,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
let tokenUsage = null;
|
||||||
const fileStream = fsSync.createReadStream(sessionFilePath);
|
const fileStream = fsSync.createReadStream(sessionFilePath);
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: fileStream,
|
input: fileStream,
|
||||||
@@ -1646,6 +1647,17 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
try {
|
try {
|
||||||
const entry = JSON.parse(line);
|
const entry = JSON.parse(line);
|
||||||
|
|
||||||
|
// Extract token usage from token_count events (keep latest)
|
||||||
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
||||||
|
const info = entry.payload.info;
|
||||||
|
if (info.total_token_usage) {
|
||||||
|
tokenUsage = {
|
||||||
|
used: info.total_token_usage.total_tokens || 0,
|
||||||
|
total: info.model_context_window || 200000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use event_msg.user_message for user-visible inputs.
|
// Use event_msg.user_message for user-visible inputs.
|
||||||
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
||||||
messages.push({
|
messages.push({
|
||||||
@@ -1808,10 +1820,11 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|||||||
hasMore,
|
hasMore,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
|
tokenUsage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { messages };
|
return { messages, tokenUsage };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export const codexAdapter = {
|
|||||||
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
||||||
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
||||||
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
||||||
|
const tokenUsage = result.tokenUsage || null;
|
||||||
|
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
for (const raw of rawMessages) {
|
for (const raw of rawMessages) {
|
||||||
@@ -241,6 +242,7 @@ export const codexAdapter = {
|
|||||||
hasMore,
|
hasMore,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
|
tokenUsage,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,14 @@ export function normalizeMessage(raw, sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (raw.type === 'result') {
|
if (raw.type === 'result') {
|
||||||
return [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
|
const msgs = [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
|
||||||
|
if (raw.stats?.total_tokens) {
|
||||||
|
msgs.push(createNormalizedMessage({
|
||||||
|
sessionId, timestamp: ts, provider: PROVIDER,
|
||||||
|
kind: 'status', text: 'Complete', tokens: raw.stats.total_tokens, canInterrupt: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw.type === 'error') {
|
if (raw.type === 'error') {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
* - stream_end: (no extra fields)
|
* - stream_end: (no extra fields)
|
||||||
* - error: content
|
* - error: content
|
||||||
* - complete: (no extra fields)
|
* - complete: (no extra fields)
|
||||||
* - status: text, canInterrupt?
|
* - status: text, tokens?, canInterrupt?
|
||||||
* - permission_request: requestId, toolName, input, context?
|
* - permission_request: requestId, toolName, input, context?
|
||||||
* - permission_cancelled: requestId
|
* - permission_cancelled: requestId
|
||||||
* - session_created: newSessionId
|
* - session_created: newSessionId
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
* @property {boolean} hasMore - Whether more messages exist before the current page
|
* @property {boolean} hasMore - Whether more messages exist before the current page
|
||||||
* @property {number} offset - Current offset
|
* @property {number} offset - Current offset
|
||||||
* @property {number|null} limit - Page size used
|
* @property {number|null} limit - Page size used
|
||||||
|
* @property {object} [tokenUsage] - Token usage data (provider-specific)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ─── Provider Adapter Interface ──────────────────────────────────────────────
|
// ─── Provider Adapter Interface ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
@@ -546,12 +546,7 @@ class ResponseCollector {
|
|||||||
const parsed = JSON.parse(msg);
|
const parsed = JSON.parse(msg);
|
||||||
// Only include claude-response messages with assistant type
|
// Only include claude-response messages with assistant type
|
||||||
if (parsed.type === 'claude-response' && parsed.data && parsed.data.type === 'assistant') {
|
if (parsed.type === 'claude-response' && parsed.data && parsed.data.type === 'assistant') {
|
||||||
const assistantMessage = { ...parsed.data };
|
assistantMessages.push(parsed.data);
|
||||||
if (assistantMessage.message?.usage) {
|
|
||||||
assistantMessage.message = { ...assistantMessage.message };
|
|
||||||
delete assistantMessage.message.usage;
|
|
||||||
}
|
|
||||||
assistantMessages.push(assistantMessage);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not JSON, skip
|
// Not JSON, skip
|
||||||
@@ -561,6 +556,49 @@ class ResponseCollector {
|
|||||||
|
|
||||||
return assistantMessages;
|
return assistantMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total tokens from all messages
|
||||||
|
*/
|
||||||
|
getTotalTokens() {
|
||||||
|
let totalInput = 0;
|
||||||
|
let totalOutput = 0;
|
||||||
|
let totalCacheRead = 0;
|
||||||
|
let totalCacheCreation = 0;
|
||||||
|
|
||||||
|
for (const msg of this.messages) {
|
||||||
|
let data = msg;
|
||||||
|
|
||||||
|
// Parse if string
|
||||||
|
if (typeof msg === 'string') {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(msg);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract usage from claude-response messages
|
||||||
|
if (data && data.type === 'claude-response' && data.data) {
|
||||||
|
const msgData = data.data;
|
||||||
|
if (msgData.message && msgData.message.usage) {
|
||||||
|
const usage = msgData.message.usage;
|
||||||
|
totalInput += usage.input_tokens || 0;
|
||||||
|
totalOutput += usage.output_tokens || 0;
|
||||||
|
totalCacheRead += usage.cache_read_input_tokens || 0;
|
||||||
|
totalCacheCreation += usage.cache_creation_input_tokens || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputTokens: totalInput,
|
||||||
|
outputTokens: totalOutput,
|
||||||
|
cacheReadTokens: totalCacheRead,
|
||||||
|
cacheCreationTokens: totalCacheCreation,
|
||||||
|
totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -751,6 +789,13 @@ class ResponseCollector {
|
|||||||
* success: true,
|
* success: true,
|
||||||
* sessionId: "session-123",
|
* sessionId: "session-123",
|
||||||
* messages: [...], // Assistant messages only (filtered)
|
* messages: [...], // Assistant messages only (filtered)
|
||||||
|
* tokens: {
|
||||||
|
* inputTokens: 150,
|
||||||
|
* outputTokens: 50,
|
||||||
|
* cacheReadTokens: 0,
|
||||||
|
* cacheCreationTokens: 0,
|
||||||
|
* totalTokens: 200
|
||||||
|
* },
|
||||||
* projectPath: "/path/to/project",
|
* projectPath: "/path/to/project",
|
||||||
* branch: { // Only if createBranch=true
|
* branch: { // Only if createBranch=true
|
||||||
* name: "feature/xyz",
|
* name: "feature/xyz",
|
||||||
@@ -1080,7 +1125,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
prBody += `Agent task: ${message}`;
|
prBody += `Agent task: ${message}`;
|
||||||
}
|
}
|
||||||
prBody += '\n\n---\n*This pull request was automatically created by CloudCLI.ai Agent.*';
|
prBody += '\n\n---\n*This pull request was automatically created by Claude Code UI Agent.*';
|
||||||
|
|
||||||
console.log(`📝 PR Title: ${prTitle}`);
|
console.log(`📝 PR Title: ${prTitle}`);
|
||||||
|
|
||||||
@@ -1128,13 +1173,15 @@ router.post('/', validateExternalApiKey, async (req, res) => {
|
|||||||
// Streaming mode: end the SSE stream
|
// Streaming mode: end the SSE stream
|
||||||
writer.end();
|
writer.end();
|
||||||
} else {
|
} else {
|
||||||
// Non-streaming mode: send filtered messages as JSON
|
// Non-streaming mode: send filtered messages and token summary as JSON
|
||||||
const assistantMessages = writer.getAssistantMessages();
|
const assistantMessages = writer.getAssistantMessages();
|
||||||
|
const tokenSummary = writer.getTotalTokens();
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
success: true,
|
success: true,
|
||||||
sessionId: writer.getSessionId(),
|
sessionId: writer.getSessionId(),
|
||||||
messages: assistantMessages,
|
messages: assistantMessages,
|
||||||
|
tokens: tokenSummary,
|
||||||
projectPath: finalProjectPath
|
projectPath: finalProjectPath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ const builtInCommands = [
|
|||||||
namespace: 'builtin',
|
namespace: 'builtin',
|
||||||
metadata: { type: 'builtin' }
|
metadata: { type: 'builtin' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '/cost',
|
||||||
|
description: 'Display token usage and cost information',
|
||||||
|
namespace: 'builtin',
|
||||||
|
metadata: { type: 'builtin' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '/memory',
|
name: '/memory',
|
||||||
description: 'Open CLAUDE.md memory file for editing',
|
description: 'Open CLAUDE.md memory file for editing',
|
||||||
@@ -203,6 +209,86 @@ Custom commands can be created in:
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'/cost': async (args, context) => {
|
||||||
|
const tokenUsage = context?.tokenUsage || {};
|
||||||
|
const provider = context?.provider || 'claude';
|
||||||
|
const model =
|
||||||
|
context?.model ||
|
||||||
|
(provider === 'cursor'
|
||||||
|
? CURSOR_MODELS.DEFAULT
|
||||||
|
: provider === 'codex'
|
||||||
|
? CODEX_MODELS.DEFAULT
|
||||||
|
: CLAUDE_MODELS.DEFAULT);
|
||||||
|
|
||||||
|
const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0;
|
||||||
|
const total =
|
||||||
|
Number(
|
||||||
|
tokenUsage.total ??
|
||||||
|
tokenUsage.contextWindow ??
|
||||||
|
parseInt(process.env.CONTEXT_WINDOW || '160000', 10),
|
||||||
|
) || 160000;
|
||||||
|
const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0;
|
||||||
|
|
||||||
|
const inputTokensRaw =
|
||||||
|
Number(
|
||||||
|
tokenUsage.inputTokens ??
|
||||||
|
tokenUsage.input ??
|
||||||
|
tokenUsage.cumulativeInputTokens ??
|
||||||
|
tokenUsage.promptTokens ??
|
||||||
|
0,
|
||||||
|
) || 0;
|
||||||
|
const outputTokens =
|
||||||
|
Number(
|
||||||
|
tokenUsage.outputTokens ??
|
||||||
|
tokenUsage.output ??
|
||||||
|
tokenUsage.cumulativeOutputTokens ??
|
||||||
|
tokenUsage.completionTokens ??
|
||||||
|
0,
|
||||||
|
) || 0;
|
||||||
|
const cacheTokens =
|
||||||
|
Number(
|
||||||
|
tokenUsage.cacheReadTokens ??
|
||||||
|
tokenUsage.cacheCreationTokens ??
|
||||||
|
tokenUsage.cacheTokens ??
|
||||||
|
tokenUsage.cachedTokens ??
|
||||||
|
0,
|
||||||
|
) || 0;
|
||||||
|
|
||||||
|
// If we only have total used tokens, treat them as input for display/estimation.
|
||||||
|
const inputTokens =
|
||||||
|
inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used;
|
||||||
|
|
||||||
|
// Rough default rates by provider (USD / 1M tokens).
|
||||||
|
const pricingByProvider = {
|
||||||
|
claude: { input: 3, output: 15 },
|
||||||
|
cursor: { input: 3, output: 15 },
|
||||||
|
codex: { input: 1.5, output: 6 },
|
||||||
|
};
|
||||||
|
const rates = pricingByProvider[provider] || pricingByProvider.claude;
|
||||||
|
|
||||||
|
const inputCost = (inputTokens / 1_000_000) * rates.input;
|
||||||
|
const outputCost = (outputTokens / 1_000_000) * rates.output;
|
||||||
|
const totalCost = inputCost + outputCost;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'builtin',
|
||||||
|
action: 'cost',
|
||||||
|
data: {
|
||||||
|
tokenUsage: {
|
||||||
|
used,
|
||||||
|
total,
|
||||||
|
percentage,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
input: inputCost.toFixed(4),
|
||||||
|
output: outputCost.toFixed(4),
|
||||||
|
total: totalCost.toFixed(4),
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
'/status': async (args, context) => {
|
'/status': async (args, context) => {
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import { open } from 'sqlite';
|
import { open } from 'sqlite';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { extractProjectDirectory } from '../projects.js';
|
import { extractProjectDirectory } from '../projects.js';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from 'path';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { addProjectManually } from '../projects.js';
|
import { addProjectManually } from '../projects.js';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import express from 'express';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import { userDb } from '../database/db.js';
|
import { userDb } from '../database/db.js';
|
||||||
import { authenticateToken } from '../middleware/auth.js';
|
import { authenticateToken } from '../middleware/auth.js';
|
||||||
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function buildPushBody(event) {
|
|||||||
const message = CODE_MAP[event.code] || 'You have a new notification';
|
const message = CODE_MAP[event.code] || 'You have a new notification';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: sessionName || 'CloudCLI',
|
title: sessionName || 'Claude Code UI',
|
||||||
body: `${providerLabel}: ${message}`,
|
body: `${providerLabel}: ${message}`,
|
||||||
data: {
|
data: {
|
||||||
sessionId: event.sessionId || null,
|
sessionId: event.sessionId || null,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
function spawnAsync(command, args) {
|
function spawnAsync(command, args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
|
|
||||||
const PLUGINS_DIR = path.join(os.homedir(), '.claude-code-ui', 'plugins');
|
const PLUGINS_DIR = path.join(os.homedir(), '.claude-code-ui', 'plugins');
|
||||||
const PLUGINS_CONFIG_PATH = path.join(os.homedir(), '.claude-code-ui', 'plugins.json');
|
const PLUGINS_CONFIG_PATH = path.join(os.homedir(), '.claude-code-ui', 'plugins.json');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'cross-spawn';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { scanPlugins, getPluginsConfig, getPluginDir } from './plugin-loader.js';
|
import { scanPlugins, getPluginsConfig, getPluginDir } from './plugin-loader.js';
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default function AuthLoadingScreen() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="mb-2 text-2xl font-bold text-foreground">CloudCLI</h1>
|
<h1 className="mb-2 text-2xl font-bold text-foreground">Claude Code UI</h1>
|
||||||
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
<div className="flex items-center justify-center space-x-2">
|
||||||
{loadingDotAnimationDelays.map((delay) => (
|
{loadingDotAnimationDelays.map((delay) => (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { MessageSquare } from 'lucide-react';
|
import { MessageSquare } from 'lucide-react';
|
||||||
import { IS_PLATFORM } from '../../../constants/config';
|
|
||||||
|
|
||||||
type AuthScreenLayoutProps = {
|
type AuthScreenLayoutProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -38,22 +37,6 @@ export default function AuthScreenLayout({
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-muted-foreground">{footerText}</p>
|
<p className="text-sm text-muted-foreground">{footerText}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<div className="flex items-center justify-center gap-1.5 pt-2">
|
|
||||||
<svg className="h-3.5 w-3.5 text-muted-foreground/50" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
|
||||||
</svg>
|
|
||||||
<a
|
|
||||||
href="https://github.com/siteboon/claudecodeui"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-xs text-muted-foreground/50 transition-colors hover:text-muted-foreground"
|
|
||||||
>
|
|
||||||
CloudCLI is open source
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function LoginForm() {
|
|||||||
<AuthScreenLayout
|
<AuthScreenLayout
|
||||||
title={t('login.title')}
|
title={t('login.title')}
|
||||||
description={t('login.description')}
|
description={t('login.description')}
|
||||||
footerText="Enter your credentials to access CloudCLI"
|
footerText="Enter your credentials to access Claude Code UI"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<AuthInputField
|
<AuthInputField
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function SetupForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthScreenLayout
|
<AuthScreenLayout
|
||||||
title="Welcome to CloudCLI"
|
title="Welcome to Claude Code UI"
|
||||||
description="Set up your account to get started"
|
description="Set up your account to get started"
|
||||||
footerText="This is a single-user system. Only one account can be created."
|
footerText="This is a single-user system. Only one account can be created."
|
||||||
logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
|
logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface UseChatComposerStateArgs {
|
|||||||
geminiModel: string;
|
geminiModel: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
canAbortSession: boolean;
|
canAbortSession: boolean;
|
||||||
|
tokenBudget: Record<string, unknown> | null;
|
||||||
sendMessage: (message: unknown) => void;
|
sendMessage: (message: unknown) => void;
|
||||||
sendByCtrlEnter?: boolean;
|
sendByCtrlEnter?: boolean;
|
||||||
onSessionActive?: (sessionId?: string | null) => void;
|
onSessionActive?: (sessionId?: string | null) => void;
|
||||||
@@ -56,7 +57,7 @@ interface UseChatComposerStateArgs {
|
|||||||
rewindMessages: (count: number) => void;
|
rewindMessages: (count: number) => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
setCanAbortSession: (canAbort: boolean) => void;
|
setCanAbortSession: (canAbort: boolean) => void;
|
||||||
setClaudeStatus: (status: { text: string; can_interrupt: boolean } | null) => void;
|
setClaudeStatus: (status: { text: string; tokens: number; can_interrupt: boolean } | null) => void;
|
||||||
setIsUserScrolledUp: (isScrolledUp: boolean) => void;
|
setIsUserScrolledUp: (isScrolledUp: boolean) => void;
|
||||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,7 @@ export function useChatComposerState({
|
|||||||
geminiModel,
|
geminiModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
canAbortSession,
|
canAbortSession,
|
||||||
|
tokenBudget,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
@@ -174,6 +176,12 @@ export function useChatComposerState({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'cost': {
|
||||||
|
const costMessage = `**Token Usage**: ${data.tokenUsage.used.toLocaleString()} / ${data.tokenUsage.total.toLocaleString()} (${data.tokenUsage.percentage}%)\n\n**Estimated Cost**:\n- Input: $${data.cost.input}\n- Output: $${data.cost.output}\n- **Total**: $${data.cost.total}\n\n**Model**: ${data.model}`;
|
||||||
|
addMessage({ type: 'assistant', content: costMessage, timestamp: Date.now() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'status': {
|
case 'status': {
|
||||||
const statusMessage = `**System Status**\n\n- Version: ${data.version}\n- Uptime: ${data.uptime}\n- Model: ${data.model}\n- Provider: ${data.provider}\n- Node.js: ${data.nodeVersion}\n- Platform: ${data.platform}`;
|
const statusMessage = `**System Status**\n\n- Version: ${data.version}\n- Uptime: ${data.uptime}\n- Model: ${data.model}\n- Provider: ${data.provider}\n- Node.js: ${data.nodeVersion}\n- Platform: ${data.platform}`;
|
||||||
addMessage({ type: 'assistant', content: statusMessage, timestamp: Date.now() });
|
addMessage({ type: 'assistant', content: statusMessage, timestamp: Date.now() });
|
||||||
@@ -274,6 +282,7 @@ export function useChatComposerState({
|
|||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
provider,
|
provider,
|
||||||
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
|
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
|
||||||
|
tokenUsage: tokenBudget,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await authenticatedFetch('/api/commands/execute', {
|
const response = await authenticatedFetch('/api/commands/execute', {
|
||||||
@@ -330,6 +339,7 @@ export function useChatComposerState({
|
|||||||
provider,
|
provider,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
addMessage,
|
addMessage,
|
||||||
|
tokenBudget,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -533,6 +543,7 @@ export function useChatComposerState({
|
|||||||
setCanAbortSession(true);
|
setCanAbortSession(true);
|
||||||
setClaudeStatus({
|
setClaudeStatus({
|
||||||
text: 'Processing',
|
text: 'Processing',
|
||||||
|
tokens: 0,
|
||||||
can_interrupt: true,
|
can_interrupt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ type LatestChatMessage = {
|
|||||||
provider?: string;
|
provider?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
tokens?: number;
|
||||||
canInterrupt?: boolean;
|
canInterrupt?: boolean;
|
||||||
|
tokenBudget?: unknown;
|
||||||
newSessionId?: string;
|
newSessionId?: string;
|
||||||
aborted?: boolean;
|
aborted?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@@ -53,7 +55,8 @@ interface UseChatRealtimeHandlersArgs {
|
|||||||
setCurrentSessionId: (sessionId: string | null) => void;
|
setCurrentSessionId: (sessionId: string | null) => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
setCanAbortSession: (canAbort: boolean) => void;
|
setCanAbortSession: (canAbort: boolean) => void;
|
||||||
setClaudeStatus: (status: { text: string; can_interrupt: boolean } | null) => void;
|
setClaudeStatus: (status: { text: string; tokens: number; can_interrupt: boolean } | null) => void;
|
||||||
|
setTokenBudget: (budget: Record<string, unknown> | null) => void;
|
||||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||||
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
||||||
streamBufferRef: MutableRefObject<string>;
|
streamBufferRef: MutableRefObject<string>;
|
||||||
@@ -82,6 +85,7 @@ export function useChatRealtimeHandlers({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
|
setTokenBudget,
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
@@ -136,6 +140,7 @@ export function useChatRealtimeHandlers({
|
|||||||
if (status) {
|
if (status) {
|
||||||
const statusInfo = {
|
const statusInfo = {
|
||||||
text: status.text || 'Working...',
|
text: status.text || 'Working...',
|
||||||
|
tokens: status.tokens || 0,
|
||||||
can_interrupt: status.can_interrupt !== undefined ? status.can_interrupt : true,
|
can_interrupt: status.can_interrupt !== undefined ? status.can_interrupt : true,
|
||||||
};
|
};
|
||||||
setClaudeStatus(statusInfo);
|
setClaudeStatus(statusInfo);
|
||||||
@@ -306,7 +311,7 @@ export function useChatRealtimeHandlers({
|
|||||||
});
|
});
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setCanAbortSession(true);
|
setCanAbortSession(true);
|
||||||
setClaudeStatus({ text: 'Waiting for permission', can_interrupt: true });
|
setClaudeStatus({ text: 'Waiting for permission', tokens: 0, can_interrupt: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,9 +323,12 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'status': {
|
case 'status': {
|
||||||
if (msg.text) {
|
if (msg.text === 'token_budget' && msg.tokenBudget) {
|
||||||
|
setTokenBudget(msg.tokenBudget as Record<string, unknown>);
|
||||||
|
} else if (msg.text) {
|
||||||
setClaudeStatus({
|
setClaudeStatus({
|
||||||
text: msg.text,
|
text: msg.text,
|
||||||
|
tokens: msg.tokens || 0,
|
||||||
can_interrupt: msg.canInterrupt !== undefined ? msg.canInterrupt : true,
|
can_interrupt: msg.canInterrupt !== undefined ? msg.canInterrupt : true,
|
||||||
});
|
});
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -344,6 +352,7 @@ export function useChatRealtimeHandlers({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
|
setTokenBudget,
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
import type { ChatMessage, Provider } from '../types/types';
|
import type { ChatMessage, Provider } from '../types/types';
|
||||||
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
|
||||||
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms';
|
||||||
@@ -107,8 +108,9 @@ export function useChatSessionState({
|
|||||||
const [totalMessages, setTotalMessages] = useState(0);
|
const [totalMessages, setTotalMessages] = useState(0);
|
||||||
const [canAbortSession, setCanAbortSession] = useState(false);
|
const [canAbortSession, setCanAbortSession] = useState(false);
|
||||||
const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
|
const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
|
||||||
|
const [tokenBudget, setTokenBudget] = useState<Record<string, unknown> | null>(null);
|
||||||
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
const [visibleMessageCount, setVisibleMessageCount] = useState(INITIAL_VISIBLE_MESSAGES);
|
||||||
const [claudeStatus, setClaudeStatus] = useState<{ text: string; can_interrupt: boolean } | null>(null);
|
const [claudeStatus, setClaudeStatus] = useState<{ text: string; tokens: number; can_interrupt: boolean } | null>(null);
|
||||||
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);
|
const [allMessagesLoaded, setAllMessagesLoaded] = useState(false);
|
||||||
const [isLoadingAllMessages, setIsLoadingAllMessages] = useState(false);
|
const [isLoadingAllMessages, setIsLoadingAllMessages] = useState(false);
|
||||||
const [loadAllJustFinished, setLoadAllJustFinished] = useState(false);
|
const [loadAllJustFinished, setLoadAllJustFinished] = useState(false);
|
||||||
@@ -317,6 +319,7 @@ export function useChatSessionState({
|
|||||||
messagesOffsetRef.current = 0;
|
messagesOffsetRef.current = 0;
|
||||||
setHasMoreMessages(false);
|
setHasMoreMessages(false);
|
||||||
setTotalMessages(0);
|
setTotalMessages(0);
|
||||||
|
setTokenBudget(null);
|
||||||
lastLoadedSessionKeyRef.current = null;
|
lastLoadedSessionKeyRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -352,6 +355,7 @@ export function useChatSessionState({
|
|||||||
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||||
|
|
||||||
if (sessionChanged) {
|
if (sessionChanged) {
|
||||||
|
setTokenBudget(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +383,7 @@ export function useChatSessionState({
|
|||||||
if (slot) {
|
if (slot) {
|
||||||
setHasMoreMessages(slot.hasMore);
|
setHasMoreMessages(slot.hasMore);
|
||||||
setTotalMessages(slot.total);
|
setTotalMessages(slot.total);
|
||||||
|
if (slot.tokenUsage) setTokenBudget(slot.tokenUsage as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
setIsLoadingSessionMessages(false);
|
setIsLoadingSessionMessages(false);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
@@ -534,6 +539,31 @@ export function useChatSessionState({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [chatMessages.length, isLoadingSessionMessages, searchTarget]);
|
}, [chatMessages.length, isLoadingSessionMessages, searchTarget]);
|
||||||
|
|
||||||
|
// Token usage fetch for Claude
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedProject || !selectedSession?.id || selectedSession.id.startsWith('new-session-')) {
|
||||||
|
setTokenBudget(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sessionProvider = selectedSession.__provider || 'claude';
|
||||||
|
if (sessionProvider !== 'claude') return;
|
||||||
|
|
||||||
|
const fetchInitialTokenUsage = async () => {
|
||||||
|
try {
|
||||||
|
const url = `/api/projects/${selectedProject.name}/sessions/${selectedSession.id}/token-usage`;
|
||||||
|
const response = await authenticatedFetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
setTokenBudget(await response.json());
|
||||||
|
} else {
|
||||||
|
setTokenBudget(null);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch initial token usage:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchInitialTokenUsage();
|
||||||
|
}, [selectedProject, selectedSession?.id, selectedSession?.__provider]);
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
if (chatMessages.length <= visibleMessageCount) return chatMessages;
|
||||||
return chatMessages.slice(-visibleMessageCount);
|
return chatMessages.slice(-visibleMessageCount);
|
||||||
@@ -683,6 +713,8 @@ export function useChatSessionState({
|
|||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
isUserScrolledUp,
|
isUserScrolledUp,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
|
tokenBudget,
|
||||||
|
setTokenBudget,
|
||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ function ChatInterface({
|
|||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
isUserScrolledUp,
|
isUserScrolledUp,
|
||||||
setIsUserScrolledUp,
|
setIsUserScrolledUp,
|
||||||
|
tokenBudget,
|
||||||
|
setTokenBudget,
|
||||||
visibleMessageCount,
|
visibleMessageCount,
|
||||||
visibleMessages,
|
visibleMessages,
|
||||||
loadEarlierMessages,
|
loadEarlierMessages,
|
||||||
@@ -181,6 +183,7 @@ function ChatInterface({
|
|||||||
geminiModel,
|
geminiModel,
|
||||||
isLoading,
|
isLoading,
|
||||||
canAbortSession,
|
canAbortSession,
|
||||||
|
tokenBudget,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendByCtrlEnter,
|
sendByCtrlEnter,
|
||||||
onSessionActive,
|
onSessionActive,
|
||||||
@@ -224,6 +227,7 @@ function ChatInterface({
|
|||||||
setIsLoading,
|
setIsLoading,
|
||||||
setCanAbortSession,
|
setCanAbortSession,
|
||||||
setClaudeStatus,
|
setClaudeStatus,
|
||||||
|
setTokenBudget,
|
||||||
setPendingPermissionRequests,
|
setPendingPermissionRequests,
|
||||||
pendingViewSessionRef,
|
pendingViewSessionRef,
|
||||||
streamBufferRef,
|
streamBufferRef,
|
||||||
@@ -348,6 +352,7 @@ function ChatInterface({
|
|||||||
onModeSwitch={cyclePermissionMode}
|
onModeSwitch={cyclePermissionMode}
|
||||||
thinkingMode={thinkingMode}
|
thinkingMode={thinkingMode}
|
||||||
setThinkingMode={setThinkingMode}
|
setThinkingMode={setThinkingMode}
|
||||||
|
tokenBudget={tokenBudget}
|
||||||
slashCommandsCount={slashCommandsCount}
|
slashCommandsCount={slashCommandsCount}
|
||||||
onToggleCommandMenu={handleToggleCommandMenu}
|
onToggleCommandMenu={handleToggleCommandMenu}
|
||||||
hasInput={Boolean(input.trim())}
|
hasInput={Boolean(input.trim())}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ interface ChatComposerProps {
|
|||||||
decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown },
|
||||||
) => void;
|
) => void;
|
||||||
handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
||||||
claudeStatus: { text: string; can_interrupt: boolean } | null;
|
claudeStatus: { text: string; tokens: number; can_interrupt: boolean } | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onAbortSession: () => void;
|
onAbortSession: () => void;
|
||||||
provider: Provider | string;
|
provider: Provider | string;
|
||||||
@@ -49,6 +49,7 @@ interface ChatComposerProps {
|
|||||||
onModeSwitch: () => void;
|
onModeSwitch: () => void;
|
||||||
thinkingMode: string;
|
thinkingMode: string;
|
||||||
setThinkingMode: Dispatch<SetStateAction<string>>;
|
setThinkingMode: Dispatch<SetStateAction<string>>;
|
||||||
|
tokenBudget: { used?: number; total?: number } | null;
|
||||||
slashCommandsCount: number;
|
slashCommandsCount: number;
|
||||||
onToggleCommandMenu: () => void;
|
onToggleCommandMenu: () => void;
|
||||||
hasInput: boolean;
|
hasInput: boolean;
|
||||||
@@ -105,6 +106,7 @@ export default function ChatComposer({
|
|||||||
onModeSwitch,
|
onModeSwitch,
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
setThinkingMode,
|
setThinkingMode,
|
||||||
|
tokenBudget,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
onToggleCommandMenu,
|
onToggleCommandMenu,
|
||||||
hasInput,
|
hasInput,
|
||||||
@@ -192,6 +194,7 @@ export default function ChatComposer({
|
|||||||
provider={provider}
|
provider={provider}
|
||||||
thinkingMode={thinkingMode}
|
thinkingMode={thinkingMode}
|
||||||
setThinkingMode={setThinkingMode}
|
setThinkingMode={setThinkingMode}
|
||||||
|
tokenBudget={tokenBudget}
|
||||||
slashCommandsCount={slashCommandsCount}
|
slashCommandsCount={slashCommandsCount}
|
||||||
onToggleCommandMenu={onToggleCommandMenu}
|
onToggleCommandMenu={onToggleCommandMenu}
|
||||||
hasInput={hasInput}
|
hasInput={hasInput}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { PermissionMode, Provider } from '../../types/types';
|
import type { PermissionMode, Provider } from '../../types/types';
|
||||||
import ThinkingModeSelector from './ThinkingModeSelector';
|
import ThinkingModeSelector from './ThinkingModeSelector';
|
||||||
|
import TokenUsagePie from './TokenUsagePie';
|
||||||
|
|
||||||
interface ChatInputControlsProps {
|
interface ChatInputControlsProps {
|
||||||
permissionMode: PermissionMode | string;
|
permissionMode: PermissionMode | string;
|
||||||
@@ -9,6 +10,7 @@ interface ChatInputControlsProps {
|
|||||||
provider: Provider | string;
|
provider: Provider | string;
|
||||||
thinkingMode: string;
|
thinkingMode: string;
|
||||||
setThinkingMode: React.Dispatch<React.SetStateAction<string>>;
|
setThinkingMode: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
tokenBudget: { used?: number; total?: number } | null;
|
||||||
slashCommandsCount: number;
|
slashCommandsCount: number;
|
||||||
onToggleCommandMenu: () => void;
|
onToggleCommandMenu: () => void;
|
||||||
hasInput: boolean;
|
hasInput: boolean;
|
||||||
@@ -24,6 +26,7 @@ export default function ChatInputControls({
|
|||||||
provider,
|
provider,
|
||||||
thinkingMode,
|
thinkingMode,
|
||||||
setThinkingMode,
|
setThinkingMode,
|
||||||
|
tokenBudget,
|
||||||
slashCommandsCount,
|
slashCommandsCount,
|
||||||
onToggleCommandMenu,
|
onToggleCommandMenu,
|
||||||
hasInput,
|
hasInput,
|
||||||
@@ -75,6 +78,8 @@ export default function ChatInputControls({
|
|||||||
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
|
<ThinkingModeSelector selectedMode={thinkingMode} onModeChange={setThinkingMode} onClose={() => {}} className="" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TokenUsagePie used={tokenBudget?.used || 0} total={tokenBudget?.total || parseInt(import.meta.env.VITE_CONTEXT_WINDOW) || 160000} />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleCommandMenu}
|
onClick={onToggleCommandMenu}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'
|
|||||||
type ClaudeStatusProps = {
|
type ClaudeStatusProps = {
|
||||||
status: {
|
status: {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
tokens?: number;
|
||||||
can_interrupt?: boolean;
|
can_interrupt?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
onAbort?: () => void;
|
onAbort?: () => void;
|
||||||
|
|||||||
54
src/components/chat/view/subcomponents/TokenUsagePie.tsx
Normal file
54
src/components/chat/view/subcomponents/TokenUsagePie.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
type TokenUsagePieProps = {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TokenUsagePie({ used, total }: TokenUsagePieProps) {
|
||||||
|
// Token usage visualization component
|
||||||
|
// Only bail out on missing values or non‐positive totals; allow used===0 to render 0%
|
||||||
|
if (used == null || total == null || total <= 0) return null;
|
||||||
|
|
||||||
|
const percentage = Math.min(100, (used / total) * 100);
|
||||||
|
const radius = 10;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const offset = circumference - (percentage / 100) * circumference;
|
||||||
|
|
||||||
|
// Color based on usage level
|
||||||
|
const getColor = () => {
|
||||||
|
if (percentage < 50) return '#3b82f6'; // blue
|
||||||
|
if (percentage < 75) return '#f59e0b'; // orange
|
||||||
|
return '#ef4444'; // red
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" className="-rotate-90 transform">
|
||||||
|
{/* Background circle */}
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
className="text-gray-300 dark:text-gray-600"
|
||||||
|
/>
|
||||||
|
{/* Progress circle */}
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke={getColor()}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span title={`${used.toLocaleString()} / ${total.toLocaleString()} tokens`}>
|
||||||
|
{percentage.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about';
|
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins';
|
||||||
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||||
export type ProjectSortOrder = 'name' | 'date';
|
export type ProjectSortOrder = 'name' | 'date';
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { ExternalLink, Lock } from 'lucide-react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
|
||||||
|
|
||||||
type PremiumFeatureCardProps = {
|
|
||||||
icon: ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
ctaText?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PremiumFeatureCard({
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
ctaText = 'Available with CloudCLI Pro',
|
|
||||||
}: PremiumFeatureCardProps) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-dashed border-border/60 bg-muted/20 p-5">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h4 className="text-sm font-medium text-foreground">{title}</h4>
|
|
||||||
<Lock className="h-3 w-3 text-muted-foreground/60" />
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={CLOUDCLI_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-3 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
|
||||||
>
|
|
||||||
{ctaText}
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
|
|||||||
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
|
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
|
||||||
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
|
||||||
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
|
||||||
import AboutTab from '../view/tabs/AboutTab';
|
|
||||||
import { useSettingsController } from '../hooks/useSettingsController';
|
import { useSettingsController } from '../hooks/useSettingsController';
|
||||||
import { useWebPush } from '../../../hooks/useWebPush';
|
import { useWebPush } from '../../../hooks/useWebPush';
|
||||||
import type { SettingsProps } from '../types/types';
|
import type { SettingsProps } from '../types/types';
|
||||||
@@ -207,8 +206,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
|||||||
{activeTab === 'api' && <CredentialsSettingsTab />}
|
{activeTab === 'api' && <CredentialsSettingsTab />}
|
||||||
|
|
||||||
{activeTab === 'plugins' && <PluginSettingsTab />}
|
{activeTab === 'plugins' && <PluginSettingsTab />}
|
||||||
|
|
||||||
{activeTab === 'about' && <AboutTab />}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GitBranch, Info, Key, Puzzle } from 'lucide-react';
|
import { GitBranch, Key, Puzzle } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { SettingsMainTab } from '../types/types';
|
import type { SettingsMainTab } from '../types/types';
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ const TAB_CONFIG: MainTabConfig[] = [
|
|||||||
{ id: 'tasks', labelKey: 'mainTabs.tasks' },
|
{ id: 'tasks', labelKey: 'mainTabs.tasks' },
|
||||||
{ id: 'notifications', labelKey: 'mainTabs.notifications' },
|
{ id: 'notifications', labelKey: 'mainTabs.notifications' },
|
||||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||||
{ id: 'about', labelKey: 'mainTabs.about', icon: Info },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) {
|
export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bell, Bot, GitBranch, Info, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
import { Bell, Bot, GitBranch, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '../../../lib/utils';
|
import { cn } from '../../../lib/utils';
|
||||||
import { PillBar, Pill } from '../../../shared/view/ui';
|
import { PillBar, Pill } from '../../../shared/view/ui';
|
||||||
@@ -23,7 +23,6 @@ const NAV_ITEMS: NavItem[] = [
|
|||||||
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
|
||||||
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
|
||||||
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
|
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
|
||||||
{ id: 'about', labelKey: 'mainTabs.about', icon: Info },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
export default function SettingsSidebar({ activeTab, onChange }: SettingsSidebarProps) {
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
import { ExternalLink, MessageSquare, Star } from 'lucide-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
|
||||||
import { useVersionCheck } from '../../../../hooks/useVersionCheck';
|
|
||||||
import PremiumFeatureCard from '../PremiumFeatureCard';
|
|
||||||
import { Cloud, Users } from 'lucide-react';
|
|
||||||
|
|
||||||
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
|
||||||
const DISCORD_URL = 'https://discord.gg/buxwujPNRE';
|
|
||||||
const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview';
|
|
||||||
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
|
||||||
|
|
||||||
function GitHubIcon({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AboutTab() {
|
|
||||||
const { t } = useTranslation('settings');
|
|
||||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
|
||||||
const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Logo + name + version */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-primary/90 shadow-sm">
|
|
||||||
<MessageSquare className="h-5 w-5 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-base font-semibold text-foreground">CloudCLI</span>
|
|
||||||
<a
|
|
||||||
href={releasesUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
v{currentVersion}
|
|
||||||
</a>
|
|
||||||
{updateAvailable && latestVersion && (
|
|
||||||
<a
|
|
||||||
href={releasesUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
|
||||||
>
|
|
||||||
{t('apiKeys.version.updateAvailable', { version: latestVersion })}
|
|
||||||
<ExternalLink className="h-2.5 w-2.5" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
|
||||||
Open-source AI coding assistant interface
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Star on GitHub button */}
|
|
||||||
<a
|
|
||||||
href={GITHUB_REPO_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-border/60 bg-background px-3.5 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
||||||
>
|
|
||||||
<GitHubIcon className="h-4 w-4" />
|
|
||||||
<Star className="h-3.5 w-3.5" />
|
|
||||||
<span>Star on GitHub</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Links */}
|
|
||||||
<div className="flex flex-wrap gap-4 text-sm">
|
|
||||||
<a
|
|
||||||
href={GITHUB_REPO_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<GitHubIcon className="h-4 w-4" />
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={DISCORD_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<DiscordIcon className="h-4 w-4" />
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={DOCS_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3.5 w-3.5" />
|
|
||||||
Docs
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={CLOUDCLI_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3.5 w-3.5" />
|
|
||||||
cloudcli.ai
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hosted CTA (OSS mode only) */}
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<div className="rounded-xl border border-primary/10 bg-primary/5 p-4">
|
|
||||||
<h4 className="text-sm font-medium text-foreground">Try CloudCLI Hosted</h4>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={CLOUDCLI_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Premium feature placeholders (OSS mode only) */}
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<div className="space-y-4 border-t border-border/50 pt-6">
|
|
||||||
<h3 className="text-sm font-medium text-foreground">CloudCLI Pro Features</h3>
|
|
||||||
<PremiumFeatureCard
|
|
||||||
icon={<Cloud className="h-5 w-5" />}
|
|
||||||
title="Sync Settings"
|
|
||||||
description="Keep your preferences, MCP configs, and theme in sync across all your environments."
|
|
||||||
/>
|
|
||||||
<PremiumFeatureCard
|
|
||||||
icon={<Users className="h-5 w-5" />}
|
|
||||||
title="Team Management"
|
|
||||||
description="Multiple users, role-based access, and shared projects for your team."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* License */}
|
|
||||||
<div className="border-t border-border/50 pt-4">
|
|
||||||
<p className="text-xs text-muted-foreground/60">
|
|
||||||
Licensed under AGPL-3.0
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Edit3, Globe, Plus, Server, Terminal, Trash2, Users, Zap } from 'lucide-react';
|
import { Edit3, Globe, Plus, Server, Terminal, Trash2, Zap } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Badge, Button } from '../../../../../../../shared/view/ui';
|
import { Badge, Button } from '../../../../../../../shared/view/ui';
|
||||||
import { IS_PLATFORM } from '../../../../../../../constants/config';
|
|
||||||
import PremiumFeatureCard from '../../../../PremiumFeatureCard';
|
|
||||||
import type { McpServer, McpToolsResult, McpTestResult } from '../../../../../types/types';
|
import type { McpServer, McpToolsResult, McpTestResult } from '../../../../../types/types';
|
||||||
|
|
||||||
const getTransportIcon = (type: string | undefined) => {
|
const getTransportIcon = (type: string | undefined) => {
|
||||||
@@ -181,14 +179,6 @@ function ClaudeMcpServers({
|
|||||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<PremiumFeatureCard
|
|
||||||
icon={<Users className="h-5 w-5" />}
|
|
||||||
title="Team MCP Configs"
|
|
||||||
description="Share MCP server configurations across your team. Everyone stays in sync automatically."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useVersionCheck } from '../../../../../hooks/useVersionCheck';
|
||||||
import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings';
|
import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings';
|
||||||
import ApiKeysSection from './sections/ApiKeysSection';
|
import ApiKeysSection from './sections/ApiKeysSection';
|
||||||
import GithubCredentialsSection from './sections/GithubCredentialsSection';
|
import GithubCredentialsSection from './sections/GithubCredentialsSection';
|
||||||
import NewApiKeyAlert from './sections/NewApiKeyAlert';
|
import NewApiKeyAlert from './sections/NewApiKeyAlert';
|
||||||
|
import VersionInfoSection from './sections/VersionInfoSection';
|
||||||
|
|
||||||
export default function CredentialsSettingsTab() {
|
export default function CredentialsSettingsTab() {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
|
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
||||||
const {
|
const {
|
||||||
apiKeys,
|
apiKeys,
|
||||||
githubCredentials,
|
githubCredentials,
|
||||||
@@ -86,6 +89,12 @@ export default function CredentialsSettingsTab() {
|
|||||||
onDeleteGithubCredential={deleteGithubCredential}
|
onDeleteGithubCredential={deleteGithubCredential}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<VersionInfoSection
|
||||||
|
currentVersion={currentVersion}
|
||||||
|
updateAvailable={updateAvailable}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
releaseInfo={releaseInfo}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,7 @@
|
|||||||
import { ExternalLink, Star, MessageSquare } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IS_PLATFORM } from '../../../../../../constants/config';
|
|
||||||
import type { ReleaseInfo } from '../../../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../../../types/sharedTypes';
|
||||||
|
|
||||||
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
|
||||||
const DISCORD_URL = 'https://discord.gg/buxwujPNRE';
|
|
||||||
const DOCS_URL = 'https://cloudcli.ai/docs/plugin-overview';
|
|
||||||
const CLOUDCLI_URL = 'https://cloudcli.ai';
|
|
||||||
|
|
||||||
function GitHubIcon({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type VersionInfoSectionProps = {
|
type VersionInfoSectionProps = {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
@@ -38,115 +16,29 @@ export default function VersionInfoSection({
|
|||||||
releaseInfo,
|
releaseInfo,
|
||||||
}: VersionInfoSectionProps) {
|
}: VersionInfoSectionProps) {
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation('settings');
|
||||||
const releasesUrl = releaseInfo?.htmlUrl || `${GITHUB_REPO_URL}/releases`;
|
const releasesUrl = releaseInfo?.htmlUrl || 'https://github.com/siteboon/claudecodeui/releases';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-border/50 pt-6">
|
<div className="border-t border-border/50 pt-6">
|
||||||
{/* About CloudCLI */}
|
<div className="flex items-center justify-between text-xs italic text-muted-foreground/60">
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Logo + name + version */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-primary/90 shadow-sm">
|
|
||||||
<MessageSquare className="h-4.5 w-4.5 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm font-semibold text-foreground">CloudCLI</span>
|
|
||||||
<a
|
|
||||||
href={releasesUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
v{currentVersion}
|
|
||||||
</a>
|
|
||||||
{updateAvailable && latestVersion && (
|
|
||||||
<a
|
|
||||||
href={releasesUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
|
||||||
>
|
|
||||||
{t('apiKeys.version.updateAvailable', { version: latestVersion })}
|
|
||||||
<ExternalLink className="h-2.5 w-2.5" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
||||||
Open-source AI coding assistant interface
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Star on GitHub button */}
|
|
||||||
<a
|
<a
|
||||||
href={GITHUB_REPO_URL}
|
href={releasesUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 rounded-lg border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
className="transition-colors hover:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<GitHubIcon className="h-4 w-4" />
|
v{currentVersion}
|
||||||
<Star className="h-3.5 w-3.5" />
|
|
||||||
<span>Star on GitHub</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
{updateAvailable && latestVersion && (
|
||||||
{/* Links */}
|
|
||||||
<div className="flex flex-wrap gap-3 text-xs">
|
|
||||||
<a
|
<a
|
||||||
href={GITHUB_REPO_URL}
|
href={releasesUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
className="flex items-center gap-1.5 rounded-full bg-green-500/10 px-2 py-0.5 font-medium not-italic text-green-600 transition-colors hover:bg-green-500/20 dark:text-green-400"
|
||||||
>
|
>
|
||||||
<GitHubIcon className="h-3.5 w-3.5" />
|
<span className="text-[10px]">{t('apiKeys.version.updateAvailable', { version: latestVersion })}</span>
|
||||||
GitHub
|
<ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href={DISCORD_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<DiscordIcon className="h-3.5 w-3.5" />
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={DOCS_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
Docs
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={CLOUDCLI_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
cloudcli.ai
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hosted CTA (OSS mode only) */}
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<div className="rounded-xl border border-primary/10 bg-primary/5 p-4">
|
|
||||||
<h4 className="text-sm font-medium text-foreground">Try CloudCLI Hosted</h4>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
Team collaboration, shared MCP configs, settings sync across environments, and managed infrastructure.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href={CLOUDCLI_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:underline"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
<ExternalLink className="h-3 w-3" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -266,7 +266,6 @@ function Sidebar({
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
projectListProps={projectListProps}
|
projectListProps={projectListProps}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Star, X } from 'lucide-react';
|
|
||||||
import { useGitHubStars } from '../../../../hooks/useGitHubStars';
|
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
|
||||||
|
|
||||||
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
|
||||||
|
|
||||||
function GitHubIcon({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GitHubStarBadge() {
|
|
||||||
const { formattedCount, isDismissed, dismiss } = useGitHubStars('siteboon', 'claudecodeui');
|
|
||||||
|
|
||||||
if (IS_PLATFORM || isDismissed) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group/star relative hidden md:block">
|
|
||||||
<a
|
|
||||||
href={GITHUB_REPO_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg border border-border/50 bg-muted/30 px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
||||||
>
|
|
||||||
<GitHubIcon className="h-3.5 w-3.5" />
|
|
||||||
<Star className="h-3 w-3" />
|
|
||||||
<span className="font-medium">Star</span>
|
|
||||||
{formattedCount && (
|
|
||||||
<span className="border-l border-border/50 pl-1.5 tabular-nums">{formattedCount}</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
dismiss();
|
|
||||||
}}
|
|
||||||
className="absolute -right-1.5 -top-1.5 hidden h-4 w-4 items-center justify-center rounded-full border border-border/50 bg-muted text-muted-foreground transition-colors hover:text-foreground group-hover/star:flex"
|
|
||||||
aria-label="Dismiss"
|
|
||||||
>
|
|
||||||
<X className="h-2.5 w-2.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Settings, Sparkles, PanelLeftOpen, Bug } from 'lucide-react';
|
import { Settings, Sparkles, PanelLeftOpen } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
|
||||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new';
|
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -51,18 +50,6 @@ export default function SidebarCollapsed({
|
|||||||
<Settings className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
<Settings className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Report Issue */}
|
|
||||||
<a
|
|
||||||
href={GITHUB_ISSUES_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group flex h-8 w-8 items-center justify-center rounded-lg transition-colors hover:bg-accent/80"
|
|
||||||
aria-label={t('actions.reportIssue')}
|
|
||||||
title={t('actions.reportIssue')}
|
|
||||||
>
|
|
||||||
<Bug className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Discord */}
|
{/* Discord */}
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ type SidebarContentProps = {
|
|||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
currentVersion: string;
|
|
||||||
onShowVersionModal: () => void;
|
onShowVersionModal: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
projectListProps: SidebarProjectListProps;
|
projectListProps: SidebarProjectListProps;
|
||||||
@@ -84,7 +83,6 @@ export default function SidebarContent({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
projectListProps,
|
projectListProps,
|
||||||
@@ -219,7 +217,6 @@ export default function SidebarContent({
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
releaseInfo={releaseInfo}
|
releaseInfo={releaseInfo}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
|
||||||
onShowVersionModal={onShowVersionModal}
|
onShowVersionModal={onShowVersionModal}
|
||||||
onShowSettings={onShowSettings}
|
onShowSettings={onShowSettings}
|
||||||
t={t}
|
t={t}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Settings, ArrowUpCircle, Bug } from 'lucide-react';
|
import { Settings, ArrowUpCircle } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
|
|
||||||
const GITHUB_ISSUES_URL = 'https://github.com/siteboon/claudecodeui/issues/new';
|
|
||||||
const GITHUB_REPO_URL = 'https://github.com/siteboon/claudecodeui';
|
|
||||||
|
|
||||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||||
|
|
||||||
function DiscordIcon({ className }: { className?: string }) {
|
function DiscordIcon({ className }: { className?: string }) {
|
||||||
@@ -20,7 +16,6 @@ type SidebarFooterProps = {
|
|||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
releaseInfo: ReleaseInfo | null;
|
releaseInfo: ReleaseInfo | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
currentVersion: string;
|
|
||||||
onShowVersionModal: () => void;
|
onShowVersionModal: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
@@ -30,7 +25,6 @@ export default function SidebarFooter({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
releaseInfo,
|
releaseInfo,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
t,
|
t,
|
||||||
@@ -85,24 +79,11 @@ export default function SidebarFooter({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Community + Settings */}
|
{/* Discord + Settings */}
|
||||||
<div className="nav-divider" />
|
<div className="nav-divider" />
|
||||||
|
|
||||||
{/* Desktop Report Issue */}
|
|
||||||
<div className="hidden px-2 pt-1.5 md:block">
|
|
||||||
<a
|
|
||||||
href={GITHUB_ISSUES_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
||||||
>
|
|
||||||
<Bug className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-sm">{t('actions.reportIssue')}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop Discord */}
|
{/* Desktop Discord */}
|
||||||
<div className="hidden px-2 md:block">
|
<div className="hidden px-2 pt-1.5 md:block">
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -125,37 +106,8 @@ export default function SidebarFooter({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop version brand line (OSS mode only) */}
|
|
||||||
{!IS_PLATFORM && (
|
|
||||||
<div className="hidden px-3 py-2 text-center md:block">
|
|
||||||
<a
|
|
||||||
href={GITHUB_REPO_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-[10px] text-muted-foreground/40 transition-colors hover:text-muted-foreground"
|
|
||||||
>
|
|
||||||
CloudCLI v{currentVersion} – {t('branding.openSource')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile Report Issue */}
|
|
||||||
<div className="px-3 pt-3 md:hidden">
|
|
||||||
<a
|
|
||||||
href={GITHUB_ISSUES_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]"
|
|
||||||
>
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80">
|
|
||||||
<Bug className="w-4.5 h-4.5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<span className="text-base font-medium text-foreground">{t('actions.reportIssue')}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Discord */}
|
{/* Mobile Discord */}
|
||||||
<div className="px-3 pt-2 md:hidden">
|
<div className="px-3 pt-3 md:hidden">
|
||||||
<a
|
<a
|
||||||
href={DISCORD_INVITE_URL}
|
href={DISCORD_INVITE_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { TFunction } from 'i18next';
|
|||||||
import { Button, Input } from '../../../../shared/view/ui';
|
import { Button, Input } from '../../../../shared/view/ui';
|
||||||
import { IS_PLATFORM } from '../../../../constants/config';
|
import { IS_PLATFORM } from '../../../../constants/config';
|
||||||
import { cn } from '../../../../lib/utils';
|
import { cn } from '../../../../lib/utils';
|
||||||
import GitHubStarBadge from './GitHubStarBadge';
|
|
||||||
|
|
||||||
type SearchMode = 'projects' | 'conversations';
|
type SearchMode = 'projects' | 'conversations';
|
||||||
|
|
||||||
@@ -107,8 +106,6 @@ export default function SidebarHeader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GitHubStarBadge />
|
|
||||||
|
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
{projectsCount > 0 && !isLoading && (
|
{projectsCount > 0 && !isLoading && (
|
||||||
<div className="mt-2.5 space-y-2">
|
<div className="mt-2.5 space-y-2">
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
|
|
||||||
const CACHE_KEY = 'CLOUDCLI_GITHUB_STARS';
|
|
||||||
const DISMISS_KEY = 'CLOUDCLI_HIDE_GITHUB_STAR';
|
|
||||||
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
||||||
|
|
||||||
type CachedStars = {
|
|
||||||
count: number;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGitHubStars = (owner: string, repo: string) => {
|
|
||||||
const [starCount, setStarCount] = useState<number | null>(null);
|
|
||||||
const [isDismissed, setIsDismissed] = useState(() => {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(DISMISS_KEY) === 'true';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDismissed) return;
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
try {
|
|
||||||
const cached = localStorage.getItem(CACHE_KEY);
|
|
||||||
if (cached) {
|
|
||||||
const parsed: CachedStars = JSON.parse(cached);
|
|
||||||
if (Date.now() - parsed.timestamp < CACHE_TTL) {
|
|
||||||
setStarCount(parsed.count);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchStars = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
|
||||||
if (!response.ok) return;
|
|
||||||
const data = await response.json();
|
|
||||||
const count = data.stargazers_count;
|
|
||||||
if (typeof count === 'number') {
|
|
||||||
setStarCount(count);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(CACHE_KEY, JSON.stringify({ count, timestamp: Date.now() }));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent fail
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void fetchStars();
|
|
||||||
}, [owner, repo, isDismissed]);
|
|
||||||
|
|
||||||
const dismiss = useCallback(() => {
|
|
||||||
setIsDismissed(true);
|
|
||||||
try {
|
|
||||||
localStorage.setItem(DISMISS_KEY, 'true');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const formattedCount = starCount !== null
|
|
||||||
? starCount >= 1000
|
|
||||||
? `${(starCount / 1000).toFixed(1)}k`
|
|
||||||
: `${starCount}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return { starCount, formattedCount, isDismissed, dismiss };
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Willkommen zurück",
|
"title": "Willkommen zurück",
|
||||||
"description": "Meld dich bei deinem CloudCLI-Konto an",
|
"description": "Meld dich bei deinem Claude Code UI-Konto an",
|
||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"submit": "Anmelden",
|
"submit": "Anmelden",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Im Editor öffnen"
|
"openInEditor": "Im Editor öffnen"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "CloudCLI wird geladen",
|
"loading": "Claude Code UI wird geladen",
|
||||||
"settingUpWorkspace": "Arbeitsbereich wird eingerichtet...",
|
"settingUpWorkspace": "Arbeitsbereich wird eingerichtet...",
|
||||||
"chooseProject": "Projekt auswählen",
|
"chooseProject": "Projekt auswählen",
|
||||||
"selectProjectDescription": "Wähl ein Projekt aus der Seitenleiste, um mit Claude zu programmieren. Jedes Projekt enthält deine Chat-Sitzungen und den Dateiverlauf.",
|
"selectProjectDescription": "Wähl ein Projekt aus der Seitenleiste, um mit Claude zu programmieren. Jedes Projekt enthält deine Chat-Sitzungen und den Dateiverlauf.",
|
||||||
|
|||||||
@@ -105,8 +105,7 @@
|
|||||||
"git": "Git",
|
"git": "Git",
|
||||||
"apiTokens": "API & Token",
|
"apiTokens": "API & Token",
|
||||||
"tasks": "Aufgaben",
|
"tasks": "Aufgaben",
|
||||||
"plugins": "Plugins",
|
"plugins": "Plugins"
|
||||||
"about": "Info"
|
|
||||||
},
|
},
|
||||||
"appearanceSettings": {
|
"appearanceSettings": {
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Führ Claude CLI in einem Projektverzeichnis aus, um zu beginnen"
|
"runClaudeCli": "Führ Claude CLI in einem Projektverzeichnis aus, um zu beginnen"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "KI-Programmierassistent-Oberfläche"
|
"subtitle": "KI-Programmierassistent-Oberfläche"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,12 +65,7 @@
|
|||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
"joinCommunity": "Community beitreten",
|
"joinCommunity": "Community beitreten"
|
||||||
"reportIssue": "Problem melden",
|
|
||||||
"starOnGithub": "Stern auf GitHub"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "Open Source"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Welcome Back",
|
"title": "Welcome Back",
|
||||||
"description": "Sign in to your CloudCLI self-hosted account",
|
"description": "Sign in to your Claude Code UI account",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"submit": "Sign In",
|
"submit": "Sign In",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Open in Editor"
|
"openInEditor": "Open in Editor"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Loading CloudCLI",
|
"loading": "Loading Claude Code UI",
|
||||||
"settingUpWorkspace": "Setting up your workspace...",
|
"settingUpWorkspace": "Setting up your workspace...",
|
||||||
"chooseProject": "Choose Your Project",
|
"chooseProject": "Choose Your Project",
|
||||||
"selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.",
|
"selectProjectDescription": "Select a project from the sidebar to start coding with Claude. Each project contains your chat sessions and file history.",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & Tokens",
|
"apiTokens": "API & Tokens",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"plugins": "Plugins",
|
"plugins": "Plugins"
|
||||||
"about": "About"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Run Claude CLI in a project directory to get started"
|
"runClaudeCli": "Run Claude CLI in a project directory to get started"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "AI coding assistant interface"
|
"subtitle": "AI coding assistant interface"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,12 +65,7 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"joinCommunity": "Join Community",
|
"joinCommunity": "Join Community"
|
||||||
"reportIssue": "Report Issue",
|
|
||||||
"starOnGithub": "Star on GitHub"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "Open Source"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "おかえりなさい",
|
"title": "おかえりなさい",
|
||||||
"description": "CloudCLIアカウントにサインイン",
|
"description": "Claude Code UIアカウントにサインイン",
|
||||||
"username": "ユーザー名",
|
"username": "ユーザー名",
|
||||||
"password": "パスワード",
|
"password": "パスワード",
|
||||||
"submit": "サインイン",
|
"submit": "サインイン",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "エディタで開く"
|
"openInEditor": "エディタで開く"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "CloudCLI を読み込んでいます",
|
"loading": "Claude Code UI を読み込んでいます",
|
||||||
"settingUpWorkspace": "ワークスペースを準備しています...",
|
"settingUpWorkspace": "ワークスペースを準備しています...",
|
||||||
"chooseProject": "プロジェクトを選択",
|
"chooseProject": "プロジェクトを選択",
|
||||||
"selectProjectDescription": "サイドバーからプロジェクトを選択して、Claudeとコーディングを始めましょう。各プロジェクトにはチャットセッションとファイル履歴が含まれています。",
|
"selectProjectDescription": "サイドバーからプロジェクトを選択して、Claudeとコーディングを始めましょう。各プロジェクトにはチャットセッションとファイル履歴が含まれています。",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & トークン",
|
"apiTokens": "API & トークン",
|
||||||
"tasks": "タスク",
|
"tasks": "タスク",
|
||||||
"notifications": "通知",
|
"notifications": "通知",
|
||||||
"plugins": "プラグイン",
|
"plugins": "プラグイン"
|
||||||
"about": "概要"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "プロジェクトディレクトリでClaude CLIを実行して始めましょう"
|
"runClaudeCli": "プロジェクトディレクトリでClaude CLIを実行して始めましょう"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "AIコーディングアシスタント"
|
"subtitle": "AIコーディングアシスタント"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -64,12 +64,7 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"rename": "名前の変更",
|
"rename": "名前の変更",
|
||||||
"joinCommunity": "コミュニティに参加",
|
"joinCommunity": "コミュニティに参加"
|
||||||
"reportIssue": "問題を報告",
|
|
||||||
"starOnGithub": "GitHubでスター"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "オープンソース"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "アクティブ",
|
"active": "アクティブ",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "다시 오신 것을 환영합니다",
|
"title": "다시 오신 것을 환영합니다",
|
||||||
"description": "CloudCLI 계정에 로그인하세요",
|
"description": "Claude Code UI 계정에 로그인하세요",
|
||||||
"username": "사용자명",
|
"username": "사용자명",
|
||||||
"password": "비밀번호",
|
"password": "비밀번호",
|
||||||
"submit": "로그인",
|
"submit": "로그인",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "에디터에서 열기"
|
"openInEditor": "에디터에서 열기"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "CloudCLI 로딩 중",
|
"loading": "Claude Code UI 로딩 중",
|
||||||
"settingUpWorkspace": "워크스페이스 설정 중...",
|
"settingUpWorkspace": "워크스페이스 설정 중...",
|
||||||
"chooseProject": "프로젝트 선택",
|
"chooseProject": "프로젝트 선택",
|
||||||
"selectProjectDescription": "사이드바에서 프로젝트를 선택하여 Claude와 코딩을 시작하세요. 각 프로젝트에는 채팅 세션과 파일 히스토리가 포함됩니다.",
|
"selectProjectDescription": "사이드바에서 프로젝트를 선택하여 Claude와 코딩을 시작하세요. 각 프로젝트에는 채팅 세션과 파일 히스토리가 포함됩니다.",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API & 토큰",
|
"apiTokens": "API & 토큰",
|
||||||
"tasks": "작업",
|
"tasks": "작업",
|
||||||
"notifications": "알림",
|
"notifications": "알림",
|
||||||
"plugins": "플러그인",
|
"plugins": "플러그인"
|
||||||
"about": "정보"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "알림",
|
"title": "알림",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "프로젝트 디렉토리에서 Claude CLI를 실행하여 시작하세요"
|
"runClaudeCli": "프로젝트 디렉토리에서 Claude CLI를 실행하여 시작하세요"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "AI 코딩 어시스턴트 UI"
|
"subtitle": "AI 코딩 어시스턴트 UI"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -64,12 +64,7 @@
|
|||||||
"save": "저장",
|
"save": "저장",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"rename": "이름 변경",
|
"rename": "이름 변경",
|
||||||
"joinCommunity": "커뮤니티 참여",
|
"joinCommunity": "커뮤니티 참여"
|
||||||
"reportIssue": "문제 신고",
|
|
||||||
"starOnGithub": "GitHub에서 스타"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "오픈 소스"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "활성",
|
"active": "활성",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Добро пожаловать",
|
"title": "Добро пожаловать",
|
||||||
"description": "Войдите в свой аккаунт CloudCLI",
|
"description": "Войдите в свой аккаунт Claude Code UI",
|
||||||
"username": "Имя пользователя",
|
"username": "Имя пользователя",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"submit": "Войти",
|
"submit": "Войти",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "Открыть в редакторе"
|
"openInEditor": "Открыть в редакторе"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "Загрузка CloudCLI",
|
"loading": "Загрузка Claude Code UI",
|
||||||
"settingUpWorkspace": "Настройка рабочего пространства...",
|
"settingUpWorkspace": "Настройка рабочего пространства...",
|
||||||
"chooseProject": "Выберите проект",
|
"chooseProject": "Выберите проект",
|
||||||
"selectProjectDescription": "Выберите проект на боковой панели, чтобы начать работу с Claude. Каждый проект содержит ваши сеансы чата и историю файлов.",
|
"selectProjectDescription": "Выберите проект на боковой панели, чтобы начать работу с Claude. Каждый проект содержит ваши сеансы чата и историю файлов.",
|
||||||
|
|||||||
@@ -105,8 +105,7 @@
|
|||||||
"git": "Git",
|
"git": "Git",
|
||||||
"apiTokens": "API и токены",
|
"apiTokens": "API и токены",
|
||||||
"tasks": "Задачи",
|
"tasks": "Задачи",
|
||||||
"plugins": "Плагины",
|
"plugins": "Плагины"
|
||||||
"about": "О программе"
|
|
||||||
},
|
},
|
||||||
"appearanceSettings": {
|
"appearanceSettings": {
|
||||||
"darkMode": {
|
"darkMode": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "Запустите Claude CLI в каталоге проекта для начала работы"
|
"runClaudeCli": "Запустите Claude CLI в каталоге проекта для начала работы"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "Интерфейс AI помощника для программирования"
|
"subtitle": "Интерфейс AI помощника для программирования"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,12 +65,7 @@
|
|||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"rename": "Переименовать",
|
"rename": "Переименовать",
|
||||||
"joinCommunity": "Присоединиться к сообществу",
|
"joinCommunity": "Присоединиться к сообществу"
|
||||||
"reportIssue": "Сообщить о проблеме",
|
|
||||||
"starOnGithub": "Звезда на GitHub"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "Открытый исходный код"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "Активен",
|
"active": "Активен",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"login": {
|
"login": {
|
||||||
"title": "欢迎回来",
|
"title": "欢迎回来",
|
||||||
"description": "登录您的 CloudCLI 账户",
|
"description": "登录您的 Claude Code UI 账户",
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"submit": "登录",
|
"submit": "登录",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"openInEditor": "在编辑器中打开"
|
"openInEditor": "在编辑器中打开"
|
||||||
},
|
},
|
||||||
"mainContent": {
|
"mainContent": {
|
||||||
"loading": "正在加载 CloudCLI",
|
"loading": "正在加载 Claude Code UI",
|
||||||
"settingUpWorkspace": "正在设置您的工作空间...",
|
"settingUpWorkspace": "正在设置您的工作空间...",
|
||||||
"chooseProject": "选择您的项目",
|
"chooseProject": "选择您的项目",
|
||||||
"selectProjectDescription": "从侧边栏选择一个项目以开始使用 Claude 进行编程。每个项目包含您的聊天会话和文件历史。",
|
"selectProjectDescription": "从侧边栏选择一个项目以开始使用 Claude 进行编程。每个项目包含您的聊天会话和文件历史。",
|
||||||
|
|||||||
@@ -106,8 +106,8 @@
|
|||||||
"apiTokens": "API 和令牌",
|
"apiTokens": "API 和令牌",
|
||||||
"tasks": "任务",
|
"tasks": "任务",
|
||||||
"notifications": "通知",
|
"notifications": "通知",
|
||||||
"plugins": "插件",
|
"plugins": "插件"
|
||||||
"about": "关于"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"title": "通知",
|
"title": "通知",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用"
|
"runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "CloudCLI",
|
"title": "Claude Code UI",
|
||||||
"subtitle": "AI 编程助手"
|
"subtitle": "AI 编程助手"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
@@ -65,12 +65,7 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"joinCommunity": "加入社区",
|
"joinCommunity": "加入社区"
|
||||||
"reportIssue": "报告问题",
|
|
||||||
"starOnGithub": "在GitHub上加星"
|
|
||||||
},
|
|
||||||
"branding": {
|
|
||||||
"openSource": "开源"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"active": "活动",
|
"active": "活动",
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export interface NormalizedMessage {
|
|||||||
toolResult?: { content: string; isError: boolean; toolUseResult?: unknown } | null;
|
toolResult?: { content: string; isError: boolean; toolUseResult?: unknown } | null;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
tokens?: number;
|
||||||
canInterrupt?: boolean;
|
canInterrupt?: boolean;
|
||||||
|
tokenBudget?: unknown;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
input?: unknown;
|
input?: unknown;
|
||||||
context?: unknown;
|
context?: unknown;
|
||||||
@@ -79,6 +81,7 @@ export interface SessionSlot {
|
|||||||
total: number;
|
total: number;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
tokenUsage: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY: NormalizedMessage[] = [];
|
const EMPTY: NormalizedMessage[] = [];
|
||||||
@@ -95,6 +98,7 @@ function createEmptySlot(): SessionSlot {
|
|||||||
total: 0,
|
total: 0,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
tokenUsage: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +208,9 @@ export function useSessionStore() {
|
|||||||
slot.fetchedAt = Date.now();
|
slot.fetchedAt = Date.now();
|
||||||
slot.status = 'idle';
|
slot.status = 'idle';
|
||||||
recomputeMergedIfNeeded(slot);
|
recomputeMergedIfNeeded(slot);
|
||||||
|
if (data.tokenUsage) {
|
||||||
|
slot.tokenUsage = data.tokenUsage;
|
||||||
|
}
|
||||||
|
|
||||||
notify(sessionId);
|
notify(sessionId);
|
||||||
return slot;
|
return slot;
|
||||||
|
|||||||
Reference in New Issue
Block a user