Skip to content

Commit 77123ec

Browse files
rajbosCopilot
andcommitted
refactor: Phase 2 - collapse getSessionLogData if-chains into adapter pattern
- Replace 6-ecosystem buildTurns if-chain in getSessionLogData() with adapter delegation (~450 lines -> ~20 lines) - Fix ClaudeDesktopAdapter constructor call with required callbacks - Replace openRawFile VS-specific handler with adapter getRawFileContent() - Fix detectEditorSource to use findEcosystem() instead of isOpenCodeSessionFile() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1247fcf commit 77123ec

8 files changed

Lines changed: 417 additions & 470 deletions

File tree

vscode-extension/src/adapters/claudeDesktopAdapter.ts

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import * as fs from 'fs';
2-
import type { ModelUsage } from '../types';
2+
import type { ModelUsage, ChatTurn, ActualUsage } from '../types';
33
import type { IEcosystemAdapter } from '../ecosystemAdapter';
44
import { ClaudeDesktopCoworkDataAccess } from '../claudedesktop';
5+
import { createEmptyContextRefs } from '../tokenEstimation';
56

67
export class ClaudeDesktopAdapter implements IEcosystemAdapter {
78
readonly id = 'claudedesktop';
89
readonly displayName = 'Claude Desktop Cowork';
910

10-
constructor(private readonly claudeDesktopCowork: ClaudeDesktopCoworkDataAccess) {}
11+
constructor(
12+
private readonly claudeDesktopCowork: ClaudeDesktopCoworkDataAccess,
13+
private readonly isMcpToolFn: (toolName: string) => boolean,
14+
private readonly extractMcpServerNameFn: (toolName: string) => string,
15+
private readonly estimateTokensFn: (text: string, model?: string) => number
16+
) {}
1117

1218
handles(sessionFile: string): boolean {
1319
return this.claudeDesktopCowork.isCoworkSessionFile(sessionFile);
@@ -47,4 +53,91 @@ export class ClaudeDesktopAdapter implements IEcosystemAdapter {
4753
getEditorRoot(_sessionFile: string): string {
4854
return this.claudeDesktopCowork.getCoworkBaseDir();
4955
}
56+
57+
async buildTurns(sessionFile: string): Promise<{ turns: ChatTurn[]; actualTokens?: number }> {
58+
const turns: ChatTurn[] = [];
59+
const events = this.claudeDesktopCowork.readCoworkEvents(sessionFile);
60+
let currentUserEvent: any = null;
61+
const pendingAssistantEvents: any[] = [];
62+
63+
const emitTurn = () => {
64+
if (!currentUserEvent) { return; }
65+
const content = currentUserEvent.message?.content;
66+
const userMessage = typeof content === 'string' ? content
67+
: Array.isArray(content) ? content.filter((c: any) => c.type === 'text').map((c: any) => c.text || '').join('\n')
68+
: '';
69+
let assistantText = '';
70+
let actualInputTokens = 0;
71+
let actualOutputTokens = 0;
72+
let model: string | null = null;
73+
const toolCalls: { toolName: string; arguments?: string }[] = [];
74+
const mcpTools: { server: string; tool: string }[] = [];
75+
76+
for (const ae of pendingAssistantEvents) {
77+
const msg = ae.message;
78+
if (!model && msg?.model) { model = msg.model; }
79+
const usage = msg?.usage;
80+
if (usage) {
81+
actualInputTokens += (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
82+
actualOutputTokens += usage.output_tokens || 0;
83+
}
84+
const contentArr: any[] = Array.isArray(msg?.content) ? msg.content : [];
85+
for (const block of contentArr) {
86+
if (block.type === 'text') { assistantText += block.text || ''; }
87+
else if (block.type === 'tool_use') {
88+
const toolName: string = block.name || 'unknown';
89+
if (this.isMcpToolFn(toolName)) {
90+
mcpTools.push({ server: this.extractMcpServerNameFn(toolName), tool: toolName });
91+
} else {
92+
toolCalls.push({ toolName, arguments: block.input ? JSON.stringify(block.input) : undefined });
93+
}
94+
}
95+
}
96+
}
97+
98+
const usedModel = model || 'claude-sonnet-4-6';
99+
const actualUsage: ActualUsage | undefined = (actualInputTokens > 0 || actualOutputTokens > 0) ? {
100+
promptTokens: actualInputTokens,
101+
completionTokens: actualOutputTokens
102+
} : undefined;
103+
104+
turns.push({
105+
turnNumber: turns.length + 1,
106+
timestamp: currentUserEvent.timestamp ? new Date(currentUserEvent.timestamp).toISOString() : null,
107+
mode: 'agent',
108+
userMessage,
109+
assistantResponse: assistantText,
110+
model: usedModel,
111+
toolCalls,
112+
contextReferences: createEmptyContextRefs(),
113+
mcpTools,
114+
inputTokensEstimate: actualInputTokens || this.estimateTokensFn(userMessage, usedModel),
115+
outputTokensEstimate: actualOutputTokens || this.estimateTokensFn(assistantText, usedModel),
116+
thinkingTokensEstimate: 0,
117+
actualUsage
118+
});
119+
};
120+
121+
const isRealUserMessage = (event: any): boolean => {
122+
const content = event.message?.content;
123+
if (typeof content === 'string') { return !!content.trim(); }
124+
if (!Array.isArray(content)) { return false; }
125+
const hasText = content.some((c: any) => c.type === 'text');
126+
const hasToolResult = content.some((c: any) => c.type === 'tool_result');
127+
return hasText && !hasToolResult;
128+
};
129+
130+
for (const event of events) {
131+
if (event.type === 'user' && !event.isSidechain && event.message?.role === 'user' && isRealUserMessage(event)) {
132+
emitTurn();
133+
currentUserEvent = event;
134+
pendingAssistantEvents.length = 0;
135+
} else if (event.type === 'assistant' && event.message?.stop_reason && event.message?.role === 'assistant') {
136+
pendingAssistantEvents.push(event);
137+
}
138+
}
139+
emitTurn();
140+
141+
return { turns };
142+
}
50143
}

vscode-extension/src/adapters/continueAdapter.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as fs from 'fs';
2-
import type { ModelUsage } from '../types';
2+
import type { ModelUsage, ChatTurn } from '../types';
33
import type { IEcosystemAdapter } from '../ecosystemAdapter';
44
import { ContinueDataAccess } from '../continue';
5+
import { createEmptyContextRefs } from '../tokenEstimation';
56

67
export class ContinueAdapter implements IEcosystemAdapter {
78
readonly id = 'continue';
@@ -64,4 +65,27 @@ export class ContinueAdapter implements IEcosystemAdapter {
6465
getEditorRoot(_sessionFile: string): string {
6566
return this.continue_.getContinueDataDir();
6667
}
68+
69+
async buildTurns(sessionFile: string): Promise<{ turns: ChatTurn[]; actualTokens?: number }> {
70+
const turns: ChatTurn[] = [];
71+
const continueTurns = this.continue_.buildContinueTurns(sessionFile);
72+
const emptyContextRefs = createEmptyContextRefs();
73+
for (const ct of continueTurns) {
74+
turns.push({
75+
turnNumber: turns.length + 1,
76+
timestamp: null,
77+
mode: 'ask',
78+
userMessage: ct.userText,
79+
assistantResponse: ct.assistantText,
80+
model: ct.model,
81+
toolCalls: ct.toolCalls,
82+
contextReferences: emptyContextRefs,
83+
mcpTools: [],
84+
inputTokensEstimate: ct.inputTokens,
85+
outputTokensEstimate: ct.outputTokens,
86+
thinkingTokensEstimate: 0
87+
});
88+
}
89+
return { turns };
90+
}
6791
}

vscode-extension/src/adapters/crushAdapter.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import type { ModelUsage } from '../types';
3+
import type { ModelUsage, ChatTurn } from '../types';
44
import type { IEcosystemAdapter } from '../ecosystemAdapter';
55
import { CrushDataAccess } from '../crush';
6+
import { createEmptyContextRefs } from '../tokenEstimation';
67

78
export class CrushAdapter implements IEcosystemAdapter {
89
readonly id = 'crush';
@@ -60,4 +61,62 @@ export class CrushAdapter implements IEcosystemAdapter {
6061
getEditorRoot(sessionFile: string): string {
6162
return path.dirname(this.crush.getCrushDbPath(sessionFile));
6263
}
64+
65+
async buildTurns(sessionFile: string): Promise<{ turns: ChatTurn[]; actualTokens?: number }> {
66+
const turns: ChatTurn[] = [];
67+
const messages = await this.crush.getCrushMessages(sessionFile);
68+
const session = await this.crush.readCrushSession(sessionFile);
69+
const userMessages = messages.filter(m => m.role === 'user');
70+
const numTurns = userMessages.length;
71+
let turnNumber = 0;
72+
for (let i = 0; i < messages.length; i++) {
73+
const msg = messages[i];
74+
if (msg.role !== 'user') { continue; }
75+
turnNumber++;
76+
const turnAssistantMsgs: any[] = [];
77+
for (let j = i + 1; j < messages.length; j++) {
78+
if (messages[j].role === 'user') { break; }
79+
if (messages[j].role === 'assistant') { turnAssistantMsgs.push(messages[j]); }
80+
}
81+
const userParts: any[] = Array.isArray(msg.parts) ? msg.parts : [];
82+
const userText = userParts
83+
.filter(p => p?.type === 'text' && p?.text)
84+
.map(p => p.text as string)
85+
.join('\n');
86+
let assistantText = '';
87+
const toolCalls: { toolName: string; arguments?: string; result?: string }[] = [];
88+
let model: string | null = null;
89+
for (const assistantMsg of turnAssistantMsgs) {
90+
if (!model) { model = assistantMsg.model || null; }
91+
const parts: any[] = Array.isArray(assistantMsg.parts) ? assistantMsg.parts : [];
92+
for (const part of parts) {
93+
if (part?.type === 'text' && part?.text) {
94+
assistantText += part.text;
95+
} else if (part?.type === 'tool_call' && part?.data?.name) {
96+
toolCalls.push({
97+
toolName: part.data.name,
98+
arguments: part.data.arguments ? JSON.stringify(part.data.arguments) : undefined
99+
});
100+
}
101+
}
102+
}
103+
const perTurnInput = session?.prompt_tokens && numTurns > 0 ? Math.round(session.prompt_tokens / numTurns) : 0;
104+
const perTurnOutput = session?.completion_tokens && numTurns > 0 ? Math.round(session.completion_tokens / numTurns) : 0;
105+
turns.push({
106+
turnNumber,
107+
timestamp: msg.created_at ? new Date(msg.created_at * 1000).toISOString() : null,
108+
mode: 'agent',
109+
userMessage: userText,
110+
assistantResponse: assistantText,
111+
model,
112+
toolCalls,
113+
contextReferences: createEmptyContextRefs(),
114+
mcpTools: [],
115+
inputTokensEstimate: perTurnInput,
116+
outputTokensEstimate: perTurnOutput,
117+
thinkingTokensEstimate: 0
118+
});
119+
}
120+
return { turns };
121+
}
63122
}

vscode-extension/src/adapters/mistralVibeAdapter.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as fs from 'fs';
2-
import type { ModelUsage } from '../types';
2+
import type { ModelUsage, ChatTurn } from '../types';
33
import type { IEcosystemAdapter } from '../ecosystemAdapter';
44
import { MistralVibeDataAccess } from '../mistralvibe';
5+
import { createEmptyContextRefs } from '../tokenEstimation';
56

67
export class MistralVibeAdapter implements IEcosystemAdapter {
78
readonly id = 'mistralvibe';
@@ -46,4 +47,63 @@ export class MistralVibeAdapter implements IEcosystemAdapter {
4647
getEditorRoot(_sessionFile: string): string {
4748
return this.mistralVibe.getSessionLogDir();
4849
}
50+
51+
async buildTurns(sessionFile: string): Promise<{ turns: ChatTurn[]; actualTokens?: number }> {
52+
const turns: ChatTurn[] = [];
53+
const messages = this.mistralVibe.readSessionMessages(sessionFile);
54+
const sessionMeta = this.mistralVibe.getSessionMeta(sessionFile);
55+
const tokenData = this.mistralVibe.getTokensFromSession(sessionFile);
56+
const model: string = sessionMeta.model || 'devstral';
57+
58+
const userMsgIndices: number[] = [];
59+
for (let i = 0; i < messages.length; i++) {
60+
if (messages[i].role === 'user' && messages[i].injected !== true) {
61+
userMsgIndices.push(i);
62+
}
63+
}
64+
65+
for (let t = 0; t < userMsgIndices.length; t++) {
66+
const userIdx = userMsgIndices[t];
67+
const nextUserIdx = t + 1 < userMsgIndices.length ? userMsgIndices[t + 1] : messages.length;
68+
const userMsg = messages[userIdx];
69+
const userText = typeof userMsg.content === 'string' ? userMsg.content : '';
70+
let assistantText = '';
71+
const toolCalls: { toolName: string; arguments?: string; result?: string }[] = [];
72+
73+
for (let j = userIdx + 1; j < nextUserIdx; j++) {
74+
const msg = messages[j];
75+
if (msg.role === 'assistant') {
76+
if (typeof msg.content === 'string') { assistantText += msg.content; }
77+
if (Array.isArray(msg.tool_calls)) {
78+
for (const tc of msg.tool_calls) {
79+
toolCalls.push({
80+
toolName: tc.function?.name || tc.name || 'unknown',
81+
arguments: tc.function?.arguments ? JSON.stringify(tc.function.arguments) : undefined
82+
});
83+
}
84+
}
85+
} else if (msg.role === 'tool') {
86+
const last = toolCalls[toolCalls.length - 1];
87+
if (last) { last.result = typeof msg.content === 'string' ? msg.content : undefined; }
88+
}
89+
}
90+
91+
turns.push({
92+
turnNumber: t + 1,
93+
timestamp: sessionMeta.firstInteraction,
94+
mode: 'agent',
95+
userMessage: userText,
96+
assistantResponse: assistantText,
97+
model,
98+
toolCalls,
99+
contextReferences: createEmptyContextRefs(),
100+
mcpTools: [],
101+
inputTokensEstimate: 0,
102+
outputTokensEstimate: 0,
103+
thinkingTokensEstimate: 0
104+
});
105+
}
106+
107+
return { turns, actualTokens: tokenData.tokens };
108+
}
49109
}

vscode-extension/src/adapters/openCodeAdapter.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3-
import type { ModelUsage } from '../types';
3+
import type { ModelUsage, ChatTurn } from '../types';
44
import type { IEcosystemAdapter } from '../ecosystemAdapter';
55
import { OpenCodeDataAccess } from '../opencode';
6+
import { createEmptyContextRefs } from '../tokenEstimation';
67

78
export class OpenCodeAdapter implements IEcosystemAdapter {
89
readonly id = 'opencode';
@@ -78,4 +79,68 @@ export class OpenCodeAdapter implements IEcosystemAdapter {
7879
getEditorRoot(_sessionFile: string): string {
7980
return this.openCode.getOpenCodeDataDir();
8081
}
82+
83+
async buildTurns(sessionFile: string): Promise<{ turns: ChatTurn[]; actualTokens?: number }> {
84+
const turns: ChatTurn[] = [];
85+
const messages = await this.openCode.getOpenCodeMessagesForSession(sessionFile);
86+
if (messages.length > 0) {
87+
let turnNumber = 0;
88+
let prevCumulativeTotal = 0;
89+
for (let i = 0; i < messages.length; i++) {
90+
const msg = messages[i];
91+
if (msg.role !== 'user') { continue; }
92+
turnNumber++;
93+
const turnAssistantMsgs = messages.filter((m, idx) => idx > i && m.role === 'assistant' && m.parentID === msg.id);
94+
const userParts = await this.openCode.getOpenCodePartsForMessage(msg.id);
95+
const userText = userParts.filter(p => p.type === 'text').map(p => p.text || '').join('\n');
96+
let assistantText = '';
97+
const toolCalls: { toolName: string; arguments?: string; result?: string }[] = [];
98+
let model: string | null = null;
99+
let thinkingTokens = 0;
100+
101+
let turnCumulativeTotal = prevCumulativeTotal;
102+
for (const assistantMsg of turnAssistantMsgs) {
103+
if (!model) { model = assistantMsg.modelID || null; }
104+
thinkingTokens += assistantMsg.tokens?.reasoning || 0;
105+
if (typeof assistantMsg.tokens?.total === 'number') {
106+
turnCumulativeTotal = Math.max(turnCumulativeTotal, assistantMsg.tokens.total);
107+
}
108+
const assistantParts = await this.openCode.getOpenCodePartsForMessage(assistantMsg.id);
109+
for (const part of assistantParts) {
110+
if (part.type === 'text' && part.text) {
111+
assistantText += part.text;
112+
} else if (part.type === 'tool' && part.tool) {
113+
toolCalls.push({
114+
toolName: part.tool,
115+
arguments: part.state?.input ? JSON.stringify(part.state.input) : undefined,
116+
result: part.state?.output || undefined
117+
});
118+
}
119+
}
120+
}
121+
122+
const turnTokens = turnCumulativeTotal - prevCumulativeTotal;
123+
const turnOutputAndThinking = turnAssistantMsgs.reduce((sum, m) => sum + (m.tokens?.output || 0) + (m.tokens?.reasoning || 0), 0);
124+
const turnInputTokens = Math.max(0, turnTokens - turnOutputAndThinking);
125+
126+
turns.push({
127+
turnNumber,
128+
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
129+
mode: (msg.agent === 'build' || msg.agent === 'agent') ? 'agent' : (msg.agent === 'ask' ? 'ask' : 'agent'),
130+
userMessage: userText,
131+
assistantResponse: assistantText,
132+
model,
133+
toolCalls,
134+
contextReferences: createEmptyContextRefs(),
135+
mcpTools: [],
136+
inputTokensEstimate: turnInputTokens,
137+
outputTokensEstimate: turnOutputAndThinking - thinkingTokens,
138+
thinkingTokensEstimate: thinkingTokens
139+
});
140+
141+
prevCumulativeTotal = turnCumulativeTotal;
142+
}
143+
}
144+
return { turns };
145+
}
81146
}

0 commit comments

Comments
 (0)