Skip to content
Open
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: 2 additions & 2 deletions webview/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 webview/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@microsoft/vscode-azext-webview",
"author": "Microsoft Corporation",
"version": "1.2.0",
"version": "1.3.0",
"description": "Common tools for web views in VS Code extensions",
"tags": [
"vscode"
Expand Down
26 changes: 19 additions & 7 deletions webview/src/extension/TemplateGalleryController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { ext } from './extensionVariables';
import { WebviewBaseController } from './WebviewBaseController';
import type { IProjectTemplate, ProjectCreationEntryPoint, TemplateGalleryConfig, WebviewToExtensionMessage, ExtensionToWebviewMessage } from '../webview/TemplateGallery/types';
import type { IProjectTemplate, ProjectCreationEntryPoint, TemplateGalleryConfig, TemplateGalleryWorkspaceOptionValues, WebviewToExtensionMessage, ExtensionToWebviewMessage } from '../webview/TemplateGallery/types';

/**
* Abstract controller for the Template Gallery webview panel.
Expand Down Expand Up @@ -83,7 +83,13 @@ export abstract class TemplateGalleryController extends WebviewBaseController<Te
protected abstract fetchTemplates(): Promise<{ templates: IProjectTemplate[]; defaultLocation: string }>;

/** Create a project from the selected template. */
protected abstract createProject(template: IProjectTemplate, language: string, location: string, entryPoint?: ProjectCreationEntryPoint): Promise<void>;
protected abstract createProject(
template: IProjectTemplate,
language: string,
location: string,
entryPoint?: ProjectCreationEntryPoint,
options?: TemplateGalleryWorkspaceOptionValues,
): Promise<void>;

/** Fetch the README markdown for a selected template. */
protected abstract getReadme(template: IProjectTemplate): Promise<string>;
Expand All @@ -97,7 +103,7 @@ export abstract class TemplateGalleryController extends WebviewBaseController<Te
* The webview hands the user off to chat for multi-turn design and
* file generation — there is no separate "generate then write" pipeline.
*/
protected async continueInChat(_prompt: string, _language: string): Promise<void> {
protected async continueInChat(_prompt: string, _language: string, _location: string, _options: TemplateGalleryWorkspaceOptionValues): Promise<void> {
// No-op by default.
}

Expand Down Expand Up @@ -160,15 +166,15 @@ export abstract class TemplateGalleryController extends WebviewBaseController<Te
break;

case 'createProject':
await this._handleCreateProject(message.template, message.language, message.location, message.entryPoint);
await this._handleCreateProject(message.template, message.language, message.location, message.entryPoint, message.options);
break;

case 'browseFolder':
await this._handleBrowseFolder(message.source);
break;

case 'continueInChat':
await this.continueInChat(message.prompt, message.language);
await this.continueInChat(message.prompt, message.language, message.location, message.options);
break;

case 'showError':
Expand Down Expand Up @@ -219,7 +225,13 @@ export abstract class TemplateGalleryController extends WebviewBaseController<Te
}
}

private async _handleCreateProject(template: IProjectTemplate, language: string, location: string, entryPoint?: ProjectCreationEntryPoint): Promise<void> {
private async _handleCreateProject(
template: IProjectTemplate,
language: string,
location: string,
entryPoint?: ProjectCreationEntryPoint,
options?: TemplateGalleryWorkspaceOptionValues,
): Promise<void> {
try {
let projectPath = location?.trim() ?? '';

Expand All @@ -245,7 +257,7 @@ export abstract class TemplateGalleryController extends WebviewBaseController<Te
this.postMessageToWebview({ type: 'folderSelected', path: projectPath, source: 'template' });
}

await this.createProject(template, language, projectPath, entryPoint);
await this.createProject(template, language, projectPath, entryPoint, options);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.postMessageToWebview({ type: 'projectCreationFailed', error: msg });
Expand Down
7 changes: 4 additions & 3 deletions webview/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export { WebviewBaseController } from './extension/WebviewBaseController';
export { WebviewController } from './extension/WebviewController';
export type { WebviewBundleLocation } from './extension/WebviewController';
export type {
ActiveView, AiState, ExtensionToWebviewMessage, FilterState, IProjectTemplate,
ProjectCreationEntryPoint, TemplateGalleryConfig, ViewMode, WebviewToExtensionMessage
ActiveView, AiExamplePrompt, AiGenerationConfig, AiState, ExtensionToWebviewMessage,
FilterState, IProjectTemplate, ProjectCreationEntryPoint, TemplateGalleryConfig,
TemplateGalleryWorkspaceOption, TemplateGalleryWorkspaceOptionValues, ViewMode,
WebviewToExtensionMessage
} from './webview/TemplateGallery/types';

Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import { createContext, useContext, useMemo } from 'react';
import {
defaultCategoryDisplayNames,
defaultCategoryOrder,
defaultAiGenerationConfig,
defaultLanguageDisplayNames,
defaultLanguageFilterMap,
defaultLanguageOrder,
defaultResourceDisplayNames,
defaultResourceOrder,
type ResolvedAiGenerationConfig,
type TemplateGalleryConfig,
type TemplateGalleryWorkspaceOption,
} from './types';

export interface TemplateGalleryConfigContextValue {
serviceName: string;
headerTitle: string;
headerSubtitle: string;
supportsAiGeneration: boolean;
aiGeneration: ResolvedAiGenerationConfig;
workspaceOptions: TemplateGalleryWorkspaceOption[];
languageDisplayNames: Record<string, string>;
categoryDisplayNames: Record<string, string>;
resourceDisplayNames: Record<string, string>;
Expand All @@ -35,6 +40,8 @@ const TemplateGalleryConfigContext = createContext<TemplateGalleryConfigContextV
headerTitle: '',
headerSubtitle: '',
supportsAiGeneration: false,
aiGeneration: defaultAiGenerationConfig,
workspaceOptions: [],
languageDisplayNames: defaultLanguageDisplayNames,
categoryDisplayNames: defaultCategoryDisplayNames,
resourceDisplayNames: defaultResourceDisplayNames,
Expand All @@ -59,6 +66,13 @@ export const TemplateGalleryConfigProvider = ({
headerTitle: config.headerTitle,
headerSubtitle: config.headerSubtitle,
supportsAiGeneration: config.supportsAiGeneration,
aiGeneration: {
...defaultAiGenerationConfig,
...config.aiGeneration,
examplePrompts: config.aiGeneration?.examplePrompts ?? defaultAiGenerationConfig.examplePrompts,
capabilities: config.aiGeneration?.capabilities ?? defaultAiGenerationConfig.capabilities,
},
workspaceOptions: config.workspaceOptions ?? [],
languageDisplayNames: { ...defaultLanguageDisplayNames, ...config.languageDisplayNames },
categoryDisplayNames: { ...defaultCategoryDisplayNames, ...config.categoryDisplayNames },
resourceDisplayNames: { ...defaultResourceDisplayNames, ...config.resourceDisplayNames },
Expand Down
39 changes: 33 additions & 6 deletions webview/src/webview/TemplateGallery/TemplateGalleryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CreatingView } from './components/CreatingView';
import { FilterBar } from './components/FilterBar';
import { TemplateCard } from './components/TemplateCard';
import { TemplateConfigView } from './components/TemplateConfigView';
import { WorkspaceOptions } from './components/WorkspaceOptions';
import type {
TemplateGalleryAction as Action,
ActiveView,
Expand All @@ -25,6 +26,8 @@ import type {
IProjectTemplate,
ProjectCreationEntryPoint,
TemplateGalleryConfig,
TemplateGalleryWorkspaceOption,
TemplateGalleryWorkspaceOptionValues,
ViewMode,
WebviewToExtensionMessage,
} from './types';
Expand Down Expand Up @@ -143,6 +146,10 @@ function createReducer(languageFilterMap: Record<string, string>, languageDispla
};
}

function defaultWorkspaceOptionValues(options: readonly TemplateGalleryWorkspaceOption[]): TemplateGalleryWorkspaceOptionValues {
return Object.fromEntries(options.map(option => [option.id, option.defaultValue]));
}

// ── Inner component that uses config context ──

const TemplateGalleryViewInner = (): JSX.Element => {
Expand All @@ -154,6 +161,7 @@ const TemplateGalleryViewInner = (): JSX.Element => {
[config.languageFilterMap, config.languageDisplayNames],
);
const [state, dispatch] = useReducer(reducer, initialState);
const [workspaceOptions, setWorkspaceOptions] = React.useState<TemplateGalleryWorkspaceOptionValues>(() => defaultWorkspaceOptionValues(config.workspaceOptions));

const postMessage = useCallback((msg: WebviewToExtensionMessage) => {
vscodeApi.postMessage(msg);
Expand Down Expand Up @@ -205,6 +213,10 @@ const TemplateGalleryViewInner = (): JSX.Element => {
postMessage({ type: 'getTemplates' });
}, [postMessage]);

useEffect(() => {
setWorkspaceOptions(defaultWorkspaceOptionValues(config.workspaceOptions));
}, [config.workspaceOptions]);

// ── Handlers ──

const handleSelectTemplate = useCallback((template: IProjectTemplate) => {
Expand All @@ -220,17 +232,21 @@ const TemplateGalleryViewInner = (): JSX.Element => {
postMessage({ type: 'browseFolder', source });
}, [postMessage]);

const handleCreateProject = useCallback((template: IProjectTemplate, language: string, location: string, entryPoint: ProjectCreationEntryPoint) => {
const handleCreateProject = useCallback((template: IProjectTemplate, language: string, location: string, options: TemplateGalleryWorkspaceOptionValues, entryPoint: ProjectCreationEntryPoint) => {
dispatch({ type: 'SET_VIEW', view: 'creating' });
postMessage({ type: 'createProject', template, language, location, entryPoint });
postMessage({ type: 'createProject', template, language, location, options, entryPoint });
}, [postMessage]);

const handleWorkspaceOptionChange = useCallback((id: string, checked: boolean) => {
setWorkspaceOptions(current => ({ ...current, [id]: checked }));
}, []);

// "Use Template" button on the card → create immediately using the default
// project location and the template's first language. Skips the details screen.
const handleUseTemplateDirect = useCallback((template: IProjectTemplate) => {
const language = template.languages[0] || '';
handleCreateProject(template, language, state.projectLocation, 'card');
}, [handleCreateProject, state.projectLocation]);
handleCreateProject(template, language, state.projectLocation, workspaceOptions, 'card');
}, [handleCreateProject, state.projectLocation, workspaceOptions]);

const handleRefresh = useCallback(() => {
dispatch({ type: 'SET_LOADING' });
Expand All @@ -256,7 +272,9 @@ const TemplateGalleryViewInner = (): JSX.Element => {
readmeLoading={state.readmeLoading}
onBack={handleBackToGallery}
onBrowse={() => handleBrowseFolder('template')}
onCreateProject={(template, language, location) => handleCreateProject(template, language, location, 'details')}
onCreateProject={(template, language, location, options) => handleCreateProject(template, language, location, options, 'details')}
workspaceOptions={workspaceOptions}
onWorkspaceOptionChange={handleWorkspaceOptionChange}
/>
);
}
Expand Down Expand Up @@ -289,7 +307,7 @@ const TemplateGalleryViewInner = (): JSX.Element => {
</Tab>
{config.supportsAiGeneration && (
<Tab value="ai" icon={<span className="codicon codicon-sparkle"></span>}>
Build with Copilot Chat
{config.aiGeneration.tabLabel}
</Tab>
)}
</TabList>
Expand All @@ -305,6 +323,12 @@ const TemplateGalleryViewInner = (): JSX.Element => {
onClearFilters={() => dispatch({ type: 'CLEAR_FILTERS' })}
/>

<WorkspaceOptions
options={config.workspaceOptions}
values={workspaceOptions}
onChange={handleWorkspaceOptionChange}
/>

<div className="results-bar">
<span className="results-count">
Showing {state.filteredTemplates.length} template{state.filteredTemplates.length !== 1 ? 's' : ''}
Expand Down Expand Up @@ -384,8 +408,11 @@ const TemplateGalleryViewInner = (): JSX.Element => {
{state.mode === 'ai' && config.supportsAiGeneration && (
<AiGenerateView
ai={state.ai}
projectLocation={state.projectLocation}
workspaceOptions={workspaceOptions}
postMessage={postMessage}
dispatch={dispatch}
onWorkspaceOptionChange={handleWorkspaceOptionChange}
/>
)}
</div>
Expand Down
Loading
Loading