Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## 0.10.17 (Preview)

### Added
- **Language Model Tools** — 5 LM-tool wrappers (`binlog_lm_overview`, `binlog_lm_errors`, `binlog_lm_search`, `binlog_lm_perf`, `binlog_lm_compare`) registered via `vscode.lm.registerTool` so any agent (@workspace, agent mode, custom chat modes) can analyze loaded binlogs — not just `@binlog`
- **"Ask @binlog" CodeAction** — click a build error/warning squiggle in the editor → Quick Fix → "Ask @binlog about this error" or "Fix with @binlog"
- **Auto-fix diagnostic** — right-click any error/warning in the Binlog Explorer → "Auto-fix with Copilot" opens agent mode to edit the source file directly
- **Diagnostic context menu** — right-click error/warning tree items for "Ask @binlog about this issue" and "Auto-fix with Copilot"
- **Fix All: before/after comparison** — "Fix All Issues" now writes to a new `{name}_fixed_{N}.binlog` (preserving the original) and auto-loads both for `/compare`
- **`/timeline` command playbook** — `/timeline` now has its own per-command instruction file
- **esbuild bundling** — extension ships as a single `dist/extension.js` (~476 KB) instead of raw TypeScript output
- **CI matrix** — GitHub Actions workflow runs on both Ubuntu and Windows

### Changed
- **Prompt refactor** — SYSTEM_PROMPT and COMMAND_PROMPTS moved to lazy-loaded markdown playbooks (`resources/playbooks/`). Total prompt budget ~250 tokens baseline vs ~1500 before
- **Auto-greeting** — loading a binlog now fires `@binlog /summary` automatically instead of asking "What would you like to analyze?"
- **Multi-binlog support** — all MCP tool calls (tree view, document provider, timeline, extension commands) now auto-inject `binlog_file` for the primary binlog when multiple are loaded

### Fixed
- **MCP startup race** — auto-greeting now waits for both MCP config AND tree client initialization before firing `/summary`
- **LM-tool readiness** — wrappers wait up to 10s for MCP client to become ready instead of immediately returning "No binlog loaded"
- **Cross-machine file navigation** — diagnostics and tree view items from binlogs built on other machines (CI, coworkers) no longer show "file not found" — paths are resolved against workspace folders
- **Multi-root workspace path resolution** — `resolveFilePath` now checks each workspace folder for file existence instead of blindly using the first
- **MCP timer leak** — `setTimeout` in `sendRequest` is now cleared on resolve/reject
- **Spawn error handler** — missing `error` event listener on MCP subprocess no longer crashes the extension host
- **processResponse error retry** — recursive tool-call retry now checks all 5 error patterns (was missing `role 'tool'`, `tool_call_id`, `400`)
- **CodeAction cap** — max 5 diagnostics per `provideCodeActions` call to prevent unusable Quick Fix menus
- **Cross-platform tests** — `workspaceMatchesBinlog` and `getSourceLabel` no longer depend on `path.sep` (fixes CI on Ubuntu)
- **Stale tool names in prompts** — `analyzeInChat` prompts no longer reference non-existent tools (`binlog_search_targets`, etc.)

## 0.10.16 (Preview)

### Fixed
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ The [BinlogInsights.Mcp](https://www.nuget.org/packages/BinlogInsights.Mcp) serv

| Feature | Description |
|---------|-------------|
| **@binlog Chat** | Ask Copilot about errors, performance, targets, imports, NuGet issues — with slash commands like `/errors`, `/perf`, `/timeline`, `/compare` |
| **@binlog Chat** | Ask Copilot about errors, performance, targets, imports, NuGet issues — with slash commands like `/errors`, `/perf`, `/timeline`, `/compare`, `/summary`, `/incremental`, `/buildcheck`, `/search`, `/targets`, `/properties`, `/items`, `/propertyhistory` |
| **Build & Collect** | Build a project and capture a `.binlog` in one step |
| **Binlog Explorer** | Sidebar tree with project → target → task hierarchy, errors, warnings, performance |
| **Build Timeline** | Visual bar charts of target/task durations with click-to-analyze in Copilot |
| **Fix All Issues** | Copilot fixes all build errors/warnings, rebuilds, and loads before/after for comparison |
| **Auto-fix Diagnostic** | Right-click any error/warning in the tree → "Auto-fix with Copilot" to fix it directly |
| **Optimize Build** | Pick optimizations, Copilot applies changes, verify with A/B comparison |
| **Build Analysis Mode** | Chat mode pre-configured with BinlogInsights MCP tools — works with any agent |
| **Language Model Tools** | `binlog_lm_overview`, `binlog_lm_errors`, `binlog_lm_search`, `binlog_lm_perf`, `binlog_lm_compare` — available to @workspace, agent mode, and custom chat modes |
| **CI/CD Integration** | Download binlogs from Azure DevOps Pipelines and GitHub Actions — filter by branch or PR |
| **Problems Panel** | Build diagnostics as native VS Code errors/warnings with per-project CodeLens |
| **Problems Panel** | Build diagnostics as native VS Code errors/warnings with per-project CodeLens and "Ask @binlog" CodeActions |
| **Search** | Search across all build events — targets, tasks, messages, properties |

## Configuration
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "binlog-analyzer",
"displayName": "MSBuild Binlog Analyzer",
"description": "Analyze MSBuild binary logs with Copilot Chat and MCP tools",
"version": "0.10.30",
"version": "0.10.17",
"preview": true,
"publisher": "dotutils",
"license": "MIT",
Expand Down
46 changes: 30 additions & 16 deletions src/binlogDocumentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
this.cache.clear();
}

/**
* Wrapper around mcpClient.callTool that auto-injects binlog_file
* for the primary (first) loaded binlog. Without this, multi-binlog
* mode causes "requires explicit binlog_file" errors in every
* document render call.
*/
private async call(tool: string, args: Record<string, unknown> = {}): Promise<{ text: string }> {
if (!this.mcpClient) { throw new Error('MCP server not connected'); }
if (!args.binlog_file && this.mcpClient.loadedBinlogs.length > 1) {
args.binlog_file = this.mcpClient.loadedBinlogs[0];
}
return this.mcpClient.callTool(tool, args);
}

invalidate(uri: vscode.Uri) {
this.cache.delete(uri.toString());
this._onDidChange.fire(uri);
Expand Down Expand Up @@ -82,7 +96,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide

// Build overview
try {
const overviewResult = await this.mcpClient!.callTool('binlog_overview');
const overviewResult = await this.call('binlog_overview');
const ov = JSON.parse(overviewResult.text);
const status = ov.succeeded ? '✅ BUILD SUCCEEDED' : '❌ BUILD FAILED';
const dur = ov.duration || '';
Expand Down Expand Up @@ -110,7 +124,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide

// Projects
try {
const projResult = await this.mcpClient!.callTool('binlog_projects');
const projResult = await this.call('binlog_projects');
const projData = JSON.parse(projResult.text);

// Handle both formats: array (BinlogInsights) and object (baronfel)
Expand Down Expand Up @@ -141,8 +155,8 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
let diagsByProject: Map<string, { errors: number; warnings: number }> | undefined;
try {
const [errResult, warnResult] = await Promise.allSettled([
this.mcpClient!.callTool('binlog_errors'),
this.mcpClient!.callTool('binlog_warnings'),
this.call('binlog_errors'),
this.call('binlog_warnings'),
]);
diagsByProject = new Map();
for (const result of [errResult, warnResult]) {
Expand Down Expand Up @@ -188,8 +202,8 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
// Diagnostics
try {
const [errResult, warnResult] = await Promise.allSettled([
this.mcpClient!.callTool('binlog_errors'),
this.mcpClient!.callTool('binlog_warnings'),
this.call('binlog_errors'),
this.call('binlog_warnings'),
]);

const parseItems = (r: PromiseSettledResult<any>) => {
Expand Down Expand Up @@ -237,7 +251,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide

// Performance
try {
const targetsResult = await this.mcpClient!.callTool('binlog_expensive_targets', { top_number: 10 });
const targetsResult = await this.call('binlog_expensive_targets', { top_number: 10 });
const targetsData = JSON.parse(targetsResult.text);
lines.push('🔥 SLOWEST TARGETS');
lines.push('─────────────────────────────────────────────────────');
Expand All @@ -250,7 +264,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
lines.push('');

try {
const tasksResult = await this.mcpClient!.callTool('binlog_expensive_tasks', { top_number: 10 });
const tasksResult = await this.call('binlog_expensive_tasks', { top_number: 10 });
const tasksData = JSON.parse(tasksResult.text);
lines.push('🔧 SLOWEST TASKS');
lines.push('─────────────────────────────────────────────────────');
Expand All @@ -263,7 +277,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide

// Analyzers
try {
const analyzersResult = await this.mcpClient!.callTool('binlog_expensive_analyzers', { limit: 10 });
const analyzersResult = await this.call('binlog_expensive_analyzers', { limit: 10 });
const analyzersData = JSON.parse(analyzersResult.text);
const entries = this.parsePerfEntries(analyzersData);
if (entries.length > 0) {
Expand Down Expand Up @@ -300,7 +314,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide

// Get per-project target times
try {
const targetTimesResult = await this.mcpClient!.callTool('binlog_project_target_times', {
const targetTimesResult = await this.call('binlog_project_target_times', {
project: projectName,
});
const targetData = JSON.parse(targetTimesResult.text);
Expand Down Expand Up @@ -330,7 +344,7 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
} catch {
// Try fallback: binlog_expensive_projects for at least a total time
try {
const buildTimeResult = await this.mcpClient!.callTool('binlog_expensive_projects');
const buildTimeResult = await this.call('binlog_expensive_projects');
const data = JSON.parse(buildTimeResult.text);
const projects = Array.isArray(data) ? data : [];
const match = projects.find((p: any) =>
Expand All @@ -348,8 +362,8 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
// Get diagnostics and filter for this project
try {
const [errResult, warnResult] = await Promise.allSettled([
this.mcpClient!.callTool('binlog_errors'),
this.mcpClient!.callTool('binlog_warnings'),
this.call('binlog_errors'),
this.call('binlog_warnings'),
]);

const parseItems = (r: PromiseSettledResult<any>) => {
Expand Down Expand Up @@ -414,18 +428,18 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide
}

private async renderProjects(): Promise<string> {
const result = await this.mcpClient!.callTool('binlog_projects');
const result = await this.call('binlog_projects');
return this.formatSection('PROJECTS', result.text);
}

private async renderDiagnostics(type: 'errors' | 'warnings'): Promise<string> {
const tool = type === 'errors' ? 'binlog_errors' : 'binlog_warnings';
const result = await this.mcpClient!.callTool(tool);
const result = await this.call(tool);
return this.formatSection(type.toUpperCase(), result.text);
}

private async renderExpensive(tool: string, title: string): Promise<string> {
const result = await this.mcpClient!.callTool(tool, { top_number: 20 });
const result = await this.call(tool, { top_number: 20 });
return this.formatSection(title.toUpperCase(), result.text);
}

Expand Down
40 changes: 29 additions & 11 deletions src/binlogTreeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre

// Try the dedicated MCP tool first
try {
const result = await this.mcpClient.callTool('binlog_expensive_analyzers', { limit: 20 });
const result = await this.mcpCall('binlog_expensive_analyzers', { limit: 20 });
const items = this.parsePerfItems(result.text, 'microscope');
if (items.length > 0) {
this.analyzersCache = items;
Expand All @@ -842,7 +842,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
// "363 ms Microsoft.CodeAnalysis.CSharp.NetAnalyzers ... = 341 ms"
try {
// First check if the binlog has analyzer data at all
const checkResult = await this.mcpClient.callTool('binlog_search', {
const checkResult = await this.mcpCall('binlog_search', {
query: 'Total analyzer execution',
limit: 5,
});
Expand Down Expand Up @@ -877,7 +877,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
];
for (const term of searchTerms) {
try {
const result = await this.mcpClient.callTool('binlog_search', {
const result = await this.mcpCall('binlog_search', {
query: term,
limit: 200,
});
Expand Down Expand Up @@ -1019,7 +1019,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
}
try {
const projectName = this.extractFileName(projectFile).replace(/\.[^.]+$/, '');
const result = await this.mcpClient.callTool('binlog_project_targets', {
const result = await this.mcpCall('binlog_project_targets', {
project: projectName,
});
const data = this.tryParseJson(result.text);
Expand Down Expand Up @@ -1089,7 +1089,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
target_name: targetName,
project: projectName,
};
const result = await this.mcpClient.callTool('binlog_tasks_in_target', args);
const result = await this.mcpCall('binlog_tasks_in_target', args);
const data = this.tryParseJson(result.text);
const items: BinlogTreeItem[] = [];
const entries = Array.isArray(data) ? data : (data && typeof data === 'object' ? Object.entries(data).map(([name, info]) => ({ name, ...(info as object) })) : []);
Expand Down Expand Up @@ -1152,7 +1152,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
project: projectName,
target_name: element.targetName || '',
};
const result = await this.mcpClient.callTool('binlog_task_details', args);
const result = await this.mcpCall('binlog_task_details', args);
const items: BinlogTreeItem[] = [];

// Try to parse as JSON first
Expand Down Expand Up @@ -1224,7 +1224,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
if (!this.mcpClient) {
throw new Error('MCP server not connected');
}
return this.mcpClient.callTool('binlog_search', {
return this.mcpCall('binlog_search', {
query,
limit: maxResults,
});
Expand Down Expand Up @@ -1405,7 +1405,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
return [this.makeInfoItem('MCP server not connected', 'info')];
}
try {
const result = await this.mcpClient.callTool('binlog_evaluations');
const result = await this.mcpCall('binlog_evaluations');
const data = this.tryParseJson(result.text);
const entries = Array.isArray(data) ? data : [];

Expand Down Expand Up @@ -1456,7 +1456,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
return [this.makeInfoItem('No evaluation ID', 'info')];
}
try {
const result = await this.mcpClient.callTool('binlog_evaluation_properties', {
const result = await this.mcpCall('binlog_evaluation_properties', {
evaluation_id: element.evaluationId,
});
const data = this.tryParseJson(result.text);
Expand Down Expand Up @@ -1502,7 +1502,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
return [this.makeInfoItem('No evaluation ID', 'info')];
}
try {
const result = await this.mcpClient.callTool('binlog_evaluation_global_properties', {
const result = await this.mcpCall('binlog_evaluation_global_properties', {
evaluation_id: element.evaluationId,
});
const data = this.tryParseJson(result.text);
Expand Down Expand Up @@ -1559,9 +1559,15 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
return [this.makeInfoItem('Loading...', 'loading')];
}

// Auto-inject binlog_file for multi-binlog to avoid
// "requires explicit binlog_file" errors.
if (!args.binlog_file && this.binlogPaths.length > 1) {
args.binlog_file = this.binlogPaths[0];
}

this.loadingSet.add(parentKind);
try {
const result = await this.mcpClient.callTool(toolName, args);
const result = await this.mcpCall(toolName, args);
this.loadingSet.delete(parentKind);
const data = parser(result.text);
if (data.length === 0) {
Expand All @@ -1575,6 +1581,18 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider<BinlogTre
}
}

/**
* Call MCP tool with auto-injected binlog_file for multi-binlog mode.
* Used by direct callTool sites outside of the callMcpTool helper.
*/
private mcpCall(tool: string, args: Record<string, unknown> = {}): Promise<{ text: string }> {
if (!this.mcpClient) { throw new Error('MCP server not connected'); }
if (!args.binlog_file && this.binlogPaths.length > 1) {
args.binlog_file = this.binlogPaths[0];
}
return this.mcpClient.callTool(tool, args);
}

// --- Helpers ---

private dataToItem(data: TreeNodeData): BinlogTreeItem {
Expand Down
Loading
Loading