Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/claude-sdk-cli/src/AppLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -337,8 +342,9 @@ export class AppLayout implements Disposable {
const visibleRows = overflow > 0 ? allContent.slice(overflow) : [...new Array<string>(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);
Expand Down
8 changes: 8 additions & 0 deletions apps/claude-sdk-cli/src/StatusState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class StatusState {
#totalCostUsd = 0;
#lastContextUsed = 0;
#contextWindow = 0;
#model = '';

public get totalInputTokens(): number {
return this.#totalInputTokens;
Expand All @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion apps/claude-sdk-cli/src/entry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
28 changes: 28 additions & 0 deletions apps/claude-sdk-cli/src/renderStatus.ts
Original file line number Diff line number Diff line change
@@ -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`;
Expand Down
4 changes: 2 additions & 2 deletions apps/claude-sdk-cli/src/runAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ 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<void> {
export async function runAgent(agent: IAnthropicAgent, prompt: string, layout: AppLayout, store: RefStore, model: string): Promise<void> {
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];
const pipe = createPipe(pipeSource);
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 => {
Expand All @@ -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({
Expand Down
29 changes: 29 additions & 0 deletions apps/claude-sdk-cli/test/StatusState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
56 changes: 55 additions & 1 deletion apps/claude-sdk-cli/test/renderStatus.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
});
});
Loading