From 60fcb36772b5f17567307729e27e834c351aa0d2 Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 02:27:00 +1000 Subject: [PATCH] Show model name on its own status bar line (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token/cost line accumulates per-turn data; the model name is a separate concern — it belongs on its own line so the two don't mix. statusBarHeight goes from 4 to 5 to accommodate the extra row. Abbreviation extracts the first non-numeric, non-'claude' segment and capitalises it, handling both name styles: claude-sonnet-4-6 -> Sonnet claude-3-5-sonnet-20241022 -> Sonnet setModel is called at the top of runAgent so the line updates as soon as a new run starts, ready for when the model becomes configurable. --- apps/claude-sdk-cli/src/AppLayout.ts | 14 +++-- apps/claude-sdk-cli/src/StatusState.ts | 8 +++ apps/claude-sdk-cli/src/entry/main.ts | 4 +- apps/claude-sdk-cli/src/renderStatus.ts | 28 ++++++++++ apps/claude-sdk-cli/src/runAgent.ts | 4 +- apps/claude-sdk-cli/test/StatusState.spec.ts | 29 ++++++++++ apps/claude-sdk-cli/test/renderStatus.spec.ts | 56 ++++++++++++++++++- 7 files changed, 135 insertions(+), 8 deletions(-) diff --git a/apps/claude-sdk-cli/src/AppLayout.ts b/apps/claude-sdk-cli/src/AppLayout.ts index 52b5713..34a48c0 100644 --- a/apps/claude-sdk-cli/src/AppLayout.ts +++ b/apps/claude-sdk-cli/src/AppLayout.ts @@ -16,7 +16,7 @@ import { logger } from './logger.js'; import { renderCommandMode } from './renderCommandMode.js'; import { buildDivider, renderBlocksToString, renderConversation } from './renderConversation.js'; import { renderEditor } from './renderEditor.js'; -import { renderStatus } from './renderStatus.js'; +import { renderModel, renderStatus } from './renderStatus.js'; import { renderToolApproval } from './renderToolApproval.js'; import { StatusState } from './StatusState.js'; import type { PendingTool } from './ToolApprovalState.js'; @@ -152,6 +152,11 @@ export class AppLayout implements Disposable { this.#cancelFn = fn; } + public setModel(model: string): void { + this.#statusState.setModel(model); + this.render(); + } + /** * Append text to the most recent sealed block of the given type. * Used for retroactive annotations (e.g. adding turn cost to the tools block after @@ -320,8 +325,8 @@ export class AppLayout implements Disposable { const { approvalRow, expandedRows: toolRows } = renderToolApproval(this.#toolApprovalState, cols, Math.floor(totalRows / 2)); const { commandRow, previewRows } = renderCommandMode(this.#commandModeState, cols, Math.max(1, Math.floor(totalRows / 3)), Math.floor(totalRows / 2)); const expandedRows = [...toolRows, ...previewRows]; - // Fixed status bar: separator (1) + status line (1) + approval row (1) + command row (always 1) + optional expanded rows - const statusBarHeight = 4 + expandedRows.length; + // Fixed status bar: separator (1) + model line (1) + status line (1) + approval row (1) + command row (always 1) + optional expanded rows + const statusBarHeight = 5 + expandedRows.length; const contentRows = Math.max(2, totalRows - statusBarHeight); // Build content rows: conversation blocks + editor (when in editor mode) @@ -337,8 +342,9 @@ export class AppLayout implements Disposable { const visibleRows = overflow > 0 ? allContent.slice(overflow) : [...new Array(contentRows - allContent.length).fill(''), ...allContent]; const separator = buildDivider(null, cols); + const modelLine = renderModel(this.#statusState, cols); const statusLine = renderStatus(this.#statusState, cols); - const allRows = [...visibleRows, separator, statusLine, approvalRow, commandRow, ...expandedRows]; + const allRows = [...visibleRows, separator, modelLine, statusLine, approvalRow, commandRow, ...expandedRows]; let out = syncStart + hideCursor; out += cursorAt(1, 1); diff --git a/apps/claude-sdk-cli/src/StatusState.ts b/apps/claude-sdk-cli/src/StatusState.ts index 1b99c32..3d82221 100644 --- a/apps/claude-sdk-cli/src/StatusState.ts +++ b/apps/claude-sdk-cli/src/StatusState.ts @@ -12,6 +12,7 @@ export class StatusState { #totalCostUsd = 0; #lastContextUsed = 0; #contextWindow = 0; + #model = ''; public get totalInputTokens(): number { return this.#totalInputTokens; @@ -34,6 +35,13 @@ export class StatusState { public get contextWindow(): number { return this.#contextWindow; } + public get model(): string { + return this.#model; + } + + public setModel(name: string): void { + this.#model = name; + } public update(msg: SdkMessageUsage): void { this.#totalInputTokens += msg.inputTokens; diff --git a/apps/claude-sdk-cli/src/entry/main.ts b/apps/claude-sdk-cli/src/entry/main.ts index eb5350b..90e86e3 100644 --- a/apps/claude-sdk-cli/src/entry/main.ts +++ b/apps/claude-sdk-cli/src/entry/main.ts @@ -63,6 +63,7 @@ const main = async () => { rl.setLayout(layout); layout.enter(); + const model = 'claude-sonnet-4-6'; const agent = createAnthropicAgent({ authToken, logger, historyFile: HISTORY_FILE }); if (config.historyReplay.enabled) { @@ -72,10 +73,11 @@ const main = async () => { } } layout.showStartupBanner(startupBannerText()); + layout.setModel(model); const store = new RefStore(); while (true) { const prompt = await layout.waitForInput(); - await runAgent(agent, prompt, layout, store); + await runAgent(agent, prompt, layout, store, model); } }; await main(); diff --git a/apps/claude-sdk-cli/src/renderStatus.ts b/apps/claude-sdk-cli/src/renderStatus.ts index 7b1910b..de9f945 100644 --- a/apps/claude-sdk-cli/src/renderStatus.ts +++ b/apps/claude-sdk-cli/src/renderStatus.ts @@ -1,6 +1,34 @@ +import { RESET, YELLOW } from '@shellicar/claude-core/ansi'; import { StatusLineBuilder } from '@shellicar/claude-core/status-line'; import type { StatusState } from './StatusState.js'; +/** + * Extracts the model family name and capitalises it. + * Handles both name styles: + * claude-sonnet-4-6 -> Sonnet + * claude-3-5-sonnet-20241022 -> Sonnet + * Skips 'claude' and any purely-numeric parts to find the family word. + */ +function abbreviateModel(model: string): string { + const parts = model.split('-'); + const name = parts.find((p, i) => i > 0 && !/^\d/.test(p)); + if (!name) { + return model; + } + return name.charAt(0).toUpperCase() + name.slice(1); +} + +/** + * Returns the model name line, or empty string if no model is set yet. + */ +export function renderModel(state: StatusState, _cols: number): string { + const model = state.model; + if (!model) { + return ''; + } + return ` ${YELLOW}⚡ ${abbreviateModel(model)}${RESET}`; +} + function formatTokens(n: number): string { if (n >= 1000) { return `${(n / 1000).toFixed(1)}k`; diff --git a/apps/claude-sdk-cli/src/runAgent.ts b/apps/claude-sdk-cli/src/runAgent.ts index 4c7a9ed..6ffa3ab 100644 --- a/apps/claude-sdk-cli/src/runAgent.ts +++ b/apps/claude-sdk-cli/src/runAgent.ts @@ -20,7 +20,7 @@ import type { AppLayout } from './AppLayout.js'; import { logger } from './logger.js'; import { systemPrompts } from './systemPrompts.js'; -export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: AppLayout, store: RefStore): Promise { +export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: AppLayout, store: RefStore, model: string): Promise { const pipeSource = [Find, ReadFile, Grep, Head, Tail, Range, SearchFiles]; const { tool: Ref, transformToolResult: refTransform } = createRef(store, 20_000); const otherTools = [PreviewEdit, EditFile, CreateFile, DeleteFile, DeleteDirectory, Exec, Ref]; @@ -28,7 +28,6 @@ export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: A const tools: AnyToolDefinition[] = [pipe, ...pipeSource, ...otherTools]; const cwd = process.cwd(); - const model = 'claude-sonnet-4-6'; const cacheTtl = CacheTtl.OneHour; const transformToolResult = (toolName: string, output: unknown): unknown => { @@ -40,6 +39,7 @@ export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: A return result; }; + layout.setModel(model); layout.startStreaming(prompt); const { port, done } = agent.runAgent({ diff --git a/apps/claude-sdk-cli/test/StatusState.spec.ts b/apps/claude-sdk-cli/test/StatusState.spec.ts index 40dff54..8ef7acc 100644 --- a/apps/claude-sdk-cli/test/StatusState.spec.ts +++ b/apps/claude-sdk-cli/test/StatusState.spec.ts @@ -119,3 +119,32 @@ describe('StatusState — update overwrites lastContextUsed and contextWindow', expect(actual).toBe(expected); }); }); + +// --------------------------------------------------------------------------- +// setModel / model +// --------------------------------------------------------------------------- + +describe('StatusState — model', () => { + it('model starts as empty string', () => { + const expected = ''; + const actual = new StatusState().model; + expect(actual).toBe(expected); + }); + + it('setModel stores the model name', () => { + const state = new StatusState(); + state.setModel('claude-sonnet-4-6'); + const expected = 'claude-sonnet-4-6'; + const actual = state.model; + expect(actual).toBe(expected); + }); + + it('setModel overwrites the previous value', () => { + const state = new StatusState(); + state.setModel('claude-sonnet-4-6'); + state.setModel('claude-opus-4-5'); + const expected = 'claude-opus-4-5'; + const actual = state.model; + expect(actual).toBe(expected); + }); +}); diff --git a/apps/claude-sdk-cli/test/renderStatus.spec.ts b/apps/claude-sdk-cli/test/renderStatus.spec.ts index b9ddba6..e521d74 100644 --- a/apps/claude-sdk-cli/test/renderStatus.spec.ts +++ b/apps/claude-sdk-cli/test/renderStatus.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { renderStatus } from '../src/renderStatus.js'; +import { renderModel, renderStatus } from '../src/renderStatus.js'; import { StatusState } from '../src/StatusState.js'; function makeState(inputTokens: number, opts: { cacheCreation?: number; cacheRead?: number; output?: number; cost?: number; contextWindow?: number } = {}): StatusState { @@ -108,3 +108,57 @@ describe('renderStatus — token formatting', () => { expect(actual).toBe(expected); }); }); + +// --------------------------------------------------------------------------- +// renderModel +// --------------------------------------------------------------------------- + +describe('renderModel — empty state', () => { + it('returns empty string when no model set', () => { + const expected = ''; + const actual = renderModel(new StatusState(), 120); + expect(actual).toBe(expected); + }); +}); + +describe('renderModel — model abbreviation', () => { + it('capitalises Sonnet from new-style name (claude-sonnet-4-6)', () => { + const state = new StatusState(); + state.setModel('claude-sonnet-4-6'); + const expected = true; + const actual = renderModel(state, 120).includes('Sonnet'); + expect(actual).toBe(expected); + }); + + it('capitalises Sonnet from old-style name (claude-3-5-sonnet-20241022)', () => { + const state = new StatusState(); + state.setModel('claude-3-5-sonnet-20241022'); + const expected = true; + const actual = renderModel(state, 120).includes('Sonnet'); + expect(actual).toBe(expected); + }); + + it('capitalises Opus', () => { + const state = new StatusState(); + state.setModel('claude-opus-4-5'); + const expected = true; + const actual = renderModel(state, 120).includes('Opus'); + expect(actual).toBe(expected); + }); + + it('capitalises Haiku', () => { + const state = new StatusState(); + state.setModel('claude-haiku-3-5'); + const expected = true; + const actual = renderModel(state, 120).includes('Haiku'); + expect(actual).toBe(expected); + }); + + it('does not contain lowercase model family', () => { + const state = new StatusState(); + state.setModel('claude-sonnet-4-6'); + const expected = false; + const actual = renderModel(state, 120).includes('sonnet'); + expect(actual).toBe(expected); + }); +});