diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js index fad05f9e..f49d8a38 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -7,19 +7,25 @@ import BaseUrlSelect from "./BaseUrlSelect"; import { rememberEndpoint } from "./cliEndpointPresets"; import ApiKeySelect from "./ApiKeySelect"; import { matchKnownEndpoint } from "./cliEndpointMatch"; +import { stripModelContextMarker } from "open-sse/utils/modelMarkers.js"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; -// Context window presets. UI shows the round number; the value written is nudged -// down 2K to stay safely under the upstream hard cap. +// Auto-compact window presets (CLAUDE_CODE_AUTO_COMPACT_WINDOW, valid 100K–1M). +// UI shows the round number; the value written is nudged down 2K to stay safely +// under the upstream hard cap. const CONTEXT_OPTIONS = [ { label: "Default", value: "" }, { label: "200K", value: "198000" }, { label: "300K", value: "298000" }, { label: "500K", value: "498000" }, - { label: "1M", value: "998000" }, + { label: "700K", value: "698000" }, ]; +// Claude Code assumes a model's window is 200K unless the name carries the `[1m]` +// marker, which is why the 1M auto-compact preset only takes effect once the +// marker is applied. + export default function ClaudeToolCard({ tool, isExpanded, @@ -51,9 +57,28 @@ export default function ClaudeToolCard({ const [customBaseUrl, setCustomBaseUrl] = useState(""); const [ccFilterNaming, setCcFilterNaming] = useState(false); const [exaMcpEnabled, setExaMcpEnabled] = useState(false); - const [maxContextTokens, setMaxContextTokens] = useState(""); + const [autoCompactWindow, setAutoCompactWindow] = useState(""); + const [oneMContext, setOneMContext] = useState(false); const hasInitializedModels = useRef(false); + // Claude Code only string-matches the marker against the model name, so it + // applies to any id — the user decides which models are worth declaring as 1M. + // Stripping first keeps repeated toggles from stacking `[1m][1m]`. + const withContextMarker = (value, enabled) => { + const { model } = stripModelContextMarker(value); + return enabled ? `${model}[1m]` : model; + }; + + // Rewrite the mappings in place on toggle, so the inputs show what will be + // written without waiting for Apply. + const handleOneMContextToggle = (enabled) => { + setOneMContext(enabled); + tool.defaultModels.forEach((model) => { + const current = modelMappings[model.alias]; + if (current) onModelMappingChange(model.alias, withContextMarker(current, enabled)); + }); + }; + const currentBaseUrl = claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL || ""; const getConfigStatus = () => { @@ -80,9 +105,15 @@ export default function ClaudeToolCard({ }, [initialStatus]); useEffect(() => { - const v = claudeStatus?.settings?.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS; - setMaxContextTokens(v || ""); - }, [claudeStatus?.settings?.env?.CLAUDE_CODE_MAX_CONTEXT_TOKENS]); + const v = claudeStatus?.settings?.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW; + setAutoCompactWindow(v || ""); + }, [claudeStatus?.settings?.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW]); + + useEffect(() => { + const env = claudeStatus?.settings?.env; + if (!env) return; + setOneMContext(tool.defaultModels.some((model) => env[model.envKey]?.endsWith("[1m]"))); + }, [claudeStatus?.settings?.env, tool.defaultModels]); useEffect(() => { if (isExpanded) { @@ -124,6 +155,8 @@ export default function ClaudeToolCard({ tool.defaultModels.forEach((model) => { if (model.envKey) { + // Kept verbatim (marker included) so the input matches what is on disk; + // withContextMarker strips before appending, so re-applying cannot double it. const value = env[model.envKey] || model.defaultValue || ""; // Only sync initial values from file once if (value) { @@ -180,15 +213,17 @@ export default function ClaudeToolCard({ tool.defaultModels.forEach((model) => { const targetModel = modelMappings[model.alias]; + // Written verbatim — the input may hold a marker typed by hand, and the + // toggle already decided the marker when it was flipped. if (targetModel && model.envKey) env[model.envKey] = targetModel; }); - if (maxContextTokens) { - env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = maxContextTokens; + if (autoCompactWindow) { + env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = autoCompactWindow; } const res = await fetch("/api/cli-tools/claude-settings", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ env, exaMcpEnabled, maxContextTokens }), + body: JSON.stringify({ env, exaMcpEnabled, autoCompactWindow }), }); const data = await res.json(); if (res.ok) { @@ -217,7 +252,8 @@ export default function ClaudeToolCard({ tool.defaultModels.forEach((model) => onModelMappingChange(model.alias, model.defaultValue || "")); setSelectedApiKey(""); setExaMcpEnabled(false); - setMaxContextTokens(""); + setAutoCompactWindow(""); + setOneMContext(false); } else { setMessage({ type: "error", text: data.error || "Failed to reset settings" }); } @@ -247,8 +283,8 @@ export default function ClaudeToolCard({ const targetModel = modelMappings[model.alias]; if (targetModel && model.envKey) env[model.envKey] = targetModel; }); - if (maxContextTokens) { - env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = maxContextTokens; + if (autoCompactWindow) { + env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = autoCompactWindow; } return [ @@ -374,17 +410,30 @@ export default function ClaudeToolCard({ ))} - {/* Context Window */} + {/* Auto-compact window */}