Create New Project design improvements#4964
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new opt-in “Create New Project” template gallery (webview + template manifest provider) and introduces smarter local workflows (Run Function App pre-start steps, Copilot-based validation, and Smart Deploy) to improve project creation, run, validate, and deploy experiences in the Azure Functions VS Code extension.
Changes:
- Added a template-gallery-driven project creation flow (webview UI + remote/bundled manifest provider + clone/copy steps).
- Added new commands: Run Function App (runtime-aware setup), Validate Function App (Copilot + diagnostics), and Smart Deploy (AZD-aware routing).
- Improved activation/init behavior by detecting and persisting the project language model when missing.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts | Detects and persists projectLanguageModel during activation when missing. |
| src/vsCodeConfig/verifyInitForVSCode.ts | Detects and persists projectLanguageModel during init verification when missing. |
| src/templates/projectTemplates/ProjectTemplateProvider.ts | Fetches/merges/caches template manifests and filters templates by language/model. |
| src/templates/projectTemplates/IProjectTemplate.ts | Defines template/manifest types used by the template gallery and wizard flow. |
| src/extensionVariables.ts | Adds a shared diagnostic collection handle to extension globals. |
| src/extension.ts | Initializes and disposes the Azure Functions diagnostic collection on activation. |
| src/commands/validateFunctionApp/FunctionAppValidator.ts | Implements Copilot-based project validation and publishes results as VS Code diagnostics. |
| src/commands/runFunctionApp/RunFunctionApp.ts | Adds runtime-aware “Run Function App” behavior (venv/npm/dotnet build + func start). |
| src/commands/registerCommands.ts | Registers new commands (validate, smart deploy, run function app). |
| src/commands/deploy/SmartDeploy.ts | Adds deploy routing logic (AZD-aware) while delegating to existing deploy where appropriate. |
| src/commands/createNewProject/TemplateListStep.ts | Adds Quick Pick template selection (category-grouped) for the wizard template flow. |
| src/commands/createNewProject/TemplateGalleryPanel.ts | Implements the template gallery webview controller, template cloning, and AI project creation. |
| src/commands/createNewProject/StartingPointStep.ts | Adds “template vs scratch” starting point step to the classic wizard flow. |
| src/commands/createNewProject/PostCloneStep.ts | Adds post-clone analysis (Bicep/README detection) and follow-up notifications. |
| src/commands/createNewProject/NewProjectLanguageStep.ts | Adds a “Browse Template Gallery…” discovery option in the classic language picker. |
| src/commands/createNewProject/IProjectWizardContext.ts | Extends wizard context with template-flow-specific properties. |
| src/commands/createNewProject/createNewProject.ts | Branches between classic wizard and new gallery flow based on an opt-in setting. |
| src/commands/createNewProject/CloneTemplateStep.ts | Adds a wizard execute step to clone/copy a template repo (including sparse checkout). |
| resources/webviews/templateGallery/styles.css | Adds styling for the template gallery and AI generation UI. |
| resources/webviews/templateGallery/main.js | Implements template gallery filtering, selection, and AI-generation UX in the webview. |
| resources/webviews/templateGallery/index.html | Static template gallery HTML used as a webview resource. |
| resources/skills/functionapp.md | Adds common (all runtimes) validator “skill” rules/prompting schema. |
| resources/skills/python.md | Adds Python-specific validator rules/prompting guidance. |
| resources/backupProjectTemplates/manifest.json | Adds an offline fallback template manifest. |
| package.json | Contributes new commands/menus and new project template settings; adds codicons dependency. |
| package.nls.json | Adds localized strings for new commands and project template settings. |
| package-lock.json | Updates lockfile for new dependency additions and version bump. |
| .vscodeignore | Ensures codicons assets are packaged for the webview. |
|
Alright, I have some feedback on this and I'm happy to provide more info for any First of all, I tested TypeScript and JavaScript templates on both Windows and It's worth noting that the "create new function" command is available and works
|
Hi @fiveisprime, Thanks for testing this new change. I have addressed your comments in microsoft/vscode-azuretools#2278 since the webview is moved to the other repo. Let me know if you have any other feedback, Thanks. |
| "@microsoft/vscode-azext-azureappsettings": "^1.0.0", | ||
| "@microsoft/vscode-azext-azureutils": "^4.0.1", | ||
| "@microsoft/vscode-azext-utils": "^4.0.5", | ||
| "@microsoft/vscode-azext-webview": "file:../vscode-azuretools/webview", |
There was a problem hiding this comment.
Will replace with the correct build before merging..
nturinski
left a comment
There was a problem hiding this comment.
Reviewed for blocking issues only — inline comments cover them. High level:
- The PR description only covers the webview Template Gallery, but the diff also adds (1) an entirely new quick-pick template flow (
StartingPointStep+ 5 supporting files + 846-line bundled manifest) that is dead code, and (2) two brand-new top-level commands (smartDeploy,runFunctionApp) that overlap existing commands. Strongly recommend splitting into separate PRs — they have very different review surfaces. package.nls.jsondoesn't parse.file:sibling dep +--no-dependenciespackaging will both fail outside the author's local checkout.- The new editor-title menu items appear on every open editor in VS Code.
- Two security issues in the AI/clone code paths (path-traversal check, shell quoting).
Happy to look at a follow-up once the dead code is removed and the gallery is split out.
| "azureFunctions.projectTemplates.additionalManifestUrls": "Additional template manifest URLs to merge with the primary manifest.", | ||
| "azureFunctions.projectTemplates.cacheExpirationHours": "Template manifest cache expiration time in hours.", | ||
| "azureFunctions.projectTemplates.preferTemplateFlow": "Pre-select 'Start from template' in the project creation wizard.", | ||
| "azureFunctions.projectTemplates.showBicepPrompt": "Prompt to deploy infrastructure when templates include Bicep files." |
There was a problem hiding this comment.
Missing trailing comma — this breaks package.nls.json parsing and all NLS resolution.
| "version": "tsc --version", | ||
| "cleanReadme": "node scripts/cleanReadme.mjs", | ||
| "package": "vsce package --githubBranch main", | ||
| "package": "vsce package --githubBranch main --no-dependencies", |
There was a problem hiding this comment.
--no-dependencies ships the VSIX without node_modules, so anything not bundled by esbuild will fail at runtime. This repo's esbuild.mjs wasn't extended to verify every entry under dependencies is bundled. Please install the packaged VSIX and exercise activation before merging, or drop this flag.
| "editor/title": [ | ||
| { | ||
| "command": "azureFunctions.runFunctionApp", | ||
| "when": "editorIsOpen", | ||
| "group": "navigation@0" | ||
| }, | ||
| { | ||
| "command": "azureFunctions.smartDeploy", | ||
| "when": "editorIsOpen", | ||
| "group": "navigation@1" |
There was a problem hiding this comment.
"when": "editorIsOpen" adds a "Run Function App" and "Deploy Function App" icon to the title bar of every open editor in VS Code, including unrelated workspaces. Needs to be gated on a Functions-project context (e.g. azureFunctions.projectLanguage), and arguably shouldn't be in editor/title at all.
| { | ||
| "command": "azureFunctions.smartDeploy", | ||
| "when": "view == azureWorkspace && viewItem =~ /azFuncLocalProject/i", | ||
| "group": "inline" | ||
| }, | ||
| { | ||
| "command": "azureFunctions.smartDeploy", | ||
| "when": "view == azureWorkspace && viewItem =~ /azFuncLocalProject/i", | ||
| "group": "1@2b" | ||
| }, |
There was a problem hiding this comment.
smartDeploy is registered twice for the same view item (inline + 1@2b); runFunctionApp below is duplicated the same way. Each command will appear twice in the context menu.
| const safePath = file.path.replace(/^[/\\]/, ''); | ||
| const filePath = path.resolve(targetDir, safePath); | ||
| if (!filePath.startsWith(path.resolve(targetDir))) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Path-traversal check is unsafe:
replace(/^[/\\]/, '')strips only one leading separator, so..\..\evilstill escapes.startsWith(path.resolve(targetDir))without a trailingpath.sepaccepts e.g.targetDir = C:\foo→C:\foobar\evil.txt.- Silently
continue-ing leaves no signal that the AI's output was partially dropped.
Suggest:
const rel = path.relative(targetDir, filePath);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(`Refusing to write outside project: ${file.path}`);
}| await cpUtils.executeCommandLine(undefined, undefined, `tar -xf "${zipPath}" -C "${path.dirname(destDir)}"`); | ||
| } catch { | ||
| if (process.platform === 'win32') { | ||
| await cpUtils.executeCommandLine(undefined, undefined, | ||
| `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${path.dirname(destDir)}' -Force"`); | ||
| } else { | ||
| await cpUtils.executeCommandLine(undefined, undefined, `unzip -q "${zipPath}" -d "${path.dirname(destDir)}"`); |
There was a problem hiding this comment.
String-interpolated shell commands are a quoting-injection class bug — os.tmpdir() contains the username on Windows, so a ' or " in the path breaks PowerShell's single-quoted args. Use cpUtils.executeCommand with an argv array, or extract the zip with a Node library (yauzl/adm-zip) so no shell is involved.
There was a problem hiding this comment.
This whole file (along with TemplateListStep, CloneTemplateStep, PostCloneStep, IProjectTemplate, ProjectTemplateProvider, and the 846-line bundled manifest.json) is dead code — nothing in createNewProjectInternal is modified to insert StartingPointStep into the wizard, and no other call site references it. Either wire it in or remove it from this PR.
| @@ -0,0 +1,511 @@ | |||
| /*--------------------------------------------------------------------------------------------- | |||
There was a problem hiding this comment.
Could you explain the intention behind RunFunctionApp.ts being added to this PR? This seems similar to what the extension already ships using VS Code's task and debug APIs. For example:
FuncTaskProvider auto-contributes func: host start (plus func: extensions install and Python pack / pack --build-native-deps) tasks for every Functions project.
The per-language FuncDebugProviderBase subclasses wire those tasks up as the preLaunchTask for F5.
validatePreDebug runs the necessary pre-flight checks (Core Tools installed and version-compatible, FUNCTIONS_WORKER_RUNTIME present, durable-storage connections, AzureWebJobsStorage and Azurite actually running).
PythonVenvCreateStep, PythonAliasListStep, and venvUtils (venvExists, runPipInstallCommandIfPossible, convertToVenvCommand) own venv lifecycle.
The language-specific init steps (JavaScriptInitVSCodeStep, TypeScriptInitVSCodeStep, PythonInitVSCodeStep, Script/C# variants) already declare npm install / npm run build / dotnet build / pip install as dependsOn tasks for host start.
funcHostTask tracks the running host so the FunctionHostDebugView can surface logs, errors, and the host
PID.
However, RunFunctionApp.ts ignores every one of those systems and reimplements each one inline — project-root discovery (a worse copy of tryGetFunctionProjectRoot), runtime detection (a worse copy of tryGetFunctionsWorkerRuntimeForProject that only knows three languages and silently falls through to "generic" for Java/PowerShell/Ballerina/Custom), Python alias probing, venv creation, pip install, npm install/build, and dotnet build. Any bug fix or new language now has two places to update, with the new copy guaranteed to drift.
It is also strictly less flexible. It hardcodes .venv while the rest of the extension respects the azureFunctions.pythonVenv workspace setting. It ignores user customizations to the func: host start task in tasks.json (custom args, env vars, problem matchers, working directory), the azureFunctions.projectLanguage / azureFunctions.preDeployTask / validateEmulators settings, custom preDeployTask hooks, and the Functions Core Tools path resolved by getFuncCliPath. It bypasses Azurite validation, durable-storage connection setup, and the worker-runtime auto-repair that preDebugValidate does.
Because it spawns func and npm / dotnet directly, the host doesn't show up in the Function Host Debug view, has no problem matcher integration, no port detection, no pickFuncProcess support, and no Activity Log entries.
Its Python workaround — a custom Pseudoterminal that silently swallows all stdin to dodge the Python extension's auto-activation injection — is a workaround for a problem the existing convertToVenvCommand-based ShellExecution path doesn't have, and it solves it by breaking every legitimate stdin use case (Ctrl+C is handled, but anything else a user types is dropped on the floor).
On top of that it ships several anti-patterns the rest of the codebase has long since moved past:
Synchronous fall-through try/catch blocks that swallow pip install / npm install / npm run build / dotnet build failures and continue to func start anyway, so the user sees a broken host instead of the real error.
JSON.parse on local.settings.json instead of the existing getLocalSettingsJson helper that handles JSONC and connection-string parsing.
A hand-rolled Windows process-tree kill via taskkill instead of the shared process utilities.
An entirely new top-level command and toolbar button that overlaps in purpose with the existing F5 debug entry point and the run icon already wired into the Functions walkthrough — adding user-visible surface area, telemetry, localization strings, and a maintenance burden for a capability the extension already provides more correctly.
| // Public entry point | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export async function smartDeploy(_context: IActionContext, resourceUri?: vscode.Uri): Promise<void> { |
There was a problem hiding this comment.
I’d consider folding the smartDeploy behavior into the existing azureFunctions.deployProject command rather than adding it as a parallel deploy command.
The main reason is that smartDeploy doesn’t seem like a fundamentally different deploy path. It’s more of an AZD-aware layer on top of the existing deploy flow. In the non-AZD case, it already falls back to:
vscode.commands.executeCommand('azureFunctions.deployProject', resourceUri)
So structurally, it feels like the AZD detection belongs inside deployProject itself.
Having both commands registered side by side also introduces a few downsides:
- User choice becomes less clear. Two similar “Deploy…” entries make the user decide which deploy path is the right one. The AZD-aware behavior feels like the thing most users would expect from the normal Deploy command.
- Existing entry points may miss the new behavior. Tree-view menus, walkthroughs, internal callers, post-deploy flows, slot deploy, and the public extension API already point at azureFunctions.deployProject. Those would bypass AZD detection unless deployProject becomes the smart default.
- Telemetry and debugging get split. Separate command IDs make it harder to understand which deploy path a user took when looking at telemetry or investigating issues.
- The two paths may drift over time. Project-root resolution, validation, sign-in handling, slot handling, and post-deploy tasks are all areas where keeping two paths aligned could become easy to miss.
A possible shape would be:
- In deployProject, resolve the project root as usual.
- Before launching the normal wizard, call detectAzdProject.
- If an azure.yaml or azure.yml is found, prompt with something like:
- Deploy with azd up
- Use Functions deploy
- Cancel
- Gate that with a setting like:
azureFunctions.deploy.azdPromptBehavior: 'prompt' | 'azd' | 'functions'
and include a “Don’t ask again” option.
With that structure, azureFunctions.deployProject remains the main deploy entry point, but becomes AZD-aware. SmartDeploy.ts can still exist as the place for detectAzdProject and runAzd* helpers; it just wouldn’t need to register a separate command/menu item.
| // AZD invocation | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| async function runAzdDeploy(context: IActionContext, projectRoot: string): Promise<void> { |
There was a problem hiding this comment.
I’d also simplify the AZD execution path by relying on the AZD extension rather than maintaining our own CLI fallback.
Right now, SmartDeploy has a few layers:
- Try azure-dev.commands.cli.*
- Fall back to spawning azd in a terminal
- If that fails, show docs/install guidance
I don’t think we need all three. The AZD extension already exposes the CLI surface through commands like:
azure-dev.commands.cli.up
azure-dev.commands.cli.init
azure-dev.commands.cli.provision
azure-dev.commands.cli.deploy
azure-dev.commands.cli.down
azure-dev.commands.cli.install
Since the AZD extension owns the install and CLI-missing experience, we can route through it instead of duplicating CLI detection and terminal handling.
| } | ||
|
|
||
| function detectAzdProject(projectRoot: string): AzdStatus { | ||
| const azureYamlPath = path.join(projectRoot, 'azure.yaml'); |
There was a problem hiding this comment.
I think technically azd handles both azure.yaml and azure.yml.
Summary
Introduces an opt-in Template Gallery experience for "Create New Project" that consumes the new shared webview package from
microsoft/vscode-azuretools. All UI lives in the shared package; this extension only owns:The extension's previous in-repo Template Gallery (HTML/CSS/JS in
resources/webviews/templateGallery/from PR #4964, and the React port insrc/webview/) is removed in favor of the shared package.How to enable
The gallery is gated behind an opt-in setting and does not affect the default Create-Project wizard:
"azureFunctions.enableTemplateGallery": trueArchitecture
The shared controller creates the
WebviewPanel, routes typed messages between the webview and the subclass, and renders the React UI bundled in the package. The subclass implementsfetchTemplates,createProject,getReadme, and optionally the three AI methods.Notable behaviors
azureFunctions.enableTemplateGallerysettingcreateNewProject.tscreateProject().vscode,.git,.DS_Store,Thumbs.db) ignored when assessing emptinesscreateProject().gitfolder removed;git initcreates a fresh history (no template author commits)createProject()_tryOpenReadme()templateGallery.getTemplates,templateGallery.createProject,templateGallery.generateWithCopilot,templateGallery.createAiProjectcallWithTelemetryAndErrorHandlingChanges
Added
src/commands/createNewProject/FunctionsTemplateGalleryController.tsTemplateGalleryController; ports all gallery business logicscripts/copyWebviewAssets.mjsviews.js/views.cssfrom the shared package intodist/webview/at build time so the VSIX ships the bundle.npmrc(install-links=true)file:dependency as a copy (not a symlink) sovsce packagedoesn't traverse outside the extension folderModified
src/extension.tsregisterWebviewExtensionVariables({ context, webviewAssetsDir })so the sharedWebviewBaseControllercan locate the bundled assetssrc/commands/createNewProject/createNewProject.tsFunctionsTemplateGalleryController.createOrShow(ext.context)src/commands/createNewProject/NewProjectLanguageStep.tspackage.json@microsoft/vscode-azext-webviewdependency; replacesbuild:webviewscript (esbuild → copy assets); adds--no-dependenciestovsce package(extension is fully bundled by esbuild).vscodeignoretsconfig.json"src/webview"fromexclude(the folder is gone)