feat: display detailed session stats on exit with resume command
When quitting the CLI, users now see a comprehensive session summary: - Total API time spent and session duration - Total code changes (+additions/-deletions) - Per-model token usage breakdown (input/output/cached) - Resume command with session ID Implementation details: - Extended SessionStats type with apiTimeSpent, apiCallStartTime, and modelUsage - Added startApiCall(), stopApiCall(), and addTokensWithModel() tracking functions - Created session-stats.ts utility with formatters and generateSessionSummary() - Updated TUI exit handler to display formatted stats - Added mouse tracking disable to drainStdin() for cleaner exit - Added modifiedFiles to getState() for exit summary access
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
} from "@services/chat-tui-service";
|
||||
import { matchesAction } from "@services/keybind-resolver";
|
||||
import { TERMINAL_RESET } from "@constants/terminal";
|
||||
import { formatExitMessage } from "@services/exit-message";
|
||||
import { generateSessionSummary } from "@utils/core/session-stats";
|
||||
import { copyToClipboard } from "@services/clipboard/text-clipboard";
|
||||
import versionData from "@/version.json";
|
||||
import { ExitProvider, useExit } from "@tui-solid/context/exit";
|
||||
@@ -582,14 +582,14 @@ export function tui(options: TuiRenderOptions): Promise<TuiOutput> {
|
||||
writeSync(1, TERMINAL_RESET);
|
||||
|
||||
const state = appStore.getState();
|
||||
const firstUserLog = state?.logs?.find(
|
||||
(log: { type: string }) => log.type === "user",
|
||||
);
|
||||
const sessionTitle = firstUserLog?.content;
|
||||
const exitMsg = formatExitMessage(output.sessionId, sessionTitle);
|
||||
if (exitMsg) {
|
||||
writeSync(1, exitMsg);
|
||||
}
|
||||
const summary = generateSessionSummary({
|
||||
sessionId: output.sessionId ?? "unknown",
|
||||
sessionStats: state.sessionStats,
|
||||
modifiedFiles: state.modifiedFiles,
|
||||
modelName: state.model,
|
||||
providerName: state.provider,
|
||||
});
|
||||
writeSync(1, summary);
|
||||
} catch {
|
||||
// Ignore - stdout may already be closed
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ interface AppContextValue {
|
||||
startThinking: () => void;
|
||||
stopThinking: () => void;
|
||||
addTokens: (input: number, output: number) => void;
|
||||
startApiCall: () => void;
|
||||
stopApiCall: () => void;
|
||||
addTokensWithModel: (modelId: string, input: number, output: number, cached?: number) => void;
|
||||
resetSessionStats: () => void;
|
||||
setContextMaxTokens: (maxTokens: number) => void;
|
||||
|
||||
@@ -234,6 +237,9 @@ const createInitialSessionStats = (): SessionStats => ({
|
||||
thinkingStartTime: null,
|
||||
lastThinkingDuration: 0,
|
||||
contextMaxTokens: 128000, // Default, updated when model is selected
|
||||
apiTimeSpent: 0,
|
||||
apiCallStartTime: null,
|
||||
modelUsage: [],
|
||||
});
|
||||
|
||||
const createInitialStreamingState = (): StreamingLogState => ({
|
||||
@@ -689,6 +695,53 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
});
|
||||
};
|
||||
|
||||
const startApiCall = (): void => {
|
||||
setStore("sessionStats", {
|
||||
...store.sessionStats,
|
||||
apiCallStartTime: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
const stopApiCall = (): void => {
|
||||
const elapsed = store.sessionStats.apiCallStartTime
|
||||
? Date.now() - store.sessionStats.apiCallStartTime
|
||||
: 0;
|
||||
setStore("sessionStats", {
|
||||
...store.sessionStats,
|
||||
apiTimeSpent: store.sessionStats.apiTimeSpent + elapsed,
|
||||
apiCallStartTime: null,
|
||||
});
|
||||
};
|
||||
|
||||
const addTokensWithModel = (
|
||||
modelId: string,
|
||||
input: number,
|
||||
output: number,
|
||||
cached?: number,
|
||||
): void => {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
const existing = s.sessionStats.modelUsage.find(
|
||||
(m) => m.modelId === modelId,
|
||||
);
|
||||
if (existing) {
|
||||
existing.inputTokens += input;
|
||||
existing.outputTokens += output;
|
||||
if (cached) existing.cachedTokens = (existing.cachedTokens ?? 0) + cached;
|
||||
} else {
|
||||
s.sessionStats.modelUsage.push({
|
||||
modelId,
|
||||
inputTokens: input,
|
||||
outputTokens: output,
|
||||
cachedTokens: cached,
|
||||
});
|
||||
}
|
||||
s.sessionStats.inputTokens += input;
|
||||
s.sessionStats.outputTokens += output;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const resetSessionStats = (): void => {
|
||||
setStore("sessionStats", createInitialSessionStats());
|
||||
};
|
||||
@@ -982,6 +1035,7 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
updateMcpServerStatus,
|
||||
|
||||
// Modified file tracking
|
||||
modifiedFiles: () => store.modifiedFiles,
|
||||
addModifiedFile,
|
||||
clearModifiedFiles,
|
||||
|
||||
@@ -995,6 +1049,9 @@ export const { provider: AppStoreProvider, use: useAppStore } =
|
||||
startThinking,
|
||||
stopThinking,
|
||||
addTokens,
|
||||
startApiCall,
|
||||
stopApiCall,
|
||||
addTokensWithModel,
|
||||
resetSessionStats,
|
||||
setContextMaxTokens,
|
||||
|
||||
@@ -1062,6 +1119,7 @@ const defaultAppState = {
|
||||
suggestions: createInitialSuggestionState(),
|
||||
mcpServers: [] as MCPServerDisplay[],
|
||||
pastedImages: [] as PastedImage[],
|
||||
modifiedFiles: [] as ModifiedFileEntry[],
|
||||
brain: {
|
||||
status: "disconnected" as BrainConnectionStatus,
|
||||
user: null,
|
||||
@@ -1105,6 +1163,7 @@ export const appStore = {
|
||||
suggestions: storeRef.suggestions(),
|
||||
mcpServers: storeRef.mcpServers(),
|
||||
pastedImages: storeRef.pastedImages(),
|
||||
modifiedFiles: storeRef.modifiedFiles(),
|
||||
brain: storeRef.brain(),
|
||||
};
|
||||
},
|
||||
@@ -1214,6 +1273,21 @@ export const appStore = {
|
||||
storeRef.addTokens(input, output);
|
||||
},
|
||||
|
||||
startApiCall: (): void => {
|
||||
if (!storeRef) return;
|
||||
storeRef.startApiCall();
|
||||
},
|
||||
|
||||
stopApiCall: (): void => {
|
||||
if (!storeRef) return;
|
||||
storeRef.stopApiCall();
|
||||
},
|
||||
|
||||
addTokensWithModel: (modelId: string, input: number, output: number, cached?: number): void => {
|
||||
if (!storeRef) return;
|
||||
storeRef.addTokensWithModel(modelId, input, output, cached);
|
||||
},
|
||||
|
||||
resetSessionStats: (): void => {
|
||||
if (!storeRef) return;
|
||||
storeRef.resetSessionStats();
|
||||
|
||||
Reference in New Issue
Block a user