Compare commits

..

2 Commits

Author SHA1 Message Date
Haileyesus
cccc1ad268 fix(chat): restrict thinking prefix to claude 2026-06-04 16:35:13 +03:00
Haileyesus
c825d342b3 fix(chat): persist thinking mode selection
Initialize the composer thinking mode from localStorage so reloads keep the user's

selected mode instead of falling back to Standard.

Keep the chosen mode after sending because the selector behaves like a

preference, not a one-shot modifier for a single prompt.
2026-06-04 16:15:02 +03:00
11 changed files with 39 additions and 91 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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
}; };
} }
} }

View File

@@ -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,
}, },
} }

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}>

View File

@@ -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) {

View File

@@ -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
} }
} }
}, },