diff --git a/src/commands/chat/state/state.ts b/src/commands/chat/state/state.ts
index ce7a7a9..fe5b820 100644
--- a/src/commands/chat/state/state.ts
+++ b/src/commands/chat/state/state.ts
@@ -24,8 +24,8 @@ export interface UserChatMessage {
export interface AiChatMessage {
type: 'ai';
text: string;
- responseTime?: number;
- usage?: ModelUsage;
+ responseTime: number;
+ usage: ModelUsage;
cost?: number;
data?: unknown;
}
diff --git a/src/commands/chat/ui/StatusBar.tsx b/src/commands/chat/ui/StatusBar.tsx
index 6cde793..1f516a2 100644
--- a/src/commands/chat/ui/StatusBar.tsx
+++ b/src/commands/chat/ui/StatusBar.tsx
@@ -1,9 +1,9 @@
import React, { useMemo } from 'react';
import { Box, Text } from 'ink';
import type { ModelUsage } from '../../../engine/inference.js';
-import { formatCost, formatTokenCount } from '../../../format.js';
+import { formatCost, formatSpeed, formatTokenCount } from '../../../format.js';
import { calculateUsageCost } from '../../../engine/session.js';
-import { useChatState } from '../state/state.js';
+import { useChatState, type ChatMessage } from '../state/state.js';
export function StatusBar() {
const verbose = useChatState((state) => state.verbose);
@@ -11,17 +11,8 @@ export function StatusBar() {
const provider = useChatState((state) => state.provider);
const providerConfig = useChatState((state) => state.providerConfig);
- const totalUsage = useMemo(() => {
- const usage: ModelUsage = { inputTokens: 0, outputTokens: 0, requests: 0 };
- items.forEach((item) => {
- if (item.type === 'ai') {
- usage.inputTokens += item.usage?.inputTokens ?? 0;
- usage.outputTokens += item.usage?.outputTokens ?? 0;
- usage.requests += item.usage?.requests ?? 0;
- }
- });
- return usage;
- }, [items]);
+ const totalUsage = useMemo(() => calculateTotalUsage(items), [items]);
+ const totalTime = useMemo(() => calculateTotalResponseTime(items), [items]);
const modelPricing = provider.pricing[providerConfig.model];
const totalCost = calculateUsageCost(totalUsage, modelPricing) ?? 0;
@@ -30,15 +21,37 @@ export function StatusBar() {
LLM: {provider.label}/{providerConfig.model} - Total Cost:{' '}
- {formatStats(totalCost, verbose ? totalUsage : undefined)}
+ {verbose ? formatVerboseStats(totalCost, totalUsage, totalTime) : formatCost(totalCost)}
);
}
-const formatStats = (cost: number, usage?: ModelUsage) => {
+function formatVerboseStats(cost: number, usage: ModelUsage, time: number) {
const usageOutput = usage
- ? ` (tokens: ${formatTokenCount(usage.inputTokens)} in + ${formatTokenCount(usage.outputTokens)} out, requests: ${usage.requests})`
+ ? ` (tokens: ${formatTokenCount(usage.inputTokens)} in + ${formatTokenCount(usage.outputTokens)} out, requests: ${usage.requests}, speed: ${formatSpeed(usage.outputTokens, time)})`
: '';
return `${formatCost(cost)}${usageOutput}`;
-};
+}
+
+function calculateTotalUsage(messages: ChatMessage[]) {
+ const usage: ModelUsage = { inputTokens: 0, outputTokens: 0, requests: 0 };
+ messages.forEach((message) => {
+ if (message.type === 'ai') {
+ usage.inputTokens += message.usage?.inputTokens ?? 0;
+ usage.outputTokens += message.usage?.outputTokens ?? 0;
+ usage.requests += message.usage?.requests ?? 0;
+ }
+ });
+ return usage;
+}
+
+function calculateTotalResponseTime(messages: ChatMessage[]) {
+ let total = 0;
+ messages.forEach((message) => {
+ if (message.type === 'ai') {
+ total += message.responseTime ?? 0;
+ }
+ });
+ return total;
+}
diff --git a/src/commands/chat/ui/list/AiChatMessageItem.tsx b/src/commands/chat/ui/list/AiChatMessageItem.tsx
index f2fe7c8..583b20f 100644
--- a/src/commands/chat/ui/list/AiChatMessageItem.tsx
+++ b/src/commands/chat/ui/list/AiChatMessageItem.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Text } from 'ink';
-import { formatTime } from '../../../../format.js';
+import { formatSpeed, formatTime } from '../../../../format.js';
import { colors } from '../../../../theme/colors.js';
import { useChatState, type AiChatMessage } from '../../state/state.js';
import { texts } from '../../texts.js';
@@ -19,7 +19,11 @@ export function AiChatMessageItem({ message }: AiChatMessageItemProps) {
{message.text}
{verbose && message.responseTime != null ? (
- ({formatTime(message.responseTime)})
+
+ {' '}
+ ({formatTime(message.responseTime)},{' '}
+ {formatSpeed(message.usage?.outputTokens, message.responseTime)})
+
) : null}
{verbose ? {JSON.stringify(message.data, null, 2)} : null}
diff --git a/src/format.ts b/src/format.ts
index f6ba3f2..e648e8f 100644
--- a/src/format.ts
+++ b/src/format.ts
@@ -48,10 +48,14 @@ export function formatSessionCost(cost: SessionCost | undefined) {
return `costs: ${formatCost(cost.current)} (total: ${formatCost(cost.total)})`;
}
-export function formatTime(timeInMs?: number) {
- if (timeInMs == null) {
- return '';
+export function formatTime(timeInMs: number) {
+ return `${(timeInMs / 1000).toFixed(1)} s`;
+}
+
+export function formatSpeed(tokens: number, timeInMs: number) {
+ if (tokens == null || timeInMs == null || timeInMs === 0) {
+ return '? tok/s';
}
- return `${(timeInMs / 1000).toFixed(1)} s`;
+ return `${((tokens * 1000) / timeInMs).toFixed(1)} tok/s`;
}