diff --git a/src/config.ts b/src/config.ts index d9f4258..51355d1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -90,6 +90,7 @@ export interface HudConfig { sevenDayThreshold: number; environmentThreshold: number; modelFormat: ModelFormatMode; + modelOverride: string; customLine: string; }; colors: HudColorOverrides; @@ -128,6 +129,7 @@ export const DEFAULT_CONFIG: HudConfig = { sevenDayThreshold: 80, environmentThreshold: 0, modelFormat: 'full', + modelOverride: '', customLine: '', }, colors: { @@ -340,6 +342,9 @@ export function mergeConfig(userConfig: Partial): HudConfig { modelFormat: validateModelFormat(migrated.display?.modelFormat) ? migrated.display.modelFormat : DEFAULT_CONFIG.display.modelFormat, + modelOverride: typeof migrated.display?.modelOverride === 'string' + ? migrated.display.modelOverride.slice(0, 80) + : DEFAULT_CONFIG.display.modelOverride, customLine: typeof migrated.display?.customLine === 'string' ? migrated.display.customLine.slice(0, 80) : DEFAULT_CONFIG.display.customLine, diff --git a/src/render/lines/project.ts b/src/render/lines/project.ts index be914d6..dcad13b 100644 --- a/src/render/lines/project.ts +++ b/src/render/lines/project.ts @@ -9,7 +9,7 @@ export function renderProjectLine(ctx: RenderContext): string | null { const parts: string[] = []; if (display?.showModel !== false) { - const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat); + const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat, ctx.config?.display?.modelOverride); const providerLabel = getProviderLabel(ctx.stdin); const showUsage = display?.showUsage !== false; const hasApiKey = !!process.env.ANTHROPIC_API_KEY; diff --git a/src/render/session-line.ts b/src/render/session-line.ts index 572306c..c1a1ef6 100644 --- a/src/render/session-line.ts +++ b/src/render/session-line.ts @@ -12,7 +12,7 @@ const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === * Used for compact layout mode. */ export function renderSessionLine(ctx: RenderContext): string { - const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat); + const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat, ctx.config?.display?.modelOverride); const rawPercent = getContextPercent(ctx.stdin); const bufferedPercent = getBufferedPercent(ctx.stdin); diff --git a/src/stdin.ts b/src/stdin.ts index 470a851..44ddc0e 100644 --- a/src/stdin.ts +++ b/src/stdin.ts @@ -167,13 +167,20 @@ export function stripContextSuffix(name: string): string { } /** - * Formats a model name according to the user's chosen display format. + * Formats a model name according to the user's chosen display settings. + * + * When `override` is set, it replaces the model name entirely. + * Otherwise, `format` controls how the raw name is abbreviated: * * full: Return raw name unchanged (e.g. "Opus 4.6 (1M context)") * compact: Strip context-window suffix (e.g. "Opus 4.6") * short: Strip context suffix AND leading "Claude " prefix (e.g. "Opus 4.6") */ -export function formatModelName(name: string, format?: ModelFormatMode): string { +export function formatModelName(name: string, format?: ModelFormatMode, override?: string): string { + if (override) { + return override; + } + if (!format || format === 'full') { return name; } diff --git a/tests/config.test.js b/tests/config.test.js index 6f2e56b..b57d416 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -135,6 +135,24 @@ test('mergeConfig falls back to full for invalid modelFormat', () => { assert.equal(mergeConfig({ display: { modelFormat: null } }).display.modelFormat, 'full'); }); +test('mergeConfig defaults modelOverride to empty string', () => { + const config = mergeConfig({}); + assert.equal(config.display.modelOverride, ''); +}); + +test('mergeConfig preserves modelOverride and truncates long values', () => { + const override = 'x'.repeat(120); + const config = mergeConfig({ display: { modelOverride: override } }); + assert.equal(config.display.modelOverride.length, 80); + assert.equal(config.display.modelOverride, override.slice(0, 80)); +}); + +test('mergeConfig falls back to empty for non-string modelOverride', () => { + assert.equal(mergeConfig({ display: { modelOverride: 123 } }).display.modelOverride, ''); + assert.equal(mergeConfig({ display: { modelOverride: null } }).display.modelOverride, ''); + assert.equal(mergeConfig({ display: { modelOverride: true } }).display.modelOverride, ''); +}); + test('getConfigPath respects CLAUDE_CONFIG_DIR', async () => { const originalConfigDir = process.env.CLAUDE_CONFIG_DIR; const customConfigDir = await mkdtemp(path.join(tmpdir(), 'claude-hud-config-dir-')); diff --git a/tests/core.test.js b/tests/core.test.js index 9da81eb..822817b 100644 --- a/tests/core.test.js +++ b/tests/core.test.js @@ -319,6 +319,17 @@ test('formatModelName short mode strips context suffix and Claude prefix', () => assert.equal(formatModelName('claude Opus 4.5', 'short'), 'Opus 4.5'); }); +test('formatModelName override replaces model name entirely', () => { + // Override takes precedence over format + assert.equal(formatModelName('Claude Opus 4.5', 'full', "zane's intelligent opus"), "zane's intelligent opus"); + assert.equal(formatModelName('Claude Opus 4.5', 'compact', 'My Model'), 'My Model'); + assert.equal(formatModelName('Claude Opus 4.5', 'short', 'Custom'), 'Custom'); + assert.equal(formatModelName('Claude Opus 4.5', undefined, 'Override'), 'Override'); + // Empty override is treated as unset (falls through to format) + assert.equal(formatModelName('Claude Opus 4.5 (1M context)', 'compact', ''), 'Claude Opus 4.5'); + assert.equal(formatModelName('Opus 4.6', 'full', ''), 'Opus 4.6'); +}); + test('bedrock model detection recognizes bedrock ids', () => { assert.ok(isBedrockModelId('anthropic.claude-3-5-sonnet-20240620-v1:0')); assert.ok(isBedrockModelId('eu.anthropic.claude-opus-4-5-20251101-v1:0'));