Skip to content

Commit d81a379

Browse files
Prompt to add extension to workspace recommendations when Dev Proxy config detected (#340)
* Initial plan * Add workspace recommendations feature to prompt users to add extension to recommendations Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> * Fix storage key handling for workspace recommendations prompt Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> * Add CHANGELOG and README documentation for workspace recommendations feature Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> * Remove non-user-facing test fix from CHANGELOG Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> * Add workspace recommendation commands and improve notification UX - Add 'Add to Workspace Recommendations' command as manual fallback - Add 'Reset State' command to clear all extension state - Change notification buttons to Yes/No/Don't ask again - Remove unnecessary node_modules exclude from config file search - Update CHANGELOG and README to reflect changes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: garrytrinder <11563347+garrytrinder@users.noreply.github.com> Co-authored-by: Garry Trinder <garry@trinder365.co.uk>
1 parent 92cb23e commit d81a379

9 files changed

Lines changed: 297 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Added:
1313

1414
- Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only
15+
- Workspace: Added automatic prompt to recommend extension in `.vscode/extensions.json` when Dev Proxy config files are detected
16+
- Command: Added `Add to Workspace Recommendations` to manually add extension to workspace recommendations
17+
- Command: Added `Reset State` to clear all extension state
1518

1619
### Fixed:
1720

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Control Dev Proxy directly from VS Code via the Command Palette (`Cmd+Shift+P` /
3434
| Create configuration file | Dev Proxy installed |
3535
| Discover URLs to watch | Dev Proxy not running |
3636
| Generate JWT | Dev Proxy installed |
37+
| Add to Workspace Recommendations | Always |
38+
| Reset State | Always |
3739

3840
### Snippets
3941

@@ -201,6 +203,18 @@ Shows Dev Proxy status at a glance:
201203
- Running state (radio tower icon when active)
202204
- Error indicator if Dev Proxy is not installed
203205

206+
### Workspace Recommendations
207+
208+
When you open a workspace containing `devproxyrc.json` or `devproxyrc.jsonc` files, the extension will prompt you to add it to your workspace's recommended extensions (`.vscode/extensions.json`). This helps teams ensure all contributors have the Dev Proxy Toolkit installed for a consistent development experience.
209+
210+
The prompt offers three options:
211+
212+
- **Yes** — adds the extension to workspace recommendations
213+
- **No** — dismisses the prompt, it will appear again next session
214+
- **Don't ask again** — permanently suppresses the prompt for this workspace
215+
216+
You can also manually add the extension to recommendations at any time using the `Add to Workspace Recommendations` command, or use `Reset State` to clear all extension state including prompt preferences.
217+
204218
## Configuration
205219

206220
| Setting | Type | Default | Description |

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@
8989
"title": "Generate JWT",
9090
"category": "Dev Proxy Toolkit",
9191
"enablement": "isDevProxyInstalled"
92+
},
93+
{
94+
"command": "dev-proxy-toolkit.add-to-recommendations",
95+
"title": "Add to Workspace Recommendations",
96+
"category": "Dev Proxy Toolkit"
97+
},
98+
{
99+
"command": "dev-proxy-toolkit.reset-state",
100+
"title": "Reset State",
101+
"category": "Dev Proxy Toolkit"
92102
}
93103
],
94104
"mcpServerDefinitionProviders": [

src/commands/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { registerInstallCommands } from './install';
66
import { registerJwtCommands } from './jwt';
77
import { registerDiscoveryCommands } from './discovery';
88
import { registerDocCommands } from './docs';
9+
import { Commands } from '../constants';
10+
import { addExtensionToRecommendations } from '../utils';
911

1012
/**
1113
* Register all commands for the extension.
@@ -18,6 +20,7 @@ import { registerDocCommands } from './docs';
1820
* - jwt: create JWT tokens
1921
* - discovery: discover URLs to watch
2022
* - docs: open plugin documentation, add language model config
23+
* - workspace: add to recommendations
2124
*/
2225
export function registerCommands(
2326
context: vscode.ExtensionContext,
@@ -30,6 +33,31 @@ export function registerCommands(
3033
registerJwtCommands(context, configuration);
3134
registerDiscoveryCommands(context, configuration);
3235
registerDocCommands(context);
36+
37+
context.subscriptions.push(
38+
vscode.commands.registerCommand(Commands.addToRecommendations, async () => {
39+
const success = await addExtensionToRecommendations();
40+
if (success) {
41+
vscode.window.showInformationMessage('Dev Proxy Toolkit added to workspace recommendations.');
42+
} else {
43+
vscode.window.showErrorMessage('Failed to add extension to workspace recommendations. Ensure a workspace folder is open.');
44+
}
45+
})
46+
);
47+
48+
context.subscriptions.push(
49+
vscode.commands.registerCommand(Commands.resetState, async () => {
50+
const keys = context.globalState.keys();
51+
for (const key of keys) {
52+
await context.globalState.update(key, undefined);
53+
}
54+
vscode.window.showInformationMessage('Dev Proxy Toolkit state has been reset. Reload the window to apply changes.', 'Reload').then(action => {
55+
if (action === 'Reload') {
56+
vscode.commands.executeCommand('workbench.action.reloadWindow');
57+
}
58+
});
59+
})
60+
);
3361
}
3462

3563
// Re-export individual modules for testing and direct access

src/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const Commands = {
3939

4040
// Language model commands
4141
addLanguageModelConfig: 'dev-proxy-toolkit.addLanguageModelConfig',
42+
43+
// Workspace commands
44+
addToRecommendations: 'dev-proxy-toolkit.add-to-recommendations',
45+
resetState: 'dev-proxy-toolkit.reset-state',
4246
} as const;
4347

4448
/**
@@ -92,3 +96,11 @@ export const Urls = {
9296
schemaBase: 'https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas',
9397
diagnosticsDoc: 'https://learn.microsoft.com/microsoft-cloud/dev/dev-proxy/technical-reference/toolkit-diagnostics',
9498
} as const;
99+
100+
/**
101+
* Extension-related constants.
102+
*/
103+
export const Extension = {
104+
id: 'garrytrinder.dev-proxy-toolkit',
105+
extensionsJsonPath: '.vscode/extensions.json',
106+
} as const;

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { updateGlobalState } from './state';
99
import { VersionPreference } from './enums';
1010
import { registerMcpServer } from './mcp';
1111
import { registerTaskProvider } from './task-provider';
12+
import { promptForWorkspaceRecommendation } from './utils';
1213

1314
// Global variable to track the interval
1415
let statusBarInterval: NodeJS.Timeout | undefined;
@@ -35,6 +36,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise<vscode
3536
const notification = handleStartNotification(context);
3637
processNotification(notification);
3738

39+
// Prompt for workspace recommendations
40+
promptForWorkspaceRecommendation(context);
41+
3842
updateStatusBar(context, statusBar);
3943

4044
// Store the interval reference for proper cleanup
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Workspace recommendations tests.
3+
* Verifies workspace recommendation functionality for Dev Proxy Toolkit.
4+
*/
5+
import * as assert from 'assert';
6+
import * as vscode from 'vscode';
7+
import * as path from 'path';
8+
import {
9+
hasDevProxyConfig,
10+
isExtensionRecommended,
11+
addExtensionToRecommendations,
12+
sleep,
13+
} from '../utils';
14+
import { Extension } from '../constants';
15+
import { getExtensionContext, testDevProxyInstall } from './helpers';
16+
17+
suite('Workspace Recommendations', () => {
18+
let tempWorkspaceFolder: vscode.WorkspaceFolder;
19+
let tempDir: string;
20+
21+
setup(async () => {
22+
const context = await getExtensionContext();
23+
await context.globalState.update('devProxyInstall', testDevProxyInstall);
24+
25+
// Create a temporary directory for test files
26+
tempDir = path.join(process.cwd(), '.test-workspace-' + Date.now());
27+
try {
28+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(tempDir));
29+
} catch {
30+
// Directory might already exist
31+
}
32+
33+
tempWorkspaceFolder = {
34+
uri: vscode.Uri.file(tempDir),
35+
name: 'test-workspace',
36+
index: 0,
37+
};
38+
});
39+
40+
teardown(async () => {
41+
// Clean up test files
42+
try {
43+
await vscode.workspace.fs.delete(vscode.Uri.file(tempDir), { recursive: true });
44+
} catch {
45+
// Ignore errors
46+
}
47+
});
48+
49+
test('hasDevProxyConfig should return false when no config files exist', async () => {
50+
const result = await hasDevProxyConfig();
51+
// In the actual workspace, we don't expect config files unless they're in test/examples
52+
// This is a best-effort test
53+
assert.ok(result !== undefined);
54+
});
55+
56+
test('isExtensionRecommended should return false when extensions.json does not exist', async () => {
57+
// This test requires a workspace folder, but we can't easily mock it
58+
// Just ensure the function runs without error
59+
const result = await isExtensionRecommended();
60+
assert.ok(result === false || result === true);
61+
});
62+
63+
test('addExtensionToRecommendations should create extensions.json if it does not exist', async () => {
64+
// This test requires manipulating workspace folders, which is difficult in tests
65+
// We'll just ensure the function is callable
66+
const result = await addExtensionToRecommendations();
67+
assert.ok(result === false || result === true);
68+
});
69+
70+
test('Extension constant should have correct ID', () => {
71+
assert.strictEqual(Extension.id, 'garrytrinder.dev-proxy-toolkit');
72+
});
73+
74+
test('Extension constant should have correct extensions.json path', () => {
75+
assert.strictEqual(Extension.extensionsJsonPath, '.vscode/extensions.json');
76+
});
77+
});

src/utils/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ export {
3030

3131
// Re-export from detect for convenience
3232
export { getDevProxyExe } from '../detect';
33+
34+
// Workspace recommendations utilities
35+
export {
36+
hasDevProxyConfig,
37+
isExtensionRecommended,
38+
addExtensionToRecommendations,
39+
promptForWorkspaceRecommendation,
40+
} from './workspace-recommendations';
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
import { Extension } from '../constants';
4+
5+
/**
6+
* Utilities for managing workspace extension recommendations.
7+
*/
8+
9+
/**
10+
* Check if workspace contains Dev Proxy config files.
11+
*/
12+
export async function hasDevProxyConfig(): Promise<boolean> {
13+
const files = await vscode.workspace.findFiles(
14+
'{devproxyrc.json,devproxyrc.jsonc}'
15+
);
16+
return files.length > 0;
17+
}
18+
19+
/**
20+
* Check if the Dev Proxy Toolkit extension is already in workspace recommendations.
21+
*/
22+
export async function isExtensionRecommended(): Promise<boolean> {
23+
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
24+
return false;
25+
}
26+
27+
const workspaceFolder = vscode.workspace.workspaceFolders[0];
28+
const extensionsJsonPath = path.join(workspaceFolder.uri.fsPath, Extension.extensionsJsonPath);
29+
30+
try {
31+
const uri = vscode.Uri.file(extensionsJsonPath);
32+
const document = await vscode.workspace.openTextDocument(uri);
33+
const content = document.getText();
34+
const json = JSON.parse(content);
35+
36+
if (json.recommendations && Array.isArray(json.recommendations)) {
37+
return json.recommendations.includes(Extension.id);
38+
}
39+
} catch (error) {
40+
// File doesn't exist or can't be parsed
41+
return false;
42+
}
43+
44+
return false;
45+
}
46+
47+
/**
48+
* Add the Dev Proxy Toolkit extension to workspace recommendations.
49+
*/
50+
export async function addExtensionToRecommendations(): Promise<boolean> {
51+
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
52+
return false;
53+
}
54+
55+
const workspaceFolder = vscode.workspace.workspaceFolders[0];
56+
const vscodeFolderPath = path.join(workspaceFolder.uri.fsPath, '.vscode');
57+
const extensionsJsonPath = path.join(workspaceFolder.uri.fsPath, Extension.extensionsJsonPath);
58+
59+
try {
60+
let json: { recommendations?: string[] } = {};
61+
62+
// Try to read existing file
63+
try {
64+
const uri = vscode.Uri.file(extensionsJsonPath);
65+
const document = await vscode.workspace.openTextDocument(uri);
66+
json = JSON.parse(document.getText());
67+
} catch {
68+
// File doesn't exist or can't be parsed, create new structure
69+
json = { recommendations: [] };
70+
}
71+
72+
// Ensure recommendations array exists
73+
if (!json.recommendations) {
74+
json.recommendations = [];
75+
}
76+
77+
// Add extension if not already present
78+
if (!json.recommendations.includes(Extension.id)) {
79+
json.recommendations.push(Extension.id);
80+
}
81+
82+
// Create .vscode directory if it doesn't exist
83+
try {
84+
await vscode.workspace.fs.createDirectory(vscode.Uri.file(vscodeFolderPath));
85+
} catch {
86+
// Directory might already exist
87+
}
88+
89+
// Write the updated file
90+
const uri = vscode.Uri.file(extensionsJsonPath);
91+
const content = JSON.stringify(json, null, 2);
92+
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8'));
93+
94+
return true;
95+
} catch (error) {
96+
console.error('Error adding extension to recommendations:', error);
97+
return false;
98+
}
99+
}
100+
101+
/**
102+
* Prompt user to add the extension to workspace recommendations.
103+
*/
104+
export async function promptForWorkspaceRecommendation(context: vscode.ExtensionContext): Promise<void> {
105+
// Check if we've already prompted for this workspace
106+
const workspaceKey = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';
107+
// Use a safe storage key by replacing path separators with underscores
108+
const storageKey = `recommendation-prompted-${workspaceKey.replace(/[/\\:]/g, '_')}`;
109+
110+
if (context.globalState.get(storageKey)) {
111+
// Already prompted for this workspace
112+
return;
113+
}
114+
115+
// Check if workspace has Dev Proxy config
116+
const hasConfig = await hasDevProxyConfig();
117+
if (!hasConfig) {
118+
return;
119+
}
120+
121+
// Check if extension is already recommended
122+
const isRecommended = await isExtensionRecommended();
123+
if (isRecommended) {
124+
return;
125+
}
126+
127+
// Show prompt
128+
const message = 'This workspace contains Dev Proxy configuration files. Would you like to add the Dev Proxy Toolkit extension to workspace recommendations?';
129+
const result = await vscode.window.showInformationMessage(message, 'Yes', 'No', 'Don\'t ask again');
130+
131+
if (result === 'Yes') {
132+
const success = await addExtensionToRecommendations();
133+
if (success) {
134+
vscode.window.showInformationMessage('Dev Proxy Toolkit added to workspace recommendations.');
135+
} else {
136+
vscode.window.showErrorMessage('Failed to add extension to workspace recommendations.');
137+
}
138+
} else if (result === 'Don\'t ask again') {
139+
await context.globalState.update(storageKey, true);
140+
}
141+
}

0 commit comments

Comments
 (0)