From 0a0a49dbb1e5e1f177b9e28173d7b8951711b9db Mon Sep 17 00:00:00 2001 From: Rutger de Knijf Date: Wed, 18 Mar 2026 21:28:31 +0100 Subject: [PATCH] feat: add ModelIntelligence and ContextWindowSize widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new context-awareness widgets: - **ContextWindowSize**: Displays the total context window size (e.g. "200k", "1.0M") based on the active model, using the existing model-context and context-window utilities. - **ModelIntelligence**: Shows a Model Intelligence (MI) score from 1.000 to 0.000 that reflects estimated model quality as the context window fills. Uses the formula MI = max(0, 1 - u^β) with per-model β values (Opus=1.8, Sonnet=1.5, Haiku=1.2), calibrated against Anthropic's MRCR v2 retrieval benchmark. MI concept and formula credit: luongnv89/cc-context-stats https://github.com/luongnv89/cc-context-stats Both widgets support raw-value mode and color customization. --- src/utils/widget-manifest.ts | 4 +- src/widgets/ContextWindowSize.ts | 44 +++++++++++++++++ src/widgets/ModelIntelligence.ts | 84 ++++++++++++++++++++++++++++++++ src/widgets/index.ts | 4 +- 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/widgets/ContextWindowSize.ts create mode 100644 src/widgets/ModelIntelligence.ts diff --git a/src/utils/widget-manifest.ts b/src/utils/widget-manifest.ts index 890abd9f..1af2d6f3 100644 --- a/src/utils/widget-manifest.ts +++ b/src/utils/widget-manifest.ts @@ -54,7 +54,9 @@ export const WIDGET_MANIFEST: WidgetManifestEntry[] = [ { type: 'context-bar', create: () => new widgets.ContextBarWidget() }, { type: 'skills', create: () => new widgets.SkillsWidget() }, { type: 'thinking-effort', create: () => new widgets.ThinkingEffortWidget() }, - { type: 'vim-mode', create: () => new widgets.VimModeWidget() } + { type: 'vim-mode', create: () => new widgets.VimModeWidget() }, + { type: 'model-intelligence', create: () => new widgets.ModelIntelligenceWidget() }, + { type: 'context-window-size', create: () => new widgets.ContextWindowSizeWidget() } ]; export const LAYOUT_WIDGET_MANIFEST: LayoutWidgetManifestEntry[] = [ diff --git a/src/widgets/ContextWindowSize.ts b/src/widgets/ContextWindowSize.ts new file mode 100644 index 00000000..4d3f1eff --- /dev/null +++ b/src/widgets/ContextWindowSize.ts @@ -0,0 +1,44 @@ +import type { RenderContext } from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + Widget, + WidgetEditorDisplay, + WidgetItem +} from '../types/Widget'; +import { getContextWindowMetrics } from '../utils/context-window'; +import { + getContextConfig, + getModelContextIdentifier +} from '../utils/model-context'; +import { formatTokens } from '../utils/renderer'; + +import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; + +export class ContextWindowSizeWidget implements Widget { + getDefaultColor(): string { return 'brightBlack'; } + getDescription(): string { return 'Shows the total context window size (e.g. 200k, 1.0M)'; } + getDisplayName(): string { return 'Context Window'; } + getCategory(): string { return 'Context'; } + getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { + return { displayText: this.getDisplayName() }; + } + + render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + if (context.isPreview) { + return formatRawOrLabeledValue(item, 'Win: ', '200.0k'); + } + + const metrics = getContextWindowMetrics(context.data); + + if (metrics.windowSize !== null) { + return formatRawOrLabeledValue(item, 'Win: ', formatTokens(metrics.windowSize)); + } + + const modelIdentifier = getModelContextIdentifier(context.data?.model); + const contextConfig = getContextConfig(modelIdentifier, null); + return formatRawOrLabeledValue(item, 'Win: ', formatTokens(contextConfig.maxTokens)); + } + + supportsRawValue(): boolean { return true; } + supportsColors(item: WidgetItem): boolean { return true; } +} \ No newline at end of file diff --git a/src/widgets/ModelIntelligence.ts b/src/widgets/ModelIntelligence.ts new file mode 100644 index 00000000..7f5cb4c8 --- /dev/null +++ b/src/widgets/ModelIntelligence.ts @@ -0,0 +1,84 @@ +import type { RenderContext } from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + Widget, + WidgetEditorDisplay, + WidgetItem +} from '../types/Widget'; +import { getContextWindowMetrics } from '../utils/context-window'; +import { + getContextConfig, + getModelContextIdentifier +} from '../utils/model-context'; + +import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; + +function getModelBeta(modelIdentifier?: string): number { + if (!modelIdentifier) { + return 1.5; + } + + const lower = modelIdentifier.toLowerCase(); + + if (lower.includes('opus')) { + return 1.8; + } + + if (lower.includes('haiku')) { + return 1.2; + } + + return 1.5; +} + +function calculateMI(usageRatio: number, beta: number): number { + const clamped = Math.max(0, Math.min(1, usageRatio)); + return Math.max(0, 1 - clamped ** beta); +} + +function getUsageRatio(context: RenderContext): number | null { + const metrics = getContextWindowMetrics(context.data); + + if (metrics.usedPercentage !== null) { + return metrics.usedPercentage / 100; + } + + if (context.tokenMetrics) { + const modelIdentifier = getModelContextIdentifier(context.data?.model); + const contextConfig = getContextConfig(modelIdentifier, metrics.windowSize); + return Math.min(1, context.tokenMetrics.contextLength / contextConfig.maxTokens); + } + + return null; +} + +export class ModelIntelligenceWidget implements Widget { + getDefaultColor(): string { return 'cyan'; } + getDescription(): string { return 'Model Intelligence score based on context fill level'; } + getDisplayName(): string { return 'Model Intelligence'; } + getCategory(): string { return 'Context'; } + getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { + return { displayText: this.getDisplayName() }; + } + + render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + const modelIdentifier = getModelContextIdentifier(context.data?.model); + const beta = getModelBeta(modelIdentifier); + + if (context.isPreview) { + const previewMI = calculateMI(0.093, beta); + return formatRawOrLabeledValue(item, 'MI: ', previewMI.toFixed(3)); + } + + const usageRatio = getUsageRatio(context); + if (usageRatio === null) { + return null; + } + + const mi = calculateMI(usageRatio, beta); + return formatRawOrLabeledValue(item, 'MI: ', mi.toFixed(3)); + } + + supportsRawValue(): boolean { return true; } + supportsColors(item: WidgetItem): boolean { return true; } +} \ No newline at end of file diff --git a/src/widgets/index.ts b/src/widgets/index.ts index 74d92bce..12954a19 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -35,4 +35,6 @@ export { ContextBarWidget } from './ContextBar'; export { LinkWidget } from './Link'; export { SkillsWidget } from './Skills'; export { ThinkingEffortWidget } from './ThinkingEffort'; -export { VimModeWidget } from './VimMode'; \ No newline at end of file +export { VimModeWidget } from './VimMode'; +export { ModelIntelligenceWidget } from './ModelIntelligence'; +export { ContextWindowSizeWidget } from './ContextWindowSize'; \ No newline at end of file