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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Install: Added automated install and upgrade support for Linux using official setup scripts
- Notification: Detect outdated Dev Proxy config files in workspace and show warning when schema versions don't match installed version
- Command: `dev-proxy-toolkit.upgrade-configs` - Upgrade config files with Copilot Chat using Dev Proxy MCP tools
- Logging: Added leveled logging to Output panel (`Dev Proxy Toolkit`) across the entire extension covering commands, diagnostics, services, and utilities to help investigate issues
- Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only
- Workspace: Added automatic prompt to recommend extension in `.vscode/extensions.json` when Dev Proxy config files are detected
- Command: Added `Add to Workspace Recommendations` to manually add extension to workspace recommendations
Expand All @@ -23,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed:

- Logging: Fixed log flooding from repeated running state checks by only logging on state changes
- Logging: Fixed log feedback loop caused by Output Channel document events triggering diagnostics

- Diagnostics: Language model diagnostic now correctly targets plugins that can use a local language model (OpenAIMockResponsePlugin, OpenApiSpecGeneratorPlugin, TypeSpecGeneratorPlugin) and shows as an informational hint instead of a warning

## [1.12.0] - 2026-01-29
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ This extension includes an MCP server for AI-assisted development. See [Dev Prox

## Troubleshooting

**View debug logs**
- Open the Output panel (`View > Output`) and select **Dev Proxy Toolkit** from the dropdown to see extension logs
- Use the log level setting to control verbosity (trace, debug, info, warning, error)

**Dev Proxy not detected?**
- Ensure Dev Proxy is installed and available in your PATH
- Check the `dev-proxy-toolkit.version` setting if you have both stable and beta installed
Expand Down
6 changes: 4 additions & 2 deletions src/code-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import parse from 'json-to-ast';
import { getASTNode, getRangeFromASTNode } from './utils';
import { pluginSnippets } from './data';
import snippetsJson from './snippets/json-snippets.json';
import * as logger from './logger';

/**
* Extract the diagnostic code value from the object format.
Expand Down Expand Up @@ -60,6 +61,7 @@ export const registerCodeActions = (context: vscode.ExtensionContext) => {
context.globalState.get<DevProxyInstall>('devProxyInstall');

if (!devProxyInstall) {
logger.debug('Dev Proxy install not found, code actions disabled');
return;
}

Expand Down Expand Up @@ -124,8 +126,8 @@ export function extractSchemaFilename(schemaUrl: string): string {
if (match) {
return match[1];
}
} catch {
// Fall through to default
} catch (error) {
logger.warn('Failed to extract schema filename, using default', { schemaUrl, error });
}

return defaultSchema;
Expand Down
73 changes: 39 additions & 34 deletions src/code-lens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { isConfigFile, getASTNode, getRangeFromASTNode } from './utils';
import parse from 'json-to-ast';
import { pluginSnippets } from './data';
import * as logger from './logger';

export const registerCodeLens = (context: vscode.ExtensionContext) => {
context.subscriptions.push(
Expand Down Expand Up @@ -31,44 +32,48 @@ export const pluginLensProvider: vscode.CodeLensProvider = {
export const createCodeLensForPluginNodes = (document: vscode.TextDocument) => {
const codeLens: vscode.CodeLens[] = [];
if (isConfigFile(document)) {
const documentNode = parse(document.getText()) as parse.ObjectNode;
const pluginsNode = getASTNode(
documentNode.children,
'Identifier',
'plugins'
);
try {
const documentNode = parse(document.getText()) as parse.ObjectNode;
const pluginsNode = getASTNode(
documentNode.children,
'Identifier',
'plugins'
);

if (
pluginsNode &&
(pluginsNode.value as parse.ArrayNode).children.length !== 0
) {
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
.children as parse.ObjectNode[];
if (
pluginsNode &&
(pluginsNode.value as parse.ArrayNode).children.length !== 0
) {
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
.children as parse.ObjectNode[];

pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
const pluginNameNode = getASTNode(
pluginNode.children,
'Identifier',
'name'
);
if (!pluginNameNode) {
return;
}
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
.value as string;
pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
const pluginNameNode = getASTNode(
pluginNode.children,
'Identifier',
'name'
);
if (!pluginNameNode) {
return;
}
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
.value as string;

const isValidName = pluginSnippets[pluginName];
const isValidName = pluginSnippets[pluginName];

if (isValidName) {
codeLens.push(
new vscode.CodeLens(getRangeFromASTNode(pluginNameNode), {
title: `📄 ${pluginName}`,
command: 'dev-proxy-toolkit.openPluginDoc',
arguments: [pluginName],
})
);
}
});
if (isValidName) {
codeLens.push(
new vscode.CodeLens(getRangeFromASTNode(pluginNameNode), {
title: `📄 ${pluginName}`,
command: 'dev-proxy-toolkit.openPluginDoc',
arguments: [pluginName],
})
);
}
});
}
} catch (error) {
logger.warn('Failed to parse config file for code lens generation', { file: document.fileName, error });
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Commands } from '../constants';
import { executeCommand } from '../utils/shell';
import { getDevProxyExe } from '../detect';
import { VersionPreference } from '../enums';
import * as logger from '../logger';

/**
* Configuration file commands: open, create new.
Expand All @@ -25,6 +26,7 @@ export function registerConfigCommands(
}

async function openConfig(devProxyExe: string): Promise<void> {
logger.debug('Opening Dev Proxy config', { devProxyExe });
await executeCommand(`${devProxyExe} config open`);
}

Expand All @@ -36,15 +38,18 @@ async function createNewConfig(devProxyExe: string): Promise<void> {

const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceFolder) {
logger.warn('Cannot create config: no workspace folder open');
vscode.window.showErrorMessage('No workspace folder open');
return;
}
logger.info('Creating new config file', { fileName, workspaceFolder });

const devProxyFolder = vscode.Uri.file(`${workspaceFolder}/.devproxy`);
const configUri = vscode.Uri.file(`${workspaceFolder}/.devproxy/${fileName}`);

// Check if file already exists
if (await fileExists(configUri)) {
logger.warn('Config file already exists', { path: configUri.fsPath });
vscode.window.showErrorMessage('A file with that name already exists');
return;
}
Expand All @@ -67,9 +72,11 @@ async function createNewConfig(devProxyExe: string): Promise<void> {
);

// Open the newly created file
logger.info('Config file created', { path: configUri.fsPath });
const document = await vscode.workspace.openTextDocument(configUri);
await vscode.window.showTextDocument(document);
} catch (error) {
logger.error('Failed to create new config file', error);
vscode.window.showErrorMessage('Failed to create new config file');
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/commands/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Commands } from '../constants';
import { TerminalService } from '../services/terminal';
import { getDevProxyExe } from '../detect';
import { VersionPreference } from '../enums';
import * as logger from '../logger';

/**
* URL discovery command.
Expand Down Expand Up @@ -40,10 +41,12 @@ async function discoverUrlsToWatch(

// User cancelled
if (processNames === undefined) {
logger.debug('URL discovery cancelled by user');
return;
}

const command = buildDiscoverCommand(devProxyExe, processNames);
logger.info('Starting URL discovery', { processNames: processNames || '(all processes)', command });
terminalService.sendCommand(terminal, command);
}

Expand Down
6 changes: 6 additions & 0 deletions src/commands/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Commands } from '../constants';
import { pluginDocs } from '../data';
import parse from 'json-to-ast';
import { getASTNode, getRangeFromASTNode } from '../utils/ast';
import * as logger from '../logger';

/**
* Documentation and language model configuration commands.
Expand All @@ -21,8 +22,11 @@ export function registerDocCommands(context: vscode.ExtensionContext): void {
function openPluginDocumentation(pluginName: string): void {
const doc = pluginDocs[pluginName];
if (doc) {
logger.debug('Opening plugin docs', { pluginName, url: doc.url });
const target = vscode.Uri.parse(doc.url);
vscode.env.openExternal(target);
} else {
logger.warn('Plugin docs not found', { pluginName });
}
}

Expand All @@ -42,11 +46,13 @@ async function addLanguageModelConfig(uri: vscode.Uri): Promise<void> {
}
} catch (error) {
// Fallback to simple text-based insertion
logger.debug('Failed to parse document with json-to-ast, using text-based fallback', error);
addLanguageModelFallback(document, edit, uri);
}

await vscode.workspace.applyEdit(edit);
await document.save();
logger.info('Language model configuration added');

vscode.window.showInformationMessage('Language model configuration added');
}
Expand Down
14 changes: 14 additions & 0 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
openUpgradeDocumentation,
} from '../utils/shell';
import { PackageManager, VersionPreference } from '../enums';
import * as logger from '../logger';

/**
* Installation and upgrade commands.
Expand All @@ -34,6 +35,7 @@ async function installDevProxy(
): Promise<void> {
const message = vscode.window.setStatusBarMessage('Installing Dev Proxy...');
const versionPreference = configuration.get('version') as VersionPreference;
logger.info('Installing Dev Proxy', { platform, versionPreference });

try {
if (platform === 'win32') {
Expand All @@ -55,17 +57,21 @@ async function installOnWindows(versionPreference: VersionPreference): Promise<v
try {
await executeCommand('winget --version');
} catch {
logger.warn('Winget not found on PATH');
vscode.window.showErrorMessage('Winget is not installed. Please install winget and try again.');
return;
}

try {
logger.info('Installing Dev Proxy via winget', { packageId });
await executeCommand(`winget install ${packageId} --silent`);
logger.info('Dev Proxy installed successfully via winget');
const result = await vscode.window.showInformationMessage('Dev Proxy installed.', 'Reload');
if (result === 'Reload') {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
}
} catch (error) {
logger.error('Failed to install Dev Proxy via winget', error);
vscode.window.showErrorMessage(`Failed to install Dev Proxy.\n${error}`);
}
}
Expand All @@ -77,18 +83,22 @@ async function installOnMac(versionPreference: VersionPreference): Promise<void>
try {
await executeCommand('brew --version');
} catch {
logger.warn('Homebrew not found on PATH');
vscode.window.showErrorMessage('Homebrew is not installed. Please install brew and try again.');
return;
}

try {
logger.info('Installing Dev Proxy via Homebrew', { packageId });
await executeCommand('brew tap dotnet/dev-proxy');
await executeCommand(`brew install ${packageId}`);
logger.info('Dev Proxy installed successfully via Homebrew');
const result = await vscode.window.showInformationMessage('Dev Proxy installed.', 'Reload');
if (result === 'Reload') {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
}
} catch (error) {
logger.error('Failed to install Dev Proxy via Homebrew', error);
vscode.window.showErrorMessage(`Failed to install Dev Proxy.\n${error}`);
}
}
Expand Down Expand Up @@ -137,6 +147,7 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr
const platform = process.platform;
const versionPreference = configuration.get('version') as VersionPreference;
const isBeta = versionPreference === VersionPreference.Beta;
logger.info('Upgrading Dev Proxy', { platform, versionPreference });

// Linux uses install script to upgrade
if (platform === 'linux') {
Expand Down Expand Up @@ -182,6 +193,7 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr
isBeta
);
if (!upgraded) {
logger.warn('Upgrade via winget failed, opening upgrade docs');
openUpgradeDocumentation();
}
return;
Expand All @@ -202,11 +214,13 @@ async function upgradeDevProxy(configuration: vscode.WorkspaceConfiguration): Pr
isBeta
);
if (!upgraded) {
logger.warn('Upgrade via Homebrew failed, opening upgrade docs');
openUpgradeDocumentation();
}
return;
}

// Unknown platform
logger.warn('Unknown platform, opening upgrade docs', { platform });
openUpgradeDocumentation();
}
5 changes: 5 additions & 0 deletions src/commands/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Commands } from '../constants';
import { executeCommand } from '../utils/shell';
import { getDevProxyExe } from '../detect';
import { VersionPreference } from '../enums';
import * as logger from '../logger';

/**
* JWT (JSON Web Token) generation commands.
Expand Down Expand Up @@ -36,8 +37,10 @@ interface JwtParams {
async function createJwt(devProxyExe: string): Promise<void> {
const params = await collectJwtParams();
if (!params) {
logger.debug('JWT creation cancelled by user');
return; // User cancelled
}
logger.info('Generating JWT', { name: params.name, issuer: params.issuer, audiences: params.audiences.length, roles: params.roles.length, scopes: params.scopes.length, validFor: params.validFor });

await vscode.window.withProgress(
{
Expand All @@ -50,8 +53,10 @@ async function createJwt(devProxyExe: string): Promise<void> {
const command = buildJwtCommand(devProxyExe, params);
const result = await executeCommand(command);
const token = extractToken(result);
logger.info('JWT token generated successfully');
await presentToken(token, command);
} catch (error) {
logger.error('Failed to generate JWT token', error);
vscode.window.showErrorMessage(`Failed to generate JWT token: ${error}`);
}
}
Expand Down
Loading