From 17c4cc76877bd1755030a8414f8d0083f48dcccf Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 11 Sep 2026 00:09:15 +0700 Subject: [PATCH] feat(claude-code): drive auto-compact window, add a 1M-context toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Context window" dropdown wrote CLAUDE_CODE_MAX_CONTEXT_TOKENS, which Claude Code ignores for any model it recognizes: its window resolver returns the env value only when the id is unknown to the model table, so every claude-* mapping kept the built-in 200K and the dropdown did nothing. It was never the compaction threshold either. - Replace it with CLAUDE_CODE_AUTO_COMPACT_WINDOW — the documented trigger (100K–1M, clamped to the model window, env beats the autoCompactWindow setting) — and relabel the field Auto-compact. The 1M preset becomes 700K, which no longer collides with the marker it depends on. - Add a "1M context" checkbox that appends the `[1m]` marker to the ANTHROPIC_DEFAULT_*_MODEL envs. Claude Code assumes 200K unless the name carries the marker — the resolver is a plain /\[1m\]/i test on the string, so it applies to any id and no model lookup is involved; the user decides which models are worth declaring as 1M. - Toggling rewrites the model inputs immediately, and Apply writes them verbatim, so a marker typed by hand is not stripped. Rename maxContextTokens -> autoCompactWindow through the POST body and RESET_ENV_KEYS so a reset clears the key actually written. Co-Authored-By: Claude Code --- .../cli-tools/components/ClaudeToolCard.js | 81 +++++++++++++++---- .../api/cli-tools/claude-settings/route.js | 15 ++-- 2 files changed, 73 insertions(+), 23 deletions(-) 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 */}
- Context window + Auto-compact arrow_forward - setAutoCompactWindow(e.target.value)} className="w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"> {CONTEXT_OPTIONS.map((opt) => ( ))}
+ {/* 1M context */} +
+ 1M context + arrow_forward + +
+ {/* CC Filter Naming */}
Filter naming diff --git a/src/app/api/cli-tools/claude-settings/route.js b/src/app/api/cli-tools/claude-settings/route.js index 76ba9232..c7087577 100644 --- a/src/app/api/cli-tools/claude-settings/route.js +++ b/src/app/api/cli-tools/claude-settings/route.js @@ -123,7 +123,7 @@ export async function GET() { // POST - Backup old fields and write new settings export async function POST(request) { try { - const { env, exaMcpEnabled, maxContextTokens } = await request.json(); + const { env, exaMcpEnabled, autoCompactWindow } = await request.json(); if (!env || typeof env !== "object") { return NextResponse.json( @@ -166,12 +166,13 @@ export async function POST(request) { }, }; - // CLAUDE_CODE_MAX_CONTEXT_TOKENS — only set when a concrete value is chosen; - // "Default" removes the key so Claude Code falls back to the model's window. - if (maxContextTokens) { - newSettings.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(maxContextTokens); + // CLAUDE_CODE_AUTO_COMPACT_WINDOW — the token threshold that triggers + // auto-compact. Only set when a concrete value is chosen; "Default" removes + // the key so Claude Code derives the window from the model. + if (autoCompactWindow) { + newSettings.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = String(autoCompactWindow); } else { - delete newSettings.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS; + delete newSettings.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW; } // Write new settings @@ -203,7 +204,7 @@ const RESET_ENV_KEYS = [ "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "API_TIMEOUT_MS", - "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + "CLAUDE_CODE_AUTO_COMPACT_WINDOW", ]; // DELETE - Reset settings (remove env fields)