feat: add token counting display to status bar

Implement real-time input/output token tracking in the status bar by
integrating ITokenService with ChatService. Token counts are calculated
from conversation history and updated on conversation load, reset, and
message completion.
This commit is contained in:
Andrew Beal 2025-10-17 22:23:45 +01:00
parent db054744ba
commit 47af43fe1f
4 changed files with 48 additions and 4 deletions

View file

@ -45,6 +45,7 @@
if (chatContainer) {
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
}
chatService.setStatusBarTokens(0, 0);
});
async function handleLinkClick(evt: MouseEvent) {
@ -119,6 +120,7 @@
onComplete: () => {
isSubmitting = false;
chatArea.onFinishedSubmitting();
chatService.updateTokenDisplay(conversation);
}
});
}
@ -162,6 +164,7 @@
$: if ($conversationStore.shouldReset) {
conversation = new Conversation();
chatService.setStatusBarTokens(0, 0);
conversationStore.clearResetFlag();
}
@ -170,6 +173,7 @@
conversation = loadedConversation;
conversationService.setCurrentConversationPath(filePath);
chatService.onNameChanged?.(loadedConversation.title);
chatService.updateTokenDisplay(loadedConversation);
conversationStore.clearLoadFlag();
scrollToBottom();
}

View file

@ -5,6 +5,9 @@ import type { IAIClass } from "AIClasses/IAIClass";
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
import type { AIFunctionService } from "./AIFunctionService";
import type { ConversationNamingService } from "./ConversationNamingService";
import type { ITokenService } from "AIClasses/ITokenService";
import type { IPrompt } from "AIClasses/IPrompt";
import type { StatusBarService } from "./StatusBarService";
import { Conversation } from "Conversations/Conversation";
import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
@ -21,6 +24,9 @@ export class ChatService {
private conversationService: ConversationFileSystemService;
private aiFunctionService: AIFunctionService;
private namingService: ConversationNamingService;
private tokenService: ITokenService | undefined;
private prompt: IPrompt;
private statusBarService: StatusBarService;
private semaphore: Semaphore;
private semaphoreHeld: boolean = false;
@ -30,16 +36,19 @@ export class ChatService {
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
this.aiFunctionService = Resolve<AIFunctionService>(Services.AIFunctionService);
this.namingService = Resolve<ConversationNamingService>(Services.ConversationNamingService);
this.prompt = Resolve<IPrompt>(Services.IPrompt);
this.statusBarService = Resolve<StatusBarService>(Services.StatusBarService);
this.semaphore = new Semaphore(1, false);
}
public onNameChanged: ((name: string) => void) | undefined = undefined;
resolveAIProvider() {
public resolveAIProvider() {
this.ai = Resolve<IAIClass>(Services.IAIClass);
this.tokenService = Resolve<ITokenService>(Services.ITokenService);
}
async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, callbacks: ChatServiceCallbacks): Promise<Conversation> {
public async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, callbacks: ChatServiceCallbacks): Promise<Conversation> {
if (!await this.semaphore.wait()) {
return conversation;
}
@ -96,7 +105,7 @@ export class ChatService {
}
}
stop(): void {
public stop(): void {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
@ -104,6 +113,35 @@ export class ChatService {
this.semaphore.release();
}
public async updateTokenDisplay(conversation: Conversation): Promise<void> {
if (this.tokenService === undefined) {
return;
}
const systemInstruction = this.prompt.systemInstruction();
const userInstruction = await this.prompt.userInstruction();
const inputMessages = conversation.contents
.filter(msg => msg.role === Role.User && !msg.isFunctionCallResponse)
.map(msg => msg.content)
.join("\n");
const outputMessages = conversation.contents
.filter(msg => msg.role === Role.Assistant && !msg.isFunctionCall)
.map(msg => msg.content)
.join("\n");
const inputText = systemInstruction + "\n" + userInstruction + "\n" + inputMessages;
const inputTokens = await this.tokenService.countTokens(inputText);
const outputTokens = await this.tokenService.countTokens(outputMessages);
this.setStatusBarTokens(inputTokens, outputTokens);
}
public setStatusBarTokens(inputTokens: number, outputTokens: number): void {
this.statusBarService.setStatusBarMessage(`Input Tokens: ${inputTokens} / Output Tokens: ${outputTokens}`);
}
private async streamRequestResponse(
conversation: Conversation, allowDestructiveActions: boolean, callbacks: ChatServiceCallbacks
): Promise<{ functionCall: AIFunctionCall | null, shouldContinue: boolean }> {

View file

@ -21,11 +21,12 @@ import { ConversationNamingService } from "./ConversationNamingService";
import { VaultService } from "./VaultService";
import type { ITokenService } from "AIClasses/ITokenService";
import { GeminiTokenService } from "AIClasses/Gemini/GeminiTokenService";
import { StatusBarService } from "./StatusBarService";
export function RegisterDependencies(plugin: AIAgentPlugin) {
RegisterSingleton<AIAgentPlugin>(Services.AIAgentPlugin, plugin);
RegisterSingleton<FileManager>(Services.FileManager, plugin.app.fileManager);
RegisterSingleton<StatusBarService>(Services.StatusBarService, new StatusBarService());
RegisterSingleton<VaultService>(Services.VaultService, new VaultService());
RegisterSingleton<WorkSpaceService>(Services.WorkSpaceService, new WorkSpaceService());
RegisterSingleton<FileSystemService>(Services.FileSystemService, new FileSystemService());

View file

@ -1,6 +1,7 @@
import type AIAgentPlugin from "main";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import { Selector } from "Enums/Selector";
export class StatusBarService {