diff --git a/.gitignore b/.gitignore index 5804c26..42d6038 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules .github/scripts/models.txt .github/scripts/scraper.log .github/scripts/scraped-models.json +example.json diff --git a/src/extension.ts b/src/extension.ts index 12861b7..332e3f2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -111,6 +111,11 @@ interface ContextReferenceUsage { workspace: number; // @workspace references terminal: number; // @terminal references vscode: number; // @vscode references + // contentReferences tracking from session logs + byKind: { [kind: string]: number }; // Count by reference kind + copilotInstructions: number; // .github/copilot-instructions.md + agentsMd: number; // agents.md in repo root + byPath: { [path: string]: number }; // Count by unique file path } interface McpToolUsage { @@ -196,7 +201,7 @@ interface SessionLogData { class CopilotTokenTracker implements vscode.Disposable { // Cache version - increment this when making changes that require cache invalidation - private static readonly CACHE_VERSION = 10; // Add file size to cache for faster validation (2026-02-02) + private static readonly CACHE_VERSION = 11; // Fix toolId extraction for tool calls (2026-02-05) private diagnosticsPanel?: vscode.WebviewPanel; // Tracks whether the diagnostics panel has already received its session files @@ -1011,7 +1016,11 @@ class CopilotTokenTracker implements vscode.Disposable { codebase: 0, workspace: 0, terminal: 0, - vscode: 0 + vscode: 0, + byKind: {}, + copilotInstructions: 0, + agentsMd: 0, + byPath: {} }, mcpTools: { total: 0, byServer: {}, byTool: {} }, modelSwitching: { @@ -1114,6 +1123,20 @@ class CopilotTokenTracker implements vscode.Disposable { period.contextReferences.workspace += analysis.contextReferences.workspace; period.contextReferences.terminal += analysis.contextReferences.terminal; period.contextReferences.vscode += analysis.contextReferences.vscode; + + // Merge contentReferences counts + period.contextReferences.copilotInstructions += analysis.contextReferences.copilotInstructions || 0; + period.contextReferences.agentsMd += analysis.contextReferences.agentsMd || 0; + + // Merge byKind tracking + for (const [kind, count] of Object.entries(analysis.contextReferences.byKind || {})) { + period.contextReferences.byKind[kind] = (period.contextReferences.byKind[kind] || 0) + count; + } + + // Merge byPath tracking + for (const [path, count] of Object.entries(analysis.contextReferences.byPath || {})) { + period.contextReferences.byPath[path] = (period.contextReferences.byPath[path] || 0) + count; + } // Merge MCP tools period.mcpTools.total += analysis.mcpTools.total; @@ -1394,7 +1417,11 @@ class CopilotTokenTracker implements vscode.Disposable { codebase: 0, workspace: 0, terminal: 0, - vscode: 0 + vscode: 0, + byKind: {}, + copilotInstructions: 0, + agentsMd: 0, + byPath: {} }, mcpTools: { total: 0, byServer: {}, byTool: {} }, modelSwitching: { @@ -1459,21 +1486,37 @@ class CopilotTokenTracker implements vscode.Disposable { analysis.toolCalls.total++; analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; } + + // Analyze contentReferences if present + if (request.contentReferences && Array.isArray(request.contentReferences)) { + this.analyzeContentReferences(request.contentReferences, analysis.contextReferences); + } + + // Extract tool calls from request.response array (when full request is added) + if (request.response && Array.isArray(request.response)) { + for (const responseItem of request.response) { + if (responseItem.kind === 'toolInvocationSerialized' || responseItem.kind === 'prepareToolInvocation') { + analysis.toolCalls.total++; + const toolName = responseItem.toolId || responseItem.toolName || responseItem.invocationMessage?.toolName || responseItem.toolSpecificData?.kind || 'unknown'; + analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; + } + } + } } } - - // Handle VS Code incremental format - tool invocations in responses - if (event.kind === 2 && event.k?.includes('response') && Array.isArray(event.v)) { - for (const responseItem of event.v) { - if (responseItem.kind === 'toolInvocationSerialized') { - analysis.toolCalls.total++; - const toolName = responseItem.toolSpecificData?.kind || 'unknown'; - analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; - } + + // Handle VS Code incremental format - tool invocations in responses + if (event.kind === 2 && event.k?.includes('response') && Array.isArray(event.v)) { + for (const responseItem of event.v) { + if (responseItem.kind === 'toolInvocationSerialized') { + analysis.toolCalls.total++; + const toolName = responseItem.toolId || responseItem.toolName || responseItem.invocationMessage?.toolName || responseItem.toolSpecificData?.kind || 'unknown'; + analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; } } - - // Handle Copilot CLI format + } + + // Handle Copilot CLI format // Detect mode from event type - CLI can be chat or agent mode if (event.type === 'user.message') { analysis.modeUsage.ask++; @@ -1570,6 +1613,10 @@ class CopilotTokenTracker implements vscode.Disposable { // Analyze variableData for @workspace, @terminal, @vscode references if (request.variableData) { + // Process variables array for prompt files and other context + this.analyzeVariableData(request.variableData, analysis.contextReferences); + + // Also check for @ references in variable names/values const varDataStr = JSON.stringify(request.variableData).toLowerCase(); if (varDataStr.includes('workspace')) { analysis.contextReferences.workspace++; @@ -1582,6 +1629,21 @@ class CopilotTokenTracker implements vscode.Disposable { } } + // Analyze contentReferences if present + if (request.contentReferences && Array.isArray(request.contentReferences)) { + this.analyzeContentReferences(request.contentReferences, analysis.contextReferences); + } + + // Analyze variableData if present + if (request.variableData) { + this.analyzeVariableData(request.variableData, analysis.contextReferences); + } + + // Analyze variableData if present + if (request.variableData) { + this.analyzeVariableData(request.variableData, analysis.contextReferences); + } + // Analyze response for tool calls and MCP tools if (request.response && Array.isArray(request.response)) { for (const responseItem of request.response) { @@ -1589,7 +1651,8 @@ class CopilotTokenTracker implements vscode.Disposable { if (responseItem.kind === 'toolInvocationSerialized' || responseItem.kind === 'prepareToolInvocation') { analysis.toolCalls.total++; - const toolName = responseItem.toolName || + const toolName = responseItem.toolId || + responseItem.toolName || responseItem.invocationMessage?.toolName || 'unknown'; analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; @@ -1602,49 +1665,30 @@ class CopilotTokenTracker implements vscode.Disposable { analysis.mcpTools.byServer[serverId] = (analysis.mcpTools.byServer[serverId] || 0) + 1; } } - } - } - - // Check metadata for tool calls - if (request.result?.metadata) { - const metadataStr = JSON.stringify(request.result.metadata).toLowerCase(); - // Look for tool-related metadata - if (metadataStr.includes('tool') || metadataStr.includes('function')) { - // This is a heuristic - actual structure may vary - try { - const metadata = request.result.metadata; - if (metadata.toolCalls || metadata.tools || metadata.functionCalls) { - const toolData = metadata.toolCalls || metadata.tools || metadata.functionCalls; - if (Array.isArray(toolData)) { - for (const toolItem of toolData) { - analysis.toolCalls.total++; - // Try to extract tool name from various possible fields - const toolName = toolItem.name || toolItem.function?.name || toolItem.toolName || 'metadata-tool'; - analysis.toolCalls.byTool[toolName] = (analysis.toolCalls.byTool[toolName] || 0) + 1; - } - } - } - } catch (e) { - // Ignore parsing errors - } + + // Detect inline references in response items + if (responseItem.kind === 'inlineReference' && responseItem.inlineReference) { + // Treat response inlineReferences as contentReferences + this.analyzeContentReferences([responseItem], analysis.contextReferences); } } } } - } catch (error) { - this.warn(`Error analyzing session usage from ${sessionFile}: ${error}`); } + } catch (error) { + this.warn(`Error analyzing session usage from ${sessionFile}: ${error}`); + } - // Calculate model switching statistics from session - await this.calculateModelSwitching(sessionFile, analysis); + // Calculate model switching statistics from session + await this.calculateModelSwitching(sessionFile, analysis); - return analysis; - } + return analysis; +} - /** - * Calculate model switching statistics for a session file. - * This method updates the analysis.modelSwitching field in place. - */ +/** + * Calculate model switching statistics for a session file. + * This method updates the analysis.modelSwitching field in place. + */ private async calculateModelSwitching(sessionFile: string, analysis: SessionUsageAnalysis): Promise { try { // Use non-cached method to avoid circular dependency @@ -1753,14 +1797,117 @@ class CopilotTokenTracker implements vscode.Disposable { } } + /** + * Analyze contentReferences from session log data to track specific file attachments. + * Looks for kind: "reference" entries and tracks by kind, path patterns. + * Also increments specific category counters like refs.file when appropriate. + */ + private analyzeContentReferences(contentReferences: any[], refs: ContextReferenceUsage): void { + if (!Array.isArray(contentReferences)) { + return; + } + + for (const contentRef of contentReferences) { + if (!contentRef || typeof contentRef !== 'object') { + continue; + } + + // Track by kind + const kind = contentRef.kind; + if (typeof kind === 'string') { + refs.byKind[kind] = (refs.byKind[kind] || 0) + 1; + } + + // Extract reference object based on kind + let reference = null; + + // Handle different reference structures + if (kind === 'reference' && contentRef.reference) { + reference = contentRef.reference; + } else if (kind === 'inlineReference' && contentRef.inlineReference) { + reference = contentRef.inlineReference; + } + + // Process the reference if found + if (reference) { + // Try to extract file path from various possible fields + const fsPath = reference.fsPath || reference.path; + if (typeof fsPath === 'string') { + // Normalize path separators for pattern matching + const normalizedPath = fsPath.replace(/\\/g, '/').toLowerCase(); + + // Track specific patterns + if (normalizedPath.endsWith('/.github/copilot-instructions.md') || + normalizedPath.includes('.github/copilot-instructions.md')) { + refs.copilotInstructions++; + } else if (normalizedPath.endsWith('/agents.md') || + normalizedPath.match(/\/agents\.md$/i)) { + refs.agentsMd++; + } else { + // For other files, increment the general file counter + // This makes actual file attachments show up in context ref counts + refs.file++; + } + + // Track by full path (limit to last 100 chars for display) + const pathKey = fsPath.length > 100 ? '...' + fsPath.substring(fsPath.length - 97) : fsPath; + refs.byPath[pathKey] = (refs.byPath[pathKey] || 0) + 1; + } + } + } + } + + /** + * Analyze variableData to track prompt file attachments and other variable-based context. + * This captures automatic attachments like copilot-instructions.md via variable system. + */ + private analyzeVariableData(variableData: any, refs: ContextReferenceUsage): void { + if (!variableData || !Array.isArray(variableData.variables)) { + return; + } + + for (const variable of variableData.variables) { + if (!variable || typeof variable !== 'object') { + continue; + } + + // Track by kind from variableData + const kind = variable.kind; + if (typeof kind === 'string') { + refs.byKind[kind] = (refs.byKind[kind] || 0) + 1; + } + + // Process promptFile variables that contain file references + if (kind === 'promptFile' && variable.value) { + const value = variable.value; + const fsPath = value.fsPath || value.path || value.external; + + if (typeof fsPath === 'string') { + const normalizedPath = fsPath.replace(/\\/g, '/').toLowerCase(); + + // Track specific patterns (but don't double-count if already in contentReferences) + if (normalizedPath.endsWith('/.github/copilot-instructions.md') || + normalizedPath.includes('.github/copilot-instructions.md')) { + // copilotInstructions - tracked via contentReferences, skip here to avoid double counting + } else if (normalizedPath.endsWith('/agents.md') || + normalizedPath.match(/\/agents\.md$/i)) { + // agents.md - tracked via contentReferences, skip here to avoid double counting + } + // Note: We don't add to byPath here as these are automatic attachments, + // not explicit user file selections + } + } + } + } + // Cached versions of session file reading methods private async getSessionFileDataCached(sessionFilePath: string, mtime: number, fileSize: number): Promise { - // Check if we have valid cached data - const cached = this.getCachedSessionData(sessionFilePath); - if (cached && cached.mtime === mtime && cached.size === fileSize) { - this._cacheHits++; - return cached; - } + // Check if we have valid cached data + const cached = this.getCachedSessionData(sessionFilePath); + if (cached && cached.mtime === mtime && cached.size === fileSize) { + this._cacheHits++; + return cached; + } this._cacheMisses++; // Cache miss - read and process the file once to get all data @@ -1809,7 +1956,11 @@ class CopilotTokenTracker implements vscode.Disposable { codebase: 0, workspace: 0, terminal: 0, - vscode: 0 + vscode: 0, + byKind: {}, + copilotInstructions: 0, + agentsMd: 0, + byPath: {} }, mcpTools: { total: 0, byServer: {}, byTool: {} }, modelSwitching: { @@ -1848,7 +1999,8 @@ class CopilotTokenTracker implements vscode.Disposable { interactions: 0, contextReferences: { file: 0, selection: 0, implicitSelection: 0, symbol: 0, codebase: 0, - workspace: 0, terminal: 0, vscode: 0 + workspace: 0, terminal: 0, vscode: 0, + byKind: {}, copilotInstructions: 0, agentsMd: 0, byPath: {} }, firstInteraction: null, lastInteraction: null, @@ -2053,143 +2205,147 @@ class CopilotTokenTracker implements vscode.Disposable { try { const fileContent = await fs.promises.readFile(sessionFile, 'utf8'); - if (sessionFile.endsWith('.jsonl')) { - // Handle JSONL formats (CLI and VS Code incremental) - const lines = fileContent.trim().split('\n'); - let turnNumber = 0; - let sessionMode: 'ask' | 'edit' | 'agent' = 'ask'; - let currentModel: string | null = null; - - // For VS Code incremental format, we need to accumulate requests - const pendingRequests: Map = new Map(); + // Check for JSONL content (either by extension or content detection) + const isJsonlContent = sessionFile.endsWith('.jsonl') || this.isJsonlContent(fileContent); + + if (isJsonlContent) { + // Handle JSONL formats (CLI and VS Code incremental/delta-based) + const lines = fileContent.trim().split('\n').filter(l => l.trim()); - for (const line of lines) { - if (!line.trim()) { continue; } + // Detect if this is delta-based format (VS Code incremental) + let isDeltaBased = false; + if (lines.length > 0) { try { - const event = JSON.parse(line); - - // Handle VS Code incremental format - detect mode from session header - if (event.kind === 0 && event.v?.inputState?.mode?.kind) { - sessionMode = event.v.inputState.mode.kind as 'ask' | 'edit' | 'agent'; - if (event.v.inputState.selectedModel?.metadata?.id) { - currentModel = event.v.inputState.selectedModel.metadata.id; - } + const firstLine = JSON.parse(lines[0]); + if (firstLine && typeof firstLine.kind === 'number') { + isDeltaBased = true; } - - // Handle mode changes - if (event.kind === 1 && event.k?.includes('mode') && event.v?.kind) { - sessionMode = event.v.kind as 'ask' | 'edit' | 'agent'; + } catch { + // Not delta format + } + } + + if (isDeltaBased) { + // Delta-based format: reconstruct full state first, then extract turns + let sessionState: any = {}; + for (const line of lines) { + try { + const delta = JSON.parse(line); + sessionState = this.applyDelta(sessionState, delta); + } catch { + // Skip invalid lines } + } + + // Extract session-level info + let sessionMode: 'ask' | 'edit' | 'agent' = 'ask'; + let currentModel: string | null = null; + + if (sessionState.inputState?.mode?.kind) { + sessionMode = sessionState.inputState.mode.kind as 'ask' | 'edit' | 'agent'; + } + if (sessionState.inputState?.selectedModel?.metadata?.id) { + currentModel = sessionState.inputState.selectedModel.metadata.id; + } + + // Extract turns from reconstructed requests array + const requests = sessionState.requests || []; + for (let i = 0; i < requests.length; i++) { + const request = requests[i]; + if (!request || !request.requestId) { continue; } - // Handle model changes - if (event.kind === 1 && event.k?.includes('selectedModel') && event.v?.metadata?.id) { - currentModel = event.v.metadata.id; - } + const contextRefs = this.createEmptyContextRefs(); + const userMessage = request.message?.text || ''; + this.analyzeContextReferences(userMessage, contextRefs); - // Handle VS Code incremental format - new requests - if (event.kind === 2 && event.k?.[0] === 'requests' && Array.isArray(event.v)) { - for (const request of event.v) { - if (request.requestId) { - turnNumber++; - const contextRefs = this.createEmptyContextRefs(); - const userMessage = request.message?.text || ''; - this.analyzeContextReferences(userMessage, contextRefs); - - const turn: ChatTurn = { - turnNumber, - timestamp: request.timestamp ? new Date(request.timestamp).toISOString() : null, - mode: sessionMode, - userMessage, - assistantResponse: '', - model: currentModel, - toolCalls: [], - contextReferences: contextRefs, - mcpTools: [], - inputTokensEstimate: this.estimateTokensFromText(userMessage, currentModel || 'gpt-4'), - outputTokensEstimate: 0 - }; - - // Process response if present - if (request.response && Array.isArray(request.response)) { - const { responseText, toolCalls, mcpTools } = this.extractResponseData(request.response); - turn.assistantResponse = responseText; - turn.toolCalls = toolCalls; - turn.mcpTools = mcpTools; - turn.outputTokensEstimate = this.estimateTokensFromText(responseText, currentModel || 'gpt-4'); - } - - pendingRequests.set(request.requestId, turn); - } - } + // Analyze contentReferences from request + if (request.contentReferences && Array.isArray(request.contentReferences)) { + this.analyzeContentReferences(request.contentReferences, contextRefs); } - // Handle VS Code incremental format - response updates - if (event.kind === 2 && event.k?.includes('response') && Array.isArray(event.v)) { - // Find the request this response belongs to - const requestIdPath = event.k?.find((k: string) => k.match(/^\d+$/)); - if (requestIdPath !== undefined) { - // This is updating an existing request's response - for (const turn of pendingRequests.values()) { - const { responseText, toolCalls, mcpTools } = this.extractResponseData(event.v); - if (responseText) { - turn.assistantResponse += responseText; - turn.outputTokensEstimate = this.estimateTokensFromText(turn.assistantResponse, turn.model || 'gpt-4'); - } - turn.toolCalls.push(...toolCalls); - turn.mcpTools.push(...mcpTools); - break; - } - } + // Analyze variableData from request + if (request.variableData) { + this.analyzeVariableData(request.variableData, contextRefs); } - // Handle Copilot CLI format (type: 'user.message') - if (event.type === 'user.message' && event.data?.content) { - turnNumber++; - const contextRefs = this.createEmptyContextRefs(); - const userMessage = event.data.content; - this.analyzeContextReferences(userMessage, contextRefs); - const turn: ChatTurn = { - turnNumber, - timestamp: event.timestamp ? new Date(event.timestamp).toISOString() : null, - mode: 'agent', // CLI is typically agent mode - userMessage, - assistantResponse: '', - model: event.model || 'gpt-4o', - toolCalls: [], - contextReferences: contextRefs, - mcpTools: [], - inputTokensEstimate: this.estimateTokensFromText(userMessage, event.model || 'gpt-4o'), - outputTokensEstimate: 0 - }; - turns.push(turn); - } + // Get model from request or fall back to session model + const requestModel = request.modelId || + currentModel || + this.getModelFromRequest(request) || + 'gpt-4'; - // Handle CLI assistant response - if (event.type === 'assistant.message' && event.data?.content && turns.length > 0) { - const lastTurn = turns[turns.length - 1]; - lastTurn.assistantResponse += event.data.content; - lastTurn.outputTokensEstimate = this.estimateTokensFromText(lastTurn.assistantResponse, lastTurn.model || 'gpt-4o'); - } + // Extract response data + const { responseText, toolCalls, mcpTools } = this.extractResponseData(request.response || []); - // Handle CLI tool calls - if ((event.type === 'tool.call' || event.type === 'tool.result') && turns.length > 0) { - const lastTurn = turns[turns.length - 1]; - const toolName = event.data?.toolName || event.toolName || 'unknown'; - lastTurn.toolCalls.push({ - toolName, - arguments: event.type === 'tool.call' ? JSON.stringify(event.data?.arguments || {}) : undefined, - result: event.type === 'tool.result' ? event.data?.output : undefined - }); + const turn: ChatTurn = { + turnNumber: i + 1, + timestamp: request.timestamp ? new Date(request.timestamp).toISOString() : null, + mode: sessionMode, + userMessage, + assistantResponse: responseText, + model: requestModel, + toolCalls, + contextReferences: contextRefs, + mcpTools, + inputTokensEstimate: this.estimateTokensFromText(userMessage, requestModel), + outputTokensEstimate: this.estimateTokensFromText(responseText, requestModel) + }; + + turns.push(turn); + } + } else { + // Non-delta JSONL (Copilot CLI format) + let turnNumber = 0; + + for (const line of lines) { + try { + const event = JSON.parse(line); + + // Handle Copilot CLI format (type: 'user.message') + if (event.type === 'user.message' && event.data?.content) { + turnNumber++; + const contextRefs = this.createEmptyContextRefs(); + const userMessage = event.data.content; + this.analyzeContextReferences(userMessage, contextRefs); + const turn: ChatTurn = { + turnNumber, + timestamp: event.timestamp ? new Date(event.timestamp).toISOString() : null, + mode: 'agent', // CLI is typically agent mode + userMessage, + assistantResponse: '', + model: event.model || 'gpt-4o', + toolCalls: [], + contextReferences: contextRefs, + mcpTools: [], + inputTokensEstimate: this.estimateTokensFromText(userMessage, event.model || 'gpt-4o'), + outputTokensEstimate: 0 + }; + turns.push(turn); + } + + // Handle CLI assistant response + if (event.type === 'assistant.message' && event.data?.content && turns.length > 0) { + const lastTurn = turns[turns.length - 1]; + lastTurn.assistantResponse += event.data.content; + lastTurn.outputTokensEstimate = this.estimateTokensFromText(lastTurn.assistantResponse, lastTurn.model || 'gpt-4o'); + } + + // Handle CLI tool calls + if ((event.type === 'tool.call' || event.type === 'tool.result') && turns.length > 0) { + const lastTurn = turns[turns.length - 1]; + const toolName = event.data?.toolName || event.toolName || 'unknown'; + lastTurn.toolCalls.push({ + toolName, + arguments: event.type === 'tool.call' ? JSON.stringify(event.data?.arguments || {}) : undefined, + result: event.type === 'tool.result' ? event.data?.output : undefined + }); + } + } catch { + // Skip malformed lines } - } catch (e) { - // Skip malformed lines } } - // Add pending requests to turns - turns.push(...pendingRequests.values()); - turns.sort((a, b) => a.turnNumber - b.turnNumber); - } else { // Handle regular .json files const sessionContent = JSON.parse(fileContent); @@ -2309,7 +2465,8 @@ class CopilotTokenTracker implements vscode.Disposable { private createEmptyContextRefs(): ContextReferenceUsage { return { file: 0, selection: 0, implicitSelection: 0, symbol: 0, codebase: 0, - workspace: 0, terminal: 0, vscode: 0 + workspace: 0, terminal: 0, vscode: 0, + byKind: {}, copilotInstructions: 0, agentsMd: 0, byPath: {} }; } @@ -2335,7 +2492,7 @@ class CopilotTokenTracker implements vscode.Disposable { // Extract tool invocations if (item.kind === 'toolInvocationSerialized' || item.kind === 'prepareToolInvocation') { - const toolName = item.toolName || item.invocationMessage?.toolName || item.toolSpecificData?.kind || 'unknown'; + const toolName = item.toolId || item.toolName || item.invocationMessage?.toolName || item.toolSpecificData?.kind || 'unknown'; toolCalls.push({ toolName, arguments: item.input ? JSON.stringify(item.input) : undefined, @@ -2804,6 +2961,94 @@ class CopilotTokenTracker implements vscode.Disposable { secondLine.startsWith('{') && secondLine.endsWith('}'); } + /** + * Apply a delta to reconstruct session state from delta-based JSONL format. + * VS Code Insiders uses this format where: + * - kind: 0 = initial state (full replacement) + * - kind: 1 = update value at key path + * - kind: 2 = append to array at key path + * - k = key path (array of strings) + * - v = value + */ + private applyDelta(state: any, delta: any): any { + if (typeof delta !== 'object' || delta === null) { + return state; + } + + const { kind, k, v } = delta; + + if (kind === 0) { + // Initial state - full replacement + return v; + } + + if (!Array.isArray(k) || k.length === 0) { + return state; + } + + const pathArr = k.map(String); + let root = typeof state === 'object' && state !== null ? state : {}; + let current: any = root; + + // Traverse to the parent of the target location + for (let i = 0; i < pathArr.length - 1; i++) { + const seg = pathArr[i]; + const nextSeg = pathArr[i + 1]; + const wantsArray = /^\d+$/.test(nextSeg); + + if (Array.isArray(current)) { + const idx = Number(seg); + if (!current[idx] || typeof current[idx] !== 'object') { + current[idx] = wantsArray ? [] : {}; + } + current = current[idx]; + } else { + if (!current[seg] || typeof current[seg] !== 'object') { + current[seg] = wantsArray ? [] : {}; + } + current = current[seg]; + } + } + + const lastSeg = pathArr[pathArr.length - 1]; + + if (kind === 1) { + // Set value at key path + if (Array.isArray(current)) { + current[Number(lastSeg)] = v; + } else { + current[lastSeg] = v; + } + return root; + } + + if (kind === 2) { + // Append value(s) to array at key path + let target: any[]; + if (Array.isArray(current)) { + const idx = Number(lastSeg); + if (!Array.isArray(current[idx])) { + current[idx] = []; + } + target = current[idx]; + } else { + if (!Array.isArray(current[lastSeg])) { + current[lastSeg] = []; + } + target = current[lastSeg]; + } + + if (Array.isArray(v)) { + target.push(...v); + } else { + target.push(v); + } + return root; + } + + return root; + } + private getModelTier(modelId: string): 'standard' | 'premium' | 'unknown' { // Determine tier based on multiplier: 0 = standard, >0 = premium // Look up from modelPricing.json diff --git a/src/toolNames.json b/src/toolNames.json index e44fcd4..232c720 100644 --- a/src/toolNames.json +++ b/src/toolNames.json @@ -1,14 +1,17 @@ { "unknown": "Unknown", - "copilot_readFile": "Copilot: Read File", - "copilot_applyPatch": "Copilot: Apply Patch", - "copilot_findTextInFiles": "Copilot: Find Text In Files", + "copilot_readFile": "Read File", + "copilot_applyPatch": "Apply Patch", + "copilot_findTextInFiles": "Find Text In Files", "run_in_terminal": "Run In Terminal", "mcp.io.github.git.assign_copilot_to_issue": "Assign Copilot to Issue", "mcp.io.github.git.create_or_update_file": "Git: Create/Update File" ,"manage_todo_list": "Manage TODO List" - ,"copilot_replaceString": "Copilot: Replace String" - ,"copilot_createFile": "Copilot: Create File" - ,"copilot_listDirectory": "Copilot: List Directory" - ,"copilot_fetchWebPage": "Copilot: Fetch Web Page" + ,"copilot_replaceString": "Replace String" + ,"copilot_createFile": "Create File" + ,"copilot_listDirectory": "List Directory" + ,"copilot_fetchWebPage": "Fetch Web Page" + ,"copilot_getErrors": "Get Errors" + ,"copilot_multiReplaceString": "Multi Replace String" + ,"get_terminal_output": "Get Terminal Output" } diff --git a/src/webview/diagnostics/main.ts b/src/webview/diagnostics/main.ts index e34bf17..991726b 100644 --- a/src/webview/diagnostics/main.ts +++ b/src/webview/diagnostics/main.ts @@ -12,11 +12,16 @@ The view will automatically update when data is ready.`; type ContextReferenceUsage = { file: number; selection: number; + implicitSelection: number; symbol: number; codebase: number; workspace: number; terminal: number; vscode: number; + byKind: { [kind: string]: number }; + copilotInstructions: number; + agentsMd: number; + byPath: { [path: string]: number }; }; type SessionFileDetails = { @@ -145,19 +150,22 @@ function sanitizeNumber(value: number | undefined | null): string { } function getTotalContextRefs(refs: ContextReferenceUsage): number { - return refs.file + refs.selection + refs.symbol + refs.codebase + - refs.workspace + refs.terminal + refs.vscode; + return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + + refs.workspace + refs.terminal + refs.vscode + refs.copilotInstructions + refs.agentsMd; } function getContextRefsSummary(refs: ContextReferenceUsage): string { const parts: string[] = []; if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } if (refs.selection > 0) { parts.push(`#sel: ${refs.selection}`); } + if (refs.implicitSelection > 0) { parts.push(`impl: ${refs.implicitSelection}`); } if (refs.symbol > 0) { parts.push(`#sym: ${refs.symbol}`); } if (refs.codebase > 0) { parts.push(`#cb: ${refs.codebase}`); } if (refs.workspace > 0) { parts.push(`@ws: ${refs.workspace}`); } if (refs.terminal > 0) { parts.push(`@term: ${refs.terminal}`); } if (refs.vscode > 0) { parts.push(`@vsc: ${refs.vscode}`); } + if (refs.copilotInstructions > 0) { parts.push(`๐Ÿ“‹ inst: ${refs.copilotInstructions}`); } + if (refs.agentsMd > 0) { parts.push(`๐Ÿค– ag: ${refs.agentsMd}`); } return parts.length > 0 ? parts.join(', ') : 'None'; } diff --git a/src/webview/logviewer/main.ts b/src/webview/logviewer/main.ts index ff5aaf3..f082897 100644 --- a/src/webview/logviewer/main.ts +++ b/src/webview/logviewer/main.ts @@ -2,11 +2,16 @@ type ContextReferenceUsage = { file: number; selection: number; + implicitSelection: number; symbol: number; codebase: number; workspace: number; terminal: number; vscode: number; + byKind: { [kind: string]: number }; + copilotInstructions: number; + agentsMd: number; + byPath: { [path: string]: number }; }; type ChatTurn = { @@ -97,22 +102,60 @@ function formatFileSize(bytes: number): string { } function getTotalContextRefs(refs: ContextReferenceUsage): number { - return refs.file + refs.selection + refs.symbol + refs.codebase + - refs.workspace + refs.terminal + refs.vscode; + return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + + refs.workspace + refs.terminal + refs.vscode + refs.copilotInstructions + refs.agentsMd; } function getContextRefsSummary(refs: ContextReferenceUsage): string { const parts: string[] = []; if (refs.file > 0) { parts.push(`#file: ${refs.file}`); } if (refs.selection > 0) { parts.push(`#selection: ${refs.selection}`); } + if (refs.implicitSelection > 0) { parts.push(`implicit: ${refs.implicitSelection}`); } if (refs.symbol > 0) { parts.push(`#symbol: ${refs.symbol}`); } if (refs.codebase > 0) { parts.push(`#codebase: ${refs.codebase}`); } if (refs.workspace > 0) { parts.push(`@workspace: ${refs.workspace}`); } if (refs.terminal > 0) { parts.push(`@terminal: ${refs.terminal}`); } if (refs.vscode > 0) { parts.push(`@vscode: ${refs.vscode}`); } + if (refs.copilotInstructions > 0) { parts.push(`๐Ÿ“‹ instructions: ${refs.copilotInstructions}`); } + if (refs.agentsMd > 0) { parts.push(`๐Ÿค– agents: ${refs.agentsMd}`); } return parts.length > 0 ? parts.join(', ') : 'None'; } +function getContextRefBadges(refs: ContextReferenceUsage): string { + const badges: string[] = []; + if (refs.selection > 0) { badges.push(`#selection: ${refs.selection}`); } + if (refs.file > 0) { badges.push(`#file: ${refs.file}`); } + if (refs.symbol > 0) { badges.push(`#symbol: ${refs.symbol}`); } + if (refs.codebase > 0) { badges.push(`#codebase: ${refs.codebase}`); } + if (refs.workspace > 0) { badges.push(`@workspace: ${refs.workspace}`); } + if (refs.terminal > 0) { badges.push(`@terminal: ${refs.terminal}`); } + if (refs.vscode > 0) { badges.push(`@vscode: ${refs.vscode}`); } + if (refs.implicitSelection > 0) { badges.push(`implicit: ${refs.implicitSelection}`); } + return badges.join(''); +} + +function renderContextReferencesDetailed(refs: ContextReferenceUsage): string { + const sections: string[] = []; + + // Show instruction file references + if (refs.copilotInstructions > 0 || refs.agentsMd > 0) { + const instrRefs: string[] = []; + if (refs.copilotInstructions > 0) { instrRefs.push(`๐Ÿ“‹ copilot-instructions: ${refs.copilotInstructions}`); } + if (refs.agentsMd > 0) { instrRefs.push(`๐Ÿค– agents.md: ${refs.agentsMd}`); } + sections.push(`
Instructions: ${instrRefs.join(', ')}
`); + } + + // Show file paths if any + if (refs.byPath && Object.keys(refs.byPath).length > 0) { + const pathList = Object.entries(refs.byPath) + .map(([path, count]) => `${getFileName(path)}: ${count}`) + .join(', '); + sections.push(`
Files: ${pathList}
`); + } + + return sections.length > 0 ? sections.join('') : '
No additional details
'; +} + function getTopEntries(map: { [key: string]: number } = {}, limit = 3): { key: string; value: number }[] { return Object.entries(map) .sort((a, b) => b[1] - a[1]) @@ -154,33 +197,76 @@ function renderTurnCard(turn: ChatTurn): string { const hasMcpTools = turn.mcpTools.length > 0; const totalRefs = getTotalContextRefs(turn.contextReferences); - const toolCallsHtml = hasToolCalls ? ` -
-
๐Ÿ”ง Tool Calls (${turn.toolCalls.length})
- - - - - - - - - ${turn.toolCalls.map((tc, idx) => ` - - - - - `).join('')} - -
Tool NameAction
- ${escapeHtml(lookupToolName(tc.toolName))} - ${tc.arguments ? `
Arguments
${escapeHtml(tc.arguments)}
` : ''} - ${tc.result ? `
Result
${escapeHtml(truncateText(tc.result, 500))}
` : ''} -
- Investigate -
-
- ` : ''; + // Build context file badges for header + const contextFileBadges: string[] = []; + if (turn.contextReferences.copilotInstructions > 0) { + contextFileBadges.push(`๐Ÿ“‹ copilot-instructions.md`); + } + if (turn.contextReferences.agentsMd > 0) { + contextFileBadges.push(`๐Ÿค– agents.md`); + } + // Add other file references + if (turn.contextReferences.byPath && Object.keys(turn.contextReferences.byPath).length > 0) { + const otherPaths = Object.entries(turn.contextReferences.byPath) + .filter(([path]) => { + const normalized = path.toLowerCase().replace(/\\/g, '/'); + return !(normalized.includes('copilot-instructions.md') || normalized.endsWith('/agents.md')); + }); + + otherPaths.forEach(([path]) => { + contextFileBadges.push(`๐Ÿ“„ ${escapeHtml(getFileName(path))}`); + }); + } + + const contextHeaderHtml = contextFileBadges.length > 0 ? contextFileBadges.join('') : ''; + + // Build tool call summary + let toolCallsHtml = ''; + if (hasToolCalls) { + const toolCounts: { [key: string]: number } = {}; + turn.toolCalls.forEach(tc => { + const toolName = lookupToolName(tc.toolName); + toolCounts[toolName] = (toolCounts[toolName] || 0) + 1; + }); + + const toolSummary = Object.entries(toolCounts) + .map(([name, count]) => `${escapeHtml(name)}: ${count}`) + .join(''); + + toolCallsHtml = ` +
+
+ + โ–ถ + ๐Ÿ”ง TOOL CALLS (${turn.toolCalls.length}) + ${toolSummary} + + + + + + + + + + ${turn.toolCalls.map((tc, idx) => ` + + + + + `).join('')} + +
Tool NameAction
+ ${escapeHtml(lookupToolName(tc.toolName))} + ${tc.arguments ? `
Arguments
${escapeHtml(tc.arguments)}
` : ''} + ${tc.result ? `
Result
${escapeHtml(truncateText(tc.result, 500))}
` : ''} +
+ Investigate +
+
+
+ `; + } const mcpToolsHtml = hasMcpTools ? `
@@ -193,10 +279,21 @@ function renderTurnCard(turn: ChatTurn): string {
` : ''; - const contextRefsHtml = totalRefs > 0 ? ` -
- ๐Ÿ”— Context: - ${escapeHtml(getContextRefsSummary(turn.contextReferences))} + // Build context references detail section + const hasContextRefs = totalRefs > 0; + const contextRefBadges = getContextRefBadges(turn.contextReferences); + const contextRefsHtml = hasContextRefs ? ` +
+
+ + โ–ถ + ๐Ÿ”— CONTEXT REFERENCES (${totalRefs}) + ${contextRefBadges} + +
+ ${renderContextReferencesDetailed(turn.contextReferences)} +
+
` : ''; @@ -208,20 +305,21 @@ function renderTurnCard(turn: ChatTurn): string { ${getModeIcon(turn.mode)} ${turn.mode} ${turn.model ? `๐ŸŽฏ ${escapeHtml(turn.model)}` : ''} ๐Ÿ“Š ${totalTokens.toLocaleString()} tokens (โ†‘${turn.inputTokensEstimate} โ†“${turn.outputTokensEstimate}) + ${contextHeaderHtml}
${formatDate(turn.timestamp)}
+ ${toolCallsHtml} + ${mcpToolsHtml} + ${contextRefsHtml} +
๐Ÿ‘ค User
${escapeHtml(turn.userMessage) || 'No message'}
- ${contextRefsHtml} - ${toolCallsHtml} - ${mcpToolsHtml} -
๐Ÿค– Assistant
${escapeHtml(turn.assistantResponse) || 'No response'}
@@ -250,7 +348,18 @@ function renderLayout(data: SessionLogData): void { const formatTopList = (entries: { key: string; value: number }[], mapper?: (k: string) => string) => { if (!entries.length) { return 'None'; } - return entries.map(e => `${escapeHtml(mapper ? mapper(e.key) : e.key)}: ${e.value}`).join(', '); + return entries.map(e => `
${escapeHtml(mapper ? mapper(e.key) : e.key)}: ${e.value}
`).join(''); + }; + + const formatTopListWithOther = (entries: { key: string; value: number }[], total: number, mapper?: (k: string) => string) => { + if (!entries.length) { return 'None'; } + const lines = entries.map(e => `
${escapeHtml(mapper ? mapper(e.key) : e.key)}: ${e.value}
`); + const topSum = entries.reduce((sum, e) => sum + e.value, 0); + const other = total - topSum; + if (other > 0) { + lines.push(`
Other: ${other}
`); + } + return lines.join(''); }; // Mode usage summary @@ -433,30 +542,18 @@ function renderLayout(data: SessionLogData): void { display: flex; justify-content: space-between; align-items: center; - flex-wrap: wrap; gap: 10px; border-bottom: 1px solid #3a3a44; + min-height: 48px; } .turn-meta { display: flex; align-items: center; gap: 8px; - flex-wrap: wrap; - } - .turn-number { - font-weight: 700; - color: #fff; - font-size: 14px; - } - .turn-mode { - padding: 2px 8px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; - color: #fff; - } - .turn-10px; - flex-wrap: wrap; + flex-wrap: nowrap; + flex: 1; + min-width: 0; + overflow: hidden; } .turn-number { font-weight: 700; @@ -465,6 +562,7 @@ function renderLayout(data: SessionLogData): void { background: #3a3a44; padding: 4px 10px; border-radius: 6px; + flex-shrink: 0; } .turn-mode { padding: 4px 12px; @@ -473,6 +571,8 @@ function renderLayout(data: SessionLogData): void { font-weight: 700; color: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.2); + flex-shrink: 0; + white-space: nowrap; } .turn-model { font-size: 12px; @@ -482,17 +582,39 @@ function renderLayout(data: SessionLogData): void { border-radius: 6px; font-weight: 600; border: 1px solid #3a3a44; + flex-shrink: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; } .turn-tokens { font-size: 12px; color: #94a3b8; font-weight: 600; + flex-shrink: 0; + white-space: nowrap; } - .turn-time { + .context-badge { font-size: 12px; - color: #71717a; - font-weight: 500; - }4px; + color: #e0e7ff; + background: linear-gradient(135deg, #4c1d95 0%, #5b21b6 100%); + padding: 4px 10px; + border-radius: 6px; + font-weight: 600; + border: 1px solid #6d28d9; + flex-shrink: 0; + white-space: nowrap; + margin-left: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + } + + /* Messages */ + .turn-content { + padding: 16px; + } + .message { + margin-bottom: 14px; } .message:last-child { margin-bottom: 0; @@ -522,31 +644,85 @@ function renderLayout(data: SessionLogData): void { background: linear-gradient(135deg, #1e293b 0%, #22222a 100%); } .assistant-message .message-text { - border-left: 4px4px; - padding: 12px 14px; - background: linear-gradient(135deg, #2a2a35 0%, #25252f 100%); - border: 1px solid #3a3a44; - border-radius: 8px; - font-size: 13px; + border-left: 4px solid #7c3aed; + background: linear-gradient(135deg, #1e1e2a 0%, #22222a 100%); } - .context-label { + + /* Shared collapse arrow for details/summary panels */ + .collapse-arrow { + display: inline-block; + width: 16px; color: #94a3b8; - margin-right: 8px; - font-weight: 600; + font-size: 10px; + transition: transform 0.2s; + flex-shrink: 0; } - .context-value { - color: #cbd5e1; + details[open] > summary .collapse-arrow { + transform: rotate(90deg); } - + /* Tool calls */ .turn-tools { margin-bottom: 14px; - background: linear-gradient(135deg, #221e2e 0%, #252030 100%); - border: 1px solid #4a4a5a; + background: linear-gradient(135deg, #2a2a35 0%, #25252f 100%); + border: 1px solid #3a3a44; border-radius: 8px; - padding: 14px; + padding: 12px 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } + .tool-calls-details { + cursor: pointer; + margin: 0; + padding: 0; + } + .tool-calls-summary { + list-style: none; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 10px; + padding: 2px 0; + padding-inline-start: 0; + margin: 0; + } + .tool-calls-summary::-webkit-details-marker { + display: none; + } + .tool-calls-summary::marker { + display: none; + } + .tool-calls-summary:hover { + color: #fff; + } + .tools-header-inline { + font-size: 13px; + font-weight: 700; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .tool-summary-text { + font-size: 12px; + font-weight: 600; + color: #c084fc; + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + } + .tool-summary-item { + background: rgba(192, 132, 252, 0.1); + border: 1px solid rgba(192, 132, 252, 0.3); + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; + } + .tool-summary-item strong { + color: #e9d5ff; + font-weight: 700; + } .tools-header { font-size: 13px; font-weight: 700; @@ -559,6 +735,7 @@ function renderLayout(data: SessionLogData): void { width: 100%; border-collapse: collapse; font-size: 13px; + margin-top: 10px; } .tools-table thead th { text-align: left; @@ -648,7 +825,60 @@ function renderLayout(data: SessionLogData): void { } .mcp-list { display: flex; - flEmpty state */ + flex-wrap: wrap; + gap: 6px; + } + .mcp-item { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + color: #cbd5e1; + } + .mcp-server { + font-weight: 600; + color: #22c55e; + } + + /* Context References */ + .turn-context-details { + margin-bottom: 14px; + background: linear-gradient(135deg, #2a2535 0%, #252530 100%); + border: 1px solid #4a4a5a; + border-radius: 8px; + padding: 14px; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + } + .context-header { + font-size: 13px; + font-weight: 700; + color: #fff; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .context-section { + font-size: 13px; + color: #cbd5e1; + margin-bottom: 8px; + line-height: 1.6; + } + .context-section:last-child { + margin-bottom: 0; + } + .context-section strong { + color: #94a3b8; + font-weight: 600; + } + .context-path { + padding-left: 10px; + color: #9ca3af; + font-size: 12px; + margin-top: 4px; + } + + /* Empty state */ .empty-state { text-align: center; padding: 60px 20px; @@ -673,16 +903,84 @@ function renderLayout(data: SessionLogData): void { border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { - background: #4a4a54{ - font-size: 12px; - font-weight: 600; + background: #4a4a54; + } + } + + /* Context References */ + .turn-context-refs { + margin-bottom: 14px; + background: linear-gradient(135deg, #2a2535 0%, #252530 100%); + border: 1px solid #4a4a5a; + border-radius: 8px; + padding: 12px 14px; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + } + .context-refs-details { + cursor: pointer; + margin: 0; + padding: 0; + } + .context-refs-summary { + list-style: none; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 10px; + padding: 2px 0; + padding-inline-start: 0; + margin: 0; + } + .context-refs-summary::-webkit-details-marker { + display: none; + } + .context-refs-summary::marker { + display: none; + } + .context-refs-summary:hover { color: #fff; - margin-bottom: 8px; } - .mcp-list { + .context-refs-header-inline { + font-size: 13px; + font-weight: 700; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .context-ref-summary-text { + font-size: 12px; + font-weight: 600; + color: #22d3ee; + flex: 1; display: flex; flex-wrap: wrap; - gap: 6px; + gap: 8px; + align-items: center; + } + .context-ref-item { + background: rgba(34, 211, 238, 0.1); + border: 1px solid rgba(34, 211, 238, 0.3); + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; + } + .context-ref-item strong { + color: #a5f3fc; + font-weight: 700; + } + .context-ref-implicit { + background: rgba(148, 163, 184, 0.1); + border: 1px solid rgba(148, 163, 184, 0.3); + color: #94a3b8; + } + .context-ref-implicit strong { + color: #cbd5e1; + } + .context-refs-content { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid rgba(255,255,255,0.1); } .mcp-item { background: #243024; @@ -719,20 +1017,33 @@ function renderLayout(data: SessionLogData): void {
${data.interactions}
Total chat turns in this session
+
+
๐Ÿ“Š Total Tokens
+
${totalTokens.toLocaleString()}
+
Input + Output tokens across all turns
+
๐Ÿ”ง Tool Calls
${usageToolTotal}
-
Top: ${formatTopList(usageTopTools, lookupToolName)}
+
${formatTopListWithOther(usageTopTools, usageToolTotal, lookupToolName)}
๐Ÿ”Œ MCP Tools
${usageMcpTotal}
-
Top: ${formatTopList(usageTopMcpTools)}
+
${formatTopListWithOther(usageTopMcpTools, usageMcpTotal)}
๐Ÿ”— Context Refs
${usageContextTotal}
-
#file ${usageContextRefs.file || 0} ยท @vscode ${usageContextRefs.vscode || 0} ยท @workspace ${usageContextRefs.workspace || 0}
+
+ ${usageContextTotal === 0 ? 'None' : ''} + ${usageContextRefs.file > 0 ? `
#file ${usageContextRefs.file}
` : ''} + ${usageContextRefs.implicitSelection > 0 ? `
implicit ${usageContextRefs.implicitSelection}
` : ''} + ${usageContextRefs.copilotInstructions > 0 ? `
๐Ÿ“‹ instructions ${usageContextRefs.copilotInstructions}
` : ''} + ${usageContextRefs.agentsMd > 0 ? `
๐Ÿค– agents ${usageContextRefs.agentsMd}
` : ''} + ${usageContextRefs.workspace > 0 ? `
@workspace ${usageContextRefs.workspace}
` : ''} + ${usageContextRefs.vscode > 0 ? `
@vscode ${usageContextRefs.vscode}
` : ''} +
๐Ÿ“ File Name
diff --git a/src/webview/usage/main.ts b/src/webview/usage/main.ts index 37511a9..e1c2d8d 100644 --- a/src/webview/usage/main.ts +++ b/src/webview/usage/main.ts @@ -12,6 +12,10 @@ type ContextReferenceUsage = { workspace: number; terminal: number; vscode: number; + byKind: { [kind: string]: number }; + copilotInstructions: number; + agentsMd: number; + byPath: { [path: string]: number }; }; type ToolCallUsage = { total: number; byTool: { [key: string]: number } }; type McpToolUsage = { total: number; byServer: { [key: string]: number }; byTool: { [key: string]: number } }; @@ -68,8 +72,13 @@ function escapeHtml(text: string): string { } function getTotalContextRefs(refs: ContextReferenceUsage): number { - return refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + + const basicRefs = refs.file + refs.selection + refs.implicitSelection + refs.symbol + refs.codebase + refs.workspace + refs.terminal + refs.vscode; + + // Add contentReferences counts + const contentRefs = refs.copilotInstructions + refs.agentsMd; + + return basicRefs + contentRefs; } import toolNames from '../../toolNames.json'; @@ -328,8 +337,34 @@ function renderLayout(stats: UsageAnalysisStats): void {
๐Ÿ“ @workspace
${stats.month.contextReferences.workspace}
Today: ${stats.today.contextReferences.workspace}
๐Ÿ’ป @terminal
${stats.month.contextReferences.terminal}
Today: ${stats.today.contextReferences.terminal}
๐Ÿ”ง @vscode
${stats.month.contextReferences.vscode}
Today: ${stats.today.contextReferences.vscode}
+
๐Ÿ“‹ Copilot Instructions
${stats.month.contextReferences.copilotInstructions}
Today: ${stats.today.contextReferences.copilotInstructions}
+
๐Ÿค– Agents.md
${stats.month.contextReferences.agentsMd}
Today: ${stats.today.contextReferences.agentsMd}
๐Ÿ“Š Total References
${monthTotalRefs}
Today: ${todayTotalRefs}
+ ${Object.keys(stats.month.contextReferences.byKind).length > 0 ? ` +
+
๐Ÿ“Ž Attached Files by Type (This Month)
+
+ ${Object.entries(stats.month.contextReferences.byKind) + .sort(([, a], [, b]) => (b as number) - (a as number)) + .slice(0, 5) + .map(([kind, count]) => `
${escapeHtml(kind)}: ${count}
`) + .join('')} +
+
+ ` : ''} + ${Object.keys(stats.month.contextReferences.byPath).length > 0 ? ` +
+
๐Ÿ“ Most Referenced Files (This Month)
+
+ ${Object.entries(stats.month.contextReferences.byPath) + .sort(([, a], [, b]) => (b as number) - (a as number)) + .slice(0, 10) + .map(([path, count]) => `
${count}ร— ${escapeHtml(path)}
`) + .join('')} +
+
+ ` : ''}