Skip to content
Draft
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
19 changes: 19 additions & 0 deletions utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,14 @@ export declare function callWithTelemetryAndErrorHandlingSync<T>(callbackId: str
*/
export declare function callWithMaskHandling<T>(callback: () => Promise<T>, valueToMask: string): Promise<T>;

/**
* A wrapper for the VS Code executeCommand command
* Used to pass in additional context when executing a command
* @param commandId Identifier of the command to execute
* @param additionalContext Full context including addtional properties
* @param args Parameters passed to the command function
*/
export declare function executeCommandWithAddedContext<T>(commandId: string, additionalContext: Partial<IActionContext>, ...args: unknown[]): Thenable<T>;
/**
* Add an extension-wide value to mask for all commands
* This will apply to telemetry and "Report Issue", but _not_ VS Code UI (i.e. the error notification or output channel)
Expand Down Expand Up @@ -1413,6 +1421,10 @@ export interface ActivityAttributes {
* Any Azure resource envelope related to the command or activity being run
*/
azureResource?: unknown;
/**
* Optional Azure subscription to be added
*/
subscription?: AzureSubscription;

// For additional one-off properties that could be useful for Copilot
[key: string]: unknown;
Expand Down Expand Up @@ -2383,6 +2395,13 @@ export declare class QuickPickAzureResourceStep extends GenericQuickPickStep<Azu
export declare function runQuickPickWizard<TPick>(context: PickExperienceContext, wizardOptions?: IWizardOptions<AzureResourceQuickPickWizardContext>, startingNode?: unknown): Promise<TPick>;
//#endregion

/**
* Creates and runs a generic prompt step. Use runQuickPickWizard to run quick pick steps
* @param context The action context
* @param wizardOptions The options used to construct the wizard
*/
export declare function runGenericPromptStep(context: PickExperienceContext, wizardOptions: IWizardOptions<AzureResourceQuickPickWizardContext>): Promise<void>;

/**
* Registers a namespace for common random utility functions
*/
Expand Down
33 changes: 27 additions & 6 deletions utils/src/copilot/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import * as vscode from "vscode";
import { InvalidCopilotResponseError } from "../errors";

const languageModelPreference: { vendor: "copilot", family: string }[] = [
// not yet seen/available
// { vendor: "copilot", family: "gpt-4-turbo-preview" },
// seen/available
{ vendor: "copilot", family: "gpt-5-mini" },
{ vendor: "copilot", family: "claude-sonnet-4" },
{ vendor: "copilot", family: "claude-sonnet-4.5" },
{ vendor: "copilot", family: "gpt-4o" },
{ vendor: "copilot", family: "gpt-4-turbo" },
{ vendor: "copilot", family: "gpt-4" },
Expand All @@ -23,12 +23,21 @@ async function selectMostPreferredLm(): Promise<vscode.LanguageModelChat | undef
return lmsInPreferredOrder?.at(0);
}

export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], relevantContext?: string): string {
export function createPrimaryPromptToGetSingleQuickPickInput(picks: string[], placeholder?: string, relevantContext?: string): string {
const subscriptionId = relevantContext ? extractSubscriptionIdFromContext(relevantContext) : undefined;
const activityChildren = extractActivityChildren(relevantContext || '');
return `The User is asking you to choose an item based on the following information:
1. Choose from this list of picks ${picks.join(", ")}.
2. You must choose one item from the list
3. Use information from this context, if available, to make your decision: ${relevantContext ? relevantContext : "No context available"}
4. If there is an option to skipForNow, you can choose that option.
3. The placeholder contains the question being asked use this to help guide your input: ${placeholder ? placeholder : "No placeholder available"}
4. Use information from this context, if available, to make your decision: ${relevantContext ? relevantContext : "No context available"}
5. If there are activity children you must use them to them to inform your decision: ${activityChildren ? activityChildren : "No activity children available"}
Here are some examples on how to use activity children to inform your decision:
- If the placeholder is 'Select a container app' and there is an activity child 'Use container app: myContainerApp', you should choose the pick with the label 'myContainerApp' from the picks.
- If the placeholder is 'Select a container apps environment' and there is an activity child 'Use managed environment: myEnv', you should choose the pick with the label'myEnv' from the picks.
6. If there is an option to skipForNow, you can choose that option.
7. If the placeholder is 'Select subscription' use the subscription id to help guide your input. Use the subscription id: ${subscriptionId}
and match to the data portion of the picks which will have the id in the form of accounts/{accountId}/tenants/{tenantId}/subscriptions/{subscriptionID}. Match the {subscriptionId} portions to inform your decision.
Respond with a JSON object of the pick you have chosen. Do not respond in a conversational tone, only JSON. `;
}

Expand Down Expand Up @@ -94,3 +103,15 @@ export async function doCopilotInteraction(primaryPrompt: string): Promise<strin
function extractJsonString(raw: string): string {
return raw.replace(/```json\s*|```/g, '').trim();
}

function extractSubscriptionIdFromContext(context: string): string | undefined {
const regex = /https:\/\/management\.azure\.com\/subscriptions\/([0-9a-fA-F-]{36})/;
const match = context.match(regex);
return match ? match[1] : undefined;
}

function extractActivityChildren(context: string): string {
const activityLog = JSON.parse(context) as { children?: unknown };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably try/catch this just in case the shape is corrupted.

const children = activityLog.children;
return JSON.stringify(children);
}
11 changes: 11 additions & 0 deletions utils/src/executeCommandWithAddedContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from "vscode";
import * as types from '../index';

export function executeCommandWithAddedContext<T = unknown>(commandId: string, additionalContext: Partial<types.IActionContext>, ...args: unknown[]): Thenable<T> {
const metadata = { __injectedContext: additionalContext };

Check warning on line 9 in utils/src/executeCommandWithAddedContext.ts

View workflow job for this annotation

GitHub Actions / Build (utils) / Build

Object Literal Property name `__injectedContext` must match one of the following formats: camelCase, PascalCase
return vscode.commands.executeCommand(commandId, ...args, metadata);
}
2 changes: 2 additions & 0 deletions utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './dev/TestOutputChannel';
export * from './dev/TestUserInput';
export * from './DialogResponses';
export * from './errors';
export * from './executeCommandWithAddedContext';
export * from './extensionUserAgent';
export { registerUIExtensionVariables } from './extensionVariables';
export { addExtensionValueToMask, callWithMaskHandling, maskUserInfo, maskValue } from './masking';
Expand All @@ -31,6 +32,7 @@ export * from './pickTreeItem/GenericQuickPickStep';
export * from './pickTreeItem/quickPickAzureResource/QuickPickAzureResourceStep';
export * from './pickTreeItem/quickPickAzureResource/QuickPickAzureSubscriptionStep';
export * from './pickTreeItem/quickPickAzureResource/QuickPickGroupStep';
export * from './pickTreeItem/runGenericPromptStep';
export * from './pickTreeItem/runQuickPickWizard';
export * from './registerCommand';
export * from './registerCommandWithTreeNodeUnwrapping';
Expand Down
17 changes: 17 additions & 0 deletions utils/src/pickTreeItem/runGenericPromptStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as types from '../../index';
import { AzureWizard } from '../wizard/AzureWizard';

export async function runGenericPromptStep(context: types.PickExperienceContext, wizardOptions?: types.IWizardOptions<types.AzureResourceQuickPickWizardContext>): Promise<void> {
const wizard = new AzureWizard(context, {
hideStepCount: true,
showLoadingPrompt: wizardOptions?.showLoadingPrompt ?? true,
...wizardOptions
});

await wizard.prompt();
}
15 changes: 13 additions & 2 deletions utils/src/registerCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,18 @@ export function registerCommand(commandId: string, callback: (context: types.IAc
return await callWithTelemetryAndErrorHandling(
telemetryId || commandId,
async (context: types.IActionContext) => {
let injectedContext: Partial<types.IActionContext> | undefined;
if (args.length > 0) {
const metadata = args[args.length - 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just do a .find on the metadata instead of assuming that it'll always be at the end if the arg list? I think that'll make this command a little more resilient.


// Look for our metadata object at the end
if (metadata && typeof metadata === "object" && "__injectedContext" in metadata) {
injectedContext = metadata.__injectedContext as types.IActionContext;

// remove only the metadata
args.pop();
}

try {
await setTelemetryProperties(context, args);
} catch (e: unknown) {
Expand All @@ -52,8 +63,8 @@ export function registerCommand(commandId: string, callback: (context: types.IAc
context.telemetry.properties.telemetryError = error.message;
}
}

return callback(context, ...args);
const finalContext = injectedContext ? { ...context, ...injectedContext } : context;
return callback(finalContext, ...args);
}
);
}));
Expand Down
21 changes: 17 additions & 4 deletions utils/src/userInput/CopilotUserInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,18 @@ export class CopilotUserInput implements types.IAzureUserInput {
public async showQuickPick<T extends types.IAzureQuickPickItem<unknown>>(items: T[] | Thenable<T[]>, options: vscodeTypes.QuickPickOptions): Promise<T | T[]> {
let primaryPrompt: string;
const resolvedItems: T[] = await Promise.resolve(items);
const jsonItems: string[] = resolvedItems.map(item => JSON.stringify(item));

// Clean up items to only include label and description
const cleanedItems = this.cleanQuickPickItems(resolvedItems);
const jsonItems = cleanedItems.map(item => JSON.stringify(item));
try {
if (options.canPickMany) {
primaryPrompt = createPrimaryPromptToGetPickManyQuickPickInput(jsonItems, this._relevantContext);
const response = await doCopilotInteraction(primaryPrompt);
const jsonResponse: T[] = JSON.parse(response) as T[];
const picks = resolvedItems.filter(item => {
return jsonResponse.some(resp => JSON.stringify(resp) === JSON.stringify(item));
return jsonResponse.some(resp => JSON.stringify(resp.label) === JSON.stringify(item.label) &&
JSON.stringify(resp.description || '') === JSON.stringify(item.description || ''));
});

if (!picks || picks.length === 0) {
Expand All @@ -109,11 +113,12 @@ export class CopilotUserInput implements types.IAzureUserInput {

return picks;
} else {
primaryPrompt = createPrimaryPromptToGetSingleQuickPickInput(jsonItems, this._relevantContext);
primaryPrompt = createPrimaryPromptToGetSingleQuickPickInput(jsonItems, options.placeHolder, this._relevantContext);
const response = await doCopilotInteraction(primaryPrompt);
const jsonResponse: T = JSON.parse(response) as T;
const pick = resolvedItems.find(item => {
return JSON.stringify(item) === JSON.stringify(jsonResponse);
return JSON.stringify(item.label) === JSON.stringify(jsonResponse.label) &&
JSON.stringify(item.description || '') === JSON.stringify(jsonResponse.description || '');
});

if (!pick) {
Expand All @@ -128,4 +133,12 @@ export class CopilotUserInput implements types.IAzureUserInput {
throw new InvalidCopilotResponseError();
}
}

private cleanQuickPickItems<T extends types.IAzureQuickPickItem<unknown>>(items: T[]): { label: string, description: string, id: string | undefined }[] {
return items.map(item => ({
label: item.label,
description: item.description || '',
id: (item.data && typeof item.data === 'object' && 'id' in item.data) ? String(item.data.id) : undefined
}));
}
}
Loading