import { feature } from 'bun:bundle';
import * as React from 'react';
import { useSyncExternalStore } from 'react';
import { Box, Text } from '@anthropic/ink';
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
import {
calculateTokenWarningState,
getEffectiveContextWindowSize,
isAutoCompactEnabled,
} from '../services/compact/autoCompact.js';
import { useCompactWarningSuppression } from '../services/compact/compactWarningHook.js';
import { getUpgradeMessage } from '../utils/model/contextWindowUpgradeCheck.js';
type Props = {
tokenUsage: number;
model: string;
};
/**
* Live collapse progress: "x / y summarized". Sub-component so
* useSyncExternalStore can subscribe to store mutations unconditionally
* (hooks-in-conditionals would violate React rules). The parent only
* renders this when feature('CONTEXT_COLLAPSE') + isContextCollapseEnabled().
*/
function CollapseLabel({ upgradeMessage }: { upgradeMessage: string | null }): React.ReactNode {
/* eslint-disable @typescript-eslint/no-require-imports */
const { getStats, subscribe } =
require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js');
/* eslint-enable @typescript-eslint/no-require-imports */
// Snapshot must be referentially stable across calls when the
// underlying counts haven't changed — returning a fresh object every
// time would infinite-loop useSyncExternalStore. Encode as a string.
const snapshot = useSyncExternalStore(subscribe, () => {
const s = getStats();
const idleWarn = s.health.emptySpawnWarningEmitted ? 1 : 0;
return `${s.collapsedSpans}|${s.stagedSpans}|${s.health.totalErrors}|${s.health.totalEmptySpawns}|${idleWarn}`;
});
const [collapsed, staged, errors, emptySpawns, idleWarn] = snapshot.split('|').map(Number) as [
number,
number,
number,
number,
number,
];
const total = collapsed + staged;
// Show error indicator when ctx-agent is failing silently
if (errors > 0 || idleWarn) {
const problem = errors > 0 ? `collapse errors: ${errors}` : `collapse idle (${emptySpawns} empty runs)`;
return (
{total > 0 ? `${collapsed} / ${total} summarized \u00b7 ${problem}` : problem}
);
}
if (total === 0) return null;
const label = `${collapsed} / ${total} summarized`;
return (
{upgradeMessage ? `${label} \u00b7 ${upgradeMessage}` : label}
);
}
export function TokenWarning({ tokenUsage, model }: Props): React.ReactNode {
const { percentLeft, isAboveWarningThreshold, isAboveErrorThreshold } = calculateTokenWarningState(tokenUsage, model);
// Use reactive hook to check if warning should be suppressed
const suppressWarning = useCompactWarningSuppression();
if (!isAboveWarningThreshold || suppressWarning) {
return null;
}
const showAutoCompactWarning = isAutoCompactEnabled();
const upgradeMessage = getUpgradeMessage('warning');
// Reactive-only or context-collapse mode: proactive autocompact never
// fires, so percentLeft's normal calculation (against the autocompact
// threshold) counts down to an event that won't happen. Recompute
// against the effective window so the percentage is honest.
//
// Each feature() block stands alone so the flag strings DCE from
// external builds independently.
let displayPercentLeft = percentLeft;
let reactiveOnlyMode = false;
let collapseMode = false;
if (feature('REACTIVE_COMPACT')) {
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
reactiveOnlyMode = true;
}
}
if (feature('CONTEXT_COLLAPSE')) {
/* eslint-disable @typescript-eslint/no-require-imports */
const { isContextCollapseEnabled } =
require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js');
/* eslint-enable @typescript-eslint/no-require-imports */
if (isContextCollapseEnabled()) {
collapseMode = true;
}
}
if (reactiveOnlyMode || collapseMode) {
const effectiveWindow = getEffectiveContextWindowSize(model);
displayPercentLeft = Math.max(0, Math.round(((effectiveWindow - tokenUsage) / effectiveWindow) * 100));
}
// Collapse mode: delegate to the subscribing sub-component so the
// indicator updates live as the ctx-agent stages and commits fire, not
// just when the next API response re-renders TokenWarning.
if (collapseMode && feature('CONTEXT_COLLAPSE')) {
return (
);
}
const autocompactLabel = reactiveOnlyMode
? `${100 - displayPercentLeft}% context used`
: `${displayPercentLeft}% until auto-compact`;
return (
{showAutoCompactWarning ? (
{upgradeMessage ? `${autocompactLabel} \u00b7 ${upgradeMessage}` : autocompactLabel}
) : (
{upgradeMessage
? `Context low (${percentLeft}% remaining) \u00b7 ${upgradeMessage}`
: `Context low (${percentLeft}% remaining) \u00b7 Run /compact to compact & continue`}
)}
);
}