Skip to content

Commit bd948af

Browse files
authored
Merge pull request #640 from rajbos/feature/mistralvibe-integration
feature/mistralvibe integration
2 parents 7cc1f1a + 49a4011 commit bd948af

12 files changed

Lines changed: 445 additions & 7 deletions

File tree

cli/src/commands/stats.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ function getEditorDisplayName(source: string): string {
139139
'opencode': 'OpenCode',
140140
'claude-code': 'Claude Code',
141141
'claude-desktop-cowork': 'Claude Desktop (Cowork)',
142+
'mistral-vibe': 'Mistral Vibe',
142143
};
143144
return names[source] || source;
144145
}

cli/src/helpers.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ContinueDataAccess } from '../../vscode-extension/src/continue';
1313
import { VisualStudioDataAccess } from '../../vscode-extension/src/visualstudio';
1414
import { ClaudeCodeDataAccess } from '../../vscode-extension/src/claudecode';
1515
import { ClaudeDesktopCoworkDataAccess } from '../../vscode-extension/src/claudedesktop';
16+
import { MistralVibeDataAccess } from '../../vscode-extension/src/mistralvibe';
1617
import { parseSessionFileContent } from '../../vscode-extension/src/sessionParser';
1718
import { estimateTokensFromText, getModelFromRequest, isJsonlContent, estimateTokensFromJsonlSession, calculateEstimatedCost, getModelTier } from '../../vscode-extension/src/tokenEstimation';
1819
import type { DetailedStats, PeriodStats, ModelUsage, EditorUsage, SessionFileCache, UsageAnalysisStats, UsageAnalysisPeriod } from '../../vscode-extension/src/types';
@@ -72,17 +73,23 @@ function createClaudeDesktopCowork(): ClaudeDesktopCoworkDataAccess {
7273
return new ClaudeDesktopCoworkDataAccess();
7374
}
7475

76+
/** Create Mistral Vibe data access instance for CLI */
77+
function createMistralVibe(): MistralVibeDataAccess {
78+
return new MistralVibeDataAccess();
79+
}
80+
7581
// Module-level singletons so sql.js WASM is only initialised once across all session files
7682
const _openCodeInstance = createOpenCode();
7783
const _crushInstance = createCrush();
7884
const _continueInstance = createContinue();
7985
const _visualStudioInstance = createVisualStudio();
8086
const _claudeCodeInstance = createClaudeCode();
8187
const _claudeDesktopCoworkInstance = createClaudeDesktopCowork();
88+
const _mistralVibeInstance = createMistralVibe();
8289

8390
/** Create session discovery instance for CLI */
8491
function createSessionDiscovery(): SessionDiscovery {
85-
return new SessionDiscovery({ log, warn, error, openCode: _openCodeInstance, crush: _crushInstance, continue_: _continueInstance, visualStudio: _visualStudioInstance, claudeCode: _claudeCodeInstance, claudeDesktopCowork: _claudeDesktopCoworkInstance });
92+
return new SessionDiscovery({ log, warn, error, openCode: _openCodeInstance, crush: _crushInstance, continue_: _continueInstance, visualStudio: _visualStudioInstance, claudeCode: _claudeCodeInstance, claudeDesktopCowork: _claudeDesktopCoworkInstance, mistralVibe: _mistralVibeInstance });
8693
}
8794

8895
/** Discover all session files on this machine */
@@ -152,6 +159,7 @@ function getEditorSourceFromPath(filePath: string): string {
152159
if (normalized.includes('/opencode/')) { return 'opencode'; }
153160
if (normalized.includes('/local-agent-mode-sessions/')) { return 'claude-desktop-cowork'; }
154161
if (normalized.includes('/.claude/projects/')) { return 'claude-code'; }
162+
if (normalized.includes('/.vibe/logs/session/')) { return 'mistral-vibe'; }
155163
if (normalized.includes('.vscode-server')) { return 'vscode-remote'; }
156164
if (normalized.includes('/.vs/') && normalized.includes('/copilot-chat/')) { return 'Visual Studio'; }
157165
return 'vscode';
@@ -293,6 +301,22 @@ const crushResult: SessionData = {
293301
return claudeResult;
294302
}
295303

304+
// Handle Mistral Vibe sessions (JSON meta.json with actual token counts in stats)
305+
if (_mistralVibeInstance.isVibeSessionFile(filePath)) {
306+
const sessionData = _mistralVibeInstance.getSessionData(filePath);
307+
const vibeResult: SessionData = {
308+
file: filePath,
309+
tokens: sessionData.tokens,
310+
thinkingTokens: 0,
311+
interactions: sessionData.interactions,
312+
modelUsage: sessionData.modelUsage,
313+
lastModified: stats.mtime,
314+
editorSource: getEditorSourceFromPath(filePath),
315+
};
316+
setCached(filePath, stats.mtimeMs, stats.size, vibeResult);
317+
return vibeResult;
318+
}
319+
296320
const content = await fs.promises.readFile(filePath, 'utf-8');
297321

298322
if (!content.trim()) {

vscode-extension/media/mistral-vibe.svg

Loading

vscode-extension/src/extension.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as vscode from 'vscode';
1+
import * as vscode from 'vscode';
22
import * as fs from 'fs';
33
import * as path from 'path';
44
import * as os from 'os';
@@ -64,6 +64,7 @@ import { VisualStudioDataAccess } from './visualstudio';
6464
import { ContinueDataAccess } from './continue';
6565
import { ClaudeCodeDataAccess } from './claudecode';
6666
import { ClaudeDesktopCoworkDataAccess } from './claudedesktop';
67+
import { MistralVibeDataAccess } from './mistralvibe';
6768
import {
6869
estimateTokensFromText as _estimateTokensFromText,
6970
estimateTokensFromJsonlSession as _estimateTokensFromJsonlSession,
@@ -168,10 +169,11 @@ class CopilotTokenTracker implements vscode.Disposable {
168169
private continue_: ContinueDataAccess;
169170
private claudeCode: ClaudeCodeDataAccess;
170171
private claudeDesktopCowork: ClaudeDesktopCoworkDataAccess;
172+
private mistralVibe: MistralVibeDataAccess;
171173
private cacheManager: CacheManager;
172174

173175
private get usageAnalysisDeps(): UsageAnalysisDeps {
174-
return { warn: (m: string) => this.warn(m), openCode: this.openCode, crush: this.crush, visualStudio: this.visualStudio, continue_: this.continue_, claudeCode: this.claudeCode, claudeDesktopCowork: this.claudeDesktopCowork, tokenEstimators: this.tokenEstimators, modelPricing: this.modelPricing, toolNameMap: this.toolNameMap };
176+
return { warn: (m: string) => this.warn(m), openCode: this.openCode, crush: this.crush, visualStudio: this.visualStudio, continue_: this.continue_, claudeCode: this.claudeCode, claudeDesktopCowork: this.claudeDesktopCowork, mistralVibe: this.mistralVibe, tokenEstimators: this.tokenEstimators, modelPricing: this.modelPricing, toolNameMap: this.toolNameMap };
175177
}
176178
private sessionDiscovery: SessionDiscovery;
177179
private statusBarItem: vscode.StatusBarItem;
@@ -324,6 +326,9 @@ class CopilotTokenTracker implements vscode.Disposable {
324326
if (this.crush.isCrushSessionFile(sessionFile)) {
325327
return this.crush.statSessionFile(sessionFile);
326328
}
329+
if (this.mistralVibe.isVibeSessionFile(sessionFile)) {
330+
return fs.promises.stat(sessionFile);
331+
}
327332
return this.openCode.statSessionFile(sessionFile);
328333
}
329334

@@ -835,6 +840,7 @@ class CopilotTokenTracker implements vscode.Disposable {
835840
this.visualStudio = new VisualStudioDataAccess();
836841
this.claudeCode = new ClaudeCodeDataAccess();
837842
this.claudeDesktopCowork = new ClaudeDesktopCoworkDataAccess();
843+
this.mistralVibe = new MistralVibeDataAccess();
838844
this.cacheManager = new CacheManager(context, { log: (m: string) => this.log(m), warn: (m: string) => this.warn(m), error: (m: string) => this.error(m) }, CopilotTokenTracker.CACHE_VERSION);
839845
this.sessionDiscovery = new SessionDiscovery({
840846
log: (m) => this.log(m),
@@ -846,6 +852,7 @@ class CopilotTokenTracker implements vscode.Disposable {
846852
continue_: this.continue_,
847853
claudeCode: this.claudeCode,
848854
claudeDesktopCowork: this.claudeDesktopCowork,
855+
mistralVibe: this.mistralVibe,
849856
sampleDataDirectoryOverride: () => this.localRegressionSampleDataDir,
850857
});
851858
this.context = context;
@@ -2458,6 +2465,11 @@ class CopilotTokenTracker implements vscode.Disposable {
24582465
return this.claudeCode.countClaudeCodeInteractions(sessionFile);
24592466
}
24602467

2468+
// Handle Mistral Vibe sessions
2469+
if (this.mistralVibe.isVibeSessionFile(sessionFile)) {
2470+
return this.mistralVibe.countInteractions(sessionFile);
2471+
}
2472+
24612473
const fileContent = preloadedContent ?? await fs.promises.readFile(sessionFile, 'utf8');
24622474

24632475
// Check if this is a UUID-only file (new Copilot CLI format)
@@ -2709,6 +2721,16 @@ class CopilotTokenTracker implements vscode.Disposable {
27092721
};
27102722
}
27112723

2724+
// Handle Mistral Vibe sessions
2725+
if (this.mistralVibe.isVibeSessionFile(sessionFile)) {
2726+
const meta = this.mistralVibe.getSessionMeta(sessionFile);
2727+
return {
2728+
title: meta?.title,
2729+
firstInteraction: meta?.firstInteraction || null,
2730+
lastInteraction: meta?.lastInteraction || null,
2731+
};
2732+
}
2733+
27122734
const fileContent = preloadedContent ?? await fs.promises.readFile(sessionFile, 'utf8');
27132735

27142736
// Check if this is a UUID-only file (new Copilot CLI format)
@@ -2946,6 +2968,11 @@ class CopilotTokenTracker implements vscode.Disposable {
29462968
details.editorName = 'Claude Code';
29472969
return;
29482970
}
2971+
if (this.mistralVibe.isVibeSessionFile(sessionFile)) {
2972+
details.editorRoot = this.mistralVibe.getSessionLogDir();
2973+
details.editorName = 'Mistral Vibe';
2974+
return;
2975+
}
29492976
try {
29502977
const parts = sessionFile.split(/[/\\]/);
29512978
const userIdx = parts.findIndex(p => p.toLowerCase() === 'user');
@@ -3858,6 +3885,84 @@ class CopilotTokenTracker implements vscode.Disposable {
38583885
};
38593886
}
38603887

3888+
// Handle Mistral Vibe sessions
3889+
if (this.mistralVibe.isVibeSessionFile(sessionFile)) {
3890+
const messages = this.mistralVibe.readSessionMessages(sessionFile);
3891+
const sessionMeta = this.mistralVibe.getSessionMeta(sessionFile);
3892+
const tokenData = this.mistralVibe.getTokensFromSession(sessionFile);
3893+
const model: string = sessionMeta.model || 'devstral';
3894+
3895+
// Collect non-injected user messages as turn boundaries
3896+
const userMsgIndices: number[] = [];
3897+
for (let i = 0; i < messages.length; i++) {
3898+
if (messages[i].role === 'user' && messages[i].injected !== true) {
3899+
userMsgIndices.push(i);
3900+
}
3901+
}
3902+
// Mistral Vibe only has session-level token counts (not per-turn)
3903+
// Set per-turn estimates to 0; actual totals go in actualTokens
3904+
3905+
for (let t = 0; t < userMsgIndices.length; t++) {
3906+
const userIdx = userMsgIndices[t];
3907+
const nextUserIdx = t + 1 < userMsgIndices.length ? userMsgIndices[t + 1] : messages.length;
3908+
const userMsg = messages[userIdx];
3909+
const userText = typeof userMsg.content === 'string' ? userMsg.content : '';
3910+
let assistantText = '';
3911+
const toolCalls: { toolName: string; arguments?: string; result?: string }[] = [];
3912+
3913+
// Collect all messages between this user message and the next non-injected user message
3914+
for (let j = userIdx + 1; j < nextUserIdx; j++) {
3915+
const msg = messages[j];
3916+
if (msg.role === 'assistant') {
3917+
if (typeof msg.content === 'string') { assistantText += msg.content; }
3918+
if (Array.isArray(msg.tool_calls)) {
3919+
for (const tc of msg.tool_calls) {
3920+
toolCalls.push({
3921+
toolName: tc.function?.name || tc.name || 'unknown',
3922+
arguments: tc.function?.arguments ? JSON.stringify(tc.function.arguments) : undefined
3923+
});
3924+
}
3925+
}
3926+
} else if (msg.role === 'tool') {
3927+
// Attach tool result to last tool call
3928+
const last = toolCalls[toolCalls.length - 1];
3929+
if (last) { last.result = typeof msg.content === 'string' ? msg.content : undefined; }
3930+
}
3931+
}
3932+
3933+
turns.push({
3934+
turnNumber: t + 1,
3935+
timestamp: sessionMeta.firstInteraction,
3936+
mode: 'agent',
3937+
userMessage: userText,
3938+
assistantResponse: assistantText,
3939+
model,
3940+
toolCalls,
3941+
contextReferences: _createEmptyContextRefs(),
3942+
mcpTools: [],
3943+
inputTokensEstimate: 0,
3944+
outputTokensEstimate: 0,
3945+
thinkingTokensEstimate: 0
3946+
});
3947+
}
3948+
3949+
return {
3950+
file: details.file,
3951+
title: details.title || null,
3952+
editorSource: details.editorSource,
3953+
editorName: 'Mistral Vibe',
3954+
size: details.size,
3955+
modified: details.modified,
3956+
interactions: details.interactions,
3957+
contextReferences: details.contextReferences,
3958+
firstInteraction: details.firstInteraction,
3959+
lastInteraction: details.lastInteraction,
3960+
turns,
3961+
actualTokens: tokenData.tokens,
3962+
usageAnalysis: undefined
3963+
};
3964+
}
3965+
38613966
const fileContent = await fs.promises.readFile(sessionFile, 'utf8');
38623967

38633968
// Check if this is a UUID-only file (new Copilot CLI format)
@@ -4348,6 +4453,12 @@ class CopilotTokenTracker implements vscode.Disposable {
43484453
return { ...result, actualTokens: result.tokens };
43494454
}
43504455

4456+
// Handle Mistral Vibe sessions - actual token counts from meta.json stats
4457+
if (this.mistralVibe.isVibeSessionFile(sessionFilePath)) {
4458+
const result = this.mistralVibe.getTokensFromSession(sessionFilePath);
4459+
return { ...result, actualTokens: result.tokens };
4460+
}
4461+
43514462
const fileContent = preloadedContent ?? await fs.promises.readFile(sessionFilePath, 'utf8');
43524463

43534464
// Check if this is a UUID-only file (new Copilot CLI format)

0 commit comments

Comments
 (0)