mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-06 21:25:34 +08:00
Compare commits
2 Commits
fix/claude
...
fix/chat-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cccc1ad268 | ||
|
|
c825d342b3 |
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,21 @@ const createFakeSubmitEvent = () => {
|
|||||||
return { preventDefault: () => undefined } as unknown as FormEvent<HTMLFormElement>;
|
return { preventDefault: () => undefined } as unknown as FormEvent<HTMLFormElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const THINKING_MODE_STORAGE_KEY = 'chat-thinking-mode';
|
||||||
|
|
||||||
|
const getInitialThinkingMode = () => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedMode = safeLocalStorage.getItem(THINKING_MODE_STORAGE_KEY);
|
||||||
|
if (!savedMode) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return thinkingModes.some((mode) => mode.id === savedMode) ? savedMode : 'none';
|
||||||
|
};
|
||||||
|
|
||||||
const getNotificationSessionSummary = (
|
const getNotificationSessionSummary = (
|
||||||
selectedSession: ProjectSession | null,
|
selectedSession: ProjectSession | null,
|
||||||
fallbackInput: string,
|
fallbackInput: string,
|
||||||
@@ -204,7 +219,7 @@ export function useChatComposerState({
|
|||||||
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
const [uploadingImages, setUploadingImages] = useState<Map<string, number>>(new Map());
|
||||||
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
const [imageErrors, setImageErrors] = useState<Map<string, string>>(new Map());
|
||||||
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
||||||
const [thinkingMode, setThinkingMode] = useState('none');
|
const [thinkingMode, setThinkingMode] = useState(getInitialThinkingMode);
|
||||||
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
const [commandModalPayload, setCommandModalPayload] = useState<CommandModalPayload | null>(null);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -564,7 +579,7 @@ export function useChatComposerState({
|
|||||||
|
|
||||||
let messageContent = currentInput;
|
let messageContent = currentInput;
|
||||||
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
|
||||||
if (selectedThinkingMode && selectedThinkingMode.prefix) {
|
if (provider === 'claude' && selectedThinkingMode && selectedThinkingMode.prefix) {
|
||||||
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,7 +764,6 @@ export function useChatComposerState({
|
|||||||
setUploadingImages(new Map());
|
setUploadingImages(new Map());
|
||||||
setImageErrors(new Map());
|
setImageErrors(new Map());
|
||||||
setIsTextareaExpanded(false);
|
setIsTextareaExpanded(false);
|
||||||
setThinkingMode('none');
|
|
||||||
|
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'auto';
|
textareaRef.current.style.height = 'auto';
|
||||||
@@ -795,6 +809,10 @@ export function useChatComposerState({
|
|||||||
inputValueRef.current = input;
|
inputValueRef.current = input;
|
||||||
}, [input]);
|
}, [input]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
safeLocalStorage.setItem(THINKING_MODE_STORAGE_KEY, thinkingMode);
|
||||||
|
}, [thinkingMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProjectId) {
|
if (!selectedProjectId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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,12 +36,8 @@ 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 () => {
|
||||||
unmountedRef.current = true;
|
unmountedRef.current = true;
|
||||||
if (reconnectTimeoutRef.current) {
|
if (reconnectTimeoutRef.current) {
|
||||||
|
|||||||
@@ -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