mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-06 13:15:38 +08:00
Compare commits
1 Commits
fix/claude
...
chore/use-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfd576ae7 |
@@ -5,6 +5,9 @@
|
|||||||
<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" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<title>CloudCLI UI</title>
|
<title>CloudCLI UI</title>
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const CLAUDE_MODELS = {
|
|||||||
{
|
{
|
||||||
value: "default",
|
value: "default",
|
||||||
label: "Default (recommended)",
|
label: "Default (recommended)",
|
||||||
description: "Use the default model (currently Opus 4.8 (1M context)) · $5/$25 per Mtok",
|
description: "Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "sonnet",
|
value: "sonnet",
|
||||||
|
|||||||
@@ -304,11 +304,7 @@ function extractTokenBudget(sdkMessage) {
|
|||||||
|
|
||||||
const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
|
const messageUsage = sdkMessage.message?.usage || sdkMessage.usage;
|
||||||
if (messageUsage && typeof messageUsage === 'object') {
|
if (messageUsage && typeof messageUsage === 'object') {
|
||||||
const directInputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
|
const inputTokens = readNumber(messageUsage.input_tokens ?? messageUsage.inputTokens);
|
||||||
const cacheCreationTokens = readNumber(messageUsage.cache_creation_input_tokens ?? messageUsage.cacheCreationInputTokens ?? messageUsage.cacheCreationTokens);
|
|
||||||
const cacheReadTokens = readNumber(messageUsage.cache_read_input_tokens ?? messageUsage.cacheReadInputTokens ?? messageUsage.cacheReadTokens);
|
|
||||||
const cacheTokens = cacheCreationTokens + cacheReadTokens;
|
|
||||||
const inputTokens = directInputTokens + cacheTokens;
|
|
||||||
const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
|
const outputTokens = readNumber(messageUsage.output_tokens ?? messageUsage.outputTokens);
|
||||||
const totalUsed = inputTokens + outputTokens;
|
const totalUsed = inputTokens + outputTokens;
|
||||||
const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
|
const contextWindow = parseInt(process.env.CONTEXT_WINDOW, 10) || 160000;
|
||||||
@@ -318,9 +314,6 @@ function extractTokenBudget(sdkMessage) {
|
|||||||
total: contextWindow,
|
total: contextWindow,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheReadTokens,
|
|
||||||
cacheCreationTokens,
|
|
||||||
cacheTokens,
|
|
||||||
breakdown: {
|
breakdown: {
|
||||||
input: inputTokens,
|
input: inputTokens,
|
||||||
output: outputTokens,
|
output: outputTokens,
|
||||||
|
|||||||
@@ -87,11 +87,6 @@ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|||||||
|
|
||||||
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
||||||
|
|
||||||
function readUsageNumber(value) {
|
|
||||||
const parsed = Number(value);
|
|
||||||
return Number.isFinite(parsed) ? parsed : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
@@ -1391,8 +1386,6 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|||||||
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
||||||
let inputTokens = 0;
|
let inputTokens = 0;
|
||||||
let outputTokens = 0;
|
let outputTokens = 0;
|
||||||
let cacheReadTokens = 0;
|
|
||||||
let cacheCreationTokens = 0;
|
|
||||||
|
|
||||||
// Find the latest assistant message with usage data (scan from end)
|
// Find the latest assistant message with usage data (scan from end)
|
||||||
for (let i = lines.length - 1; i >= 0; i--) {
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
@@ -1404,11 +1397,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|||||||
const usage = entry.message.usage;
|
const usage = entry.message.usage;
|
||||||
|
|
||||||
// Use token counts from latest assistant message only
|
// Use token counts from latest assistant message only
|
||||||
const directInputTokens = readUsageNumber(usage.input_tokens ?? usage.inputTokens);
|
inputTokens = usage.input_tokens || 0;
|
||||||
cacheReadTokens = readUsageNumber(usage.cache_read_input_tokens ?? usage.cacheReadInputTokens ?? usage.cacheReadTokens);
|
outputTokens = usage.output_tokens || 0;
|
||||||
cacheCreationTokens = readUsageNumber(usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens ?? usage.cacheCreationTokens);
|
|
||||||
inputTokens = directInputTokens + cacheReadTokens + cacheCreationTokens;
|
|
||||||
outputTokens = readUsageNumber(usage.output_tokens ?? usage.outputTokens);
|
|
||||||
|
|
||||||
break; // Stop after finding the latest assistant message
|
break; // Stop after finding the latest assistant message
|
||||||
}
|
}
|
||||||
@@ -1419,16 +1409,12 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalUsed = inputTokens + outputTokens;
|
const totalUsed = inputTokens + outputTokens;
|
||||||
const cacheTokens = cacheReadTokens + cacheCreationTokens;
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
used: totalUsed,
|
used: totalUsed,
|
||||||
total: contextWindow,
|
total: contextWindow,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheReadTokens,
|
|
||||||
cacheCreationTokens,
|
|
||||||
cacheTokens,
|
|
||||||
breakdown: {
|
breakdown: {
|
||||||
input: inputTokens,
|
input: inputTokens,
|
||||||
output: outputTokens
|
output: outputTokens
|
||||||
|
|||||||
@@ -31,24 +31,6 @@ export function createWebSocketServer(
|
|||||||
});
|
});
|
||||||
|
|
||||||
wss.on('connection', (ws, request) => {
|
wss.on('connection', (ws, request) => {
|
||||||
// Keep WebSocket alive across reverse-proxy idle timeouts (Cloudflare ~100s,
|
|
||||||
// AWS ALB 60s, nginx 60s, etc.). Without app-level pings these connections
|
|
||||||
// are silently torn down even when the UI is active, causing repeated
|
|
||||||
// reconnect cycles. ws library heartbeat is opt-in.
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
||||||
const heartbeat = setInterval(() => {
|
|
||||||
if (ws.readyState === ws.OPEN) {
|
|
||||||
try {
|
|
||||||
ws.ping();
|
|
||||||
} catch {
|
|
||||||
// socket may have been closed concurrently — interval will be cleared below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, HEARTBEAT_INTERVAL_MS);
|
|
||||||
const stopHeartbeat = () => clearInterval(heartbeat);
|
|
||||||
ws.on('close', stopHeartbeat);
|
|
||||||
ws.on('error', stopHeartbeat);
|
|
||||||
|
|
||||||
const incomingRequest = request as AuthenticatedWebSocketRequest;
|
const incomingRequest = request as AuthenticatedWebSocketRequest;
|
||||||
const url = incomingRequest.url ?? '/';
|
const url = incomingRequest.url ?? '/';
|
||||||
const pathname = new URL(url, 'http://localhost').pathname;
|
const pathname = new URL(url, 'http://localhost').pathname;
|
||||||
|
|||||||
@@ -592,14 +592,12 @@ class ResponseCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputTokens = totalInput + totalCacheRead + totalCacheCreation;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputTokens,
|
inputTokens: totalInput,
|
||||||
outputTokens: totalOutput,
|
outputTokens: totalOutput,
|
||||||
cacheReadTokens: totalCacheRead,
|
cacheReadTokens: totalCacheRead,
|
||||||
cacheCreationTokens: totalCacheCreation,
|
cacheCreationTokens: totalCacheCreation,
|
||||||
totalTokens: inputTokens + totalOutput
|
totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,35 +268,16 @@ Custom commands can be created in:
|
|||||||
tokenUsage.contextWindow ??
|
tokenUsage.contextWindow ??
|
||||||
0,
|
0,
|
||||||
) || 0;
|
) || 0;
|
||||||
const normalizedInputValue =
|
const inputTokensRaw =
|
||||||
tokenUsage.inputTokens ??
|
|
||||||
tokenUsage.input ??
|
|
||||||
tokenUsage.cumulativeInputTokens ??
|
|
||||||
tokenUsage.breakdown?.input ??
|
|
||||||
tokenUsage.promptTokens;
|
|
||||||
const directInputTokens =
|
|
||||||
Number(
|
Number(
|
||||||
normalizedInputValue ??
|
tokenUsage.inputTokens ??
|
||||||
|
tokenUsage.input ??
|
||||||
tokenUsage.input_tokens ??
|
tokenUsage.input_tokens ??
|
||||||
0
|
tokenUsage.cumulativeInputTokens ??
|
||||||
) || 0;
|
tokenUsage.breakdown?.input ??
|
||||||
const cacheReadTokens =
|
tokenUsage.promptTokens ??
|
||||||
Number(
|
|
||||||
tokenUsage.cacheReadTokens ??
|
|
||||||
tokenUsage.cache_read_input_tokens ??
|
|
||||||
tokenUsage.cacheReadInputTokens ??
|
|
||||||
0,
|
0,
|
||||||
) || 0;
|
) || 0;
|
||||||
const cacheCreationTokens =
|
|
||||||
Number(
|
|
||||||
tokenUsage.cacheCreationTokens ??
|
|
||||||
tokenUsage.cache_creation_input_tokens ??
|
|
||||||
tokenUsage.cacheCreationInputTokens ??
|
|
||||||
0,
|
|
||||||
) || 0;
|
|
||||||
const inputTokens = normalizedInputValue == null
|
|
||||||
? directInputTokens + cacheReadTokens + cacheCreationTokens
|
|
||||||
: directInputTokens;
|
|
||||||
const outputTokens =
|
const outputTokens =
|
||||||
Number(
|
Number(
|
||||||
tokenUsage.outputTokens ??
|
tokenUsage.outputTokens ??
|
||||||
@@ -307,9 +288,8 @@ Custom commands can be created in:
|
|||||||
tokenUsage.completionTokens ??
|
tokenUsage.completionTokens ??
|
||||||
0,
|
0,
|
||||||
) || 0;
|
) || 0;
|
||||||
const computedUsed = inputTokens + outputTokens;
|
const hasTokenBreakdown = inputTokensRaw > 0 || outputTokens > 0;
|
||||||
const hasTokenBreakdown = computedUsed > 0;
|
const used = reportedUsed || inputTokensRaw + outputTokens;
|
||||||
const used = Math.max(reportedUsed, computedUsed);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "builtin",
|
type: "builtin",
|
||||||
@@ -322,7 +302,7 @@ Custom commands can be created in:
|
|||||||
...(hasTokenBreakdown
|
...(hasTokenBreakdown
|
||||||
? {
|
? {
|
||||||
tokenBreakdown: {
|
tokenBreakdown: {
|
||||||
input: inputTokens,
|
input: inputTokensRaw,
|
||||||
output: outputTokens,
|
output: outputTokens,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,7 +295,6 @@ export default function ChatComposer({
|
|||||||
|
|
||||||
<PromptInputTextarea
|
<PromptInputTextarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
dir="auto"
|
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
onClick={onTextareaClick}
|
onClick={onTextareaClick}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
|
|||||||
/* User message bubble on the right */
|
/* User message bubble on the right */
|
||||||
<div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
|
<div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
|
||||||
<div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
|
<div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
|
||||||
<div dir="auto" className="whitespace-pre-wrap break-words text-sm">
|
<div className="whitespace-pre-wrap break-words text-sm">
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
{message.images && message.images.length > 0 && (
|
{message.images && message.images.length > 0 && (
|
||||||
@@ -405,7 +405,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
|
|||||||
</ReasoningContent>
|
</ReasoningContent>
|
||||||
</Reasoning>
|
</Reasoning>
|
||||||
) : (
|
) : (
|
||||||
<div dir="auto" className="text-sm text-gray-700 dark:text-gray-300">
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{/* Reasoning accordion */}
|
{/* Reasoning accordion */}
|
||||||
{showThinking && message.reasoning && (
|
{showThinking && message.reasoning && (
|
||||||
<Reasoning className="mb-3" defaultOpen={false}>
|
<Reasoning className="mb-3" defaultOpen={false}>
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ const useWebSocketProviderState = (): WebSocketContextType => {
|
|||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// The cleanup below sets unmountedRef = true. Without this reset, every
|
|
||||||
// re-run of the effect (e.g. on token refresh) would short-circuit connect()
|
|
||||||
// at its unmounted guard and leave the socket permanently disconnected.
|
|
||||||
unmountedRef.current = false;
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
'/shell': {
|
'/shell': {
|
||||||
target: `ws://${proxyHost}:${serverPort}`,
|
target: `ws://${proxyHost}:${serverPort}`,
|
||||||
ws: true
|
ws: true
|
||||||
},
|
|
||||||
'/plugin-ws': {
|
|
||||||
target: `ws://${proxyHost}:${serverPort}`,
|
|
||||||
ws: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user