Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ export class DeploymentPlanViewController extends WebviewController<DeploymentPl
case 'approve':
void this.approvePlan();
break;
case 'subscriptionChanged':
if (typeof message.data === 'string') {
this.planData.subscription = message.data;
}
break;
case 'locationChanged':
if (typeof message.data === 'string') {
this.planData.locationCode = message.data;
}
break;
case 'submitPlanFeedback': {
const query = message.prompt?.trim();
if (!query) {
Expand Down
43 changes: 41 additions & 2 deletions src/webviews/copilotOnRails/extension/openDeploymentPlanView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { LocationListStep } from "@microsoft/vscode-azext-azureutils";
import { callWithTelemetryAndErrorHandling, createSubscriptionContext, IActionContext, ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import * as vscode from "vscode";
import { ext } from "../../../extensionVariables";
import { CopilotOnRailsContext } from "../../../utils/copilotOnRails/CopilotOnRailsContext";
import { DEPLOYMENT_PLAN_FILE_GLOB } from "../../../tree/project/projectPlanFiles";
import { CopilotOnRailsContext } from "../../../utils/copilotOnRails/CopilotOnRailsContext";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Any idea why this got moved? I don't see any obvious difference

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Probably just formatting differences I'll move it back

import type { DeploymentPlanData } from "../views/utils/deploymentPlanTypes";
import { getDeploymentPlanRenderIssue, parseDeploymentPlanMarkdown } from "../views/utils/parseDeploymentPlanMarkdown";
import { DeploymentPlanViewController } from "./controllers/DeploymentPlanViewController";
Expand Down Expand Up @@ -35,10 +37,25 @@ export function openDeploymentPlanViewWithContent(content: string, sourceFileUri

async function openDeploymentPlanViewWithContentAsync(content: string, sourceFileUri?: vscode.Uri): Promise<void> {
const planData = tryParseDeploymentPlan(content, sourceFileUri);
const liveSubscriptions = await getAvailableAzureSubscriptions();
const [liveSubscriptions, liveLocations] = await Promise.all([
getAvailableAzureSubscriptions(),
getAvailableAzureLocations(),
]);
if (liveSubscriptions) {
planData.availableSubscriptions = liveSubscriptions;
}
if (liveLocations) {
planData.availableLocations = liveLocations;
// Resolve the location code from the display name when the plan omitted it.
if (!planData.locationCode && planData.location) {
const needle = planData.location.toLowerCase();
const matched = liveLocations.find(l => l.name.toLowerCase() === needle || l.code.toLowerCase() === needle);
if (matched) {
planData.locationCode = matched.code;
planData.location = matched.name;
}
}
}

host.show(planData, sourceFileUri);
}
Expand All @@ -56,6 +73,28 @@ async function getAvailableAzureSubscriptions(): Promise<string[] | undefined> {
}
}

async function getAvailableAzureLocations(): Promise<{ name: string; code: string }[] | undefined> {
return await callWithTelemetryAndErrorHandling('copilotOnRails.deploymentPlan.getLocations', async (context: IActionContext) => {
context.errorHandling.rethrow = false;
context.telemetry.suppressIfSuccessful = true;

const provider = await ext.subscriptionProviderFactory();
const subscriptions = await provider.getAvailableSubscriptions({ filter: false });
if (subscriptions.length === 0) {
return undefined;
}

const wizardContext: ISubscriptionActionContext = { ...context, ...createSubscriptionContext(subscriptions[0]) };
const locations = await LocationListStep.getLocations(wizardContext);
const mapped = locations
.map(l => ({ name: l.displayName ?? l.name, code: l.name }))
.filter((l): l is { name: string; code: string } => Boolean(l.name && l.code));
const unique = Array.from(new Map(mapped.map(l => [l.code, l])).values())
.sort((a, b) => a.name.localeCompare(b.name));
return unique.length > 0 ? unique : undefined;
});
}

function tryParseDeploymentPlan(content: string, sourceFileUri: vscode.Uri | undefined): DeploymentPlanData {
let parsed: DeploymentPlanData | undefined;
let errorMessage: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export function parseDeploymentPlanMarkdown(markdown: string): DeploymentPlanDat

const status = extractMetadata(lines, 'Status') ?? 'Unknown';
const mode = extractMetadata(lines, 'Mode') ?? 'Unknown';
const subscription = extractMetadata(lines, 'Subscription') ?? findAttribute(requirements, 'Subscription') ?? 'Unknown';
const rawSubscription = extractMetadata(lines, 'Subscription') ?? findAttribute(requirements, 'Subscription') ?? 'Unknown';
const subscription = stripAnnotation(rawSubscription);
const rawLocation = extractMetadata(lines, 'Location') ?? findAttribute(requirements, 'Location') ?? 'Unknown';

// Parse location: "East US (`eastus`)" -> name="East US", code="eastus"
Expand Down Expand Up @@ -74,26 +75,11 @@ export function parseDeploymentPlanMarkdown(markdown: string): DeploymentPlanDat
? ['Visual Studio Enterprise', 'Azure for Students', 'Pay-As-You-Go', 'MSDN Platforms']
: undefined;

const knownLocations = [
{ name: 'East US', code: 'eastus' },
{ name: 'East US 2', code: 'eastus2' },
{ name: 'West US', code: 'westus' },
{ name: 'West US 2', code: 'westus2' },
{ name: 'Central US', code: 'centralus' },
{ name: 'North Europe', code: 'northeurope' },
{ name: 'West Europe', code: 'westeurope' },
{ name: 'Southeast Asia', code: 'southeastasia' },
];

let resolvedLocationCode = locationCode;
let resolvedLocation = location;
if (resolvedLocationCode === 'unknown' && location !== 'Unknown') {
const needle = location.toLowerCase();
const matched = knownLocations.find(l => l.name.toLowerCase() === needle || l.code.toLowerCase() === needle);
if (matched) {
resolvedLocationCode = matched.code;
resolvedLocation = matched.name;
}
const resolvedLocation = location;
if (resolvedLocationCode === 'unknown' && location !== 'Unknown' && /^[a-z][a-z0-9]+$/.test(location.toLowerCase())) {
// The location was authored as a bare Azure region code (e.g. `eastus`).
resolvedLocationCode = location.toLowerCase();
}

return {
Expand All @@ -103,7 +89,6 @@ export function parseDeploymentPlanMarkdown(markdown: string): DeploymentPlanDat
availableSubscriptions,
location: resolvedLocation === 'Unknown' ? '' : resolvedLocation,
locationCode: resolvedLocationCode === 'unknown' ? '' : resolvedLocationCode,
availableLocations: knownLocations,
architecture,
workspaceScan: workspaceCandidate?.table ?? emptyTable(),
decisions: decisionsCandidate?.table ?? emptyTable(),
Expand Down Expand Up @@ -288,6 +273,11 @@ function findAttribute(table: DeploymentPlanTable, attribute: string): string |
return row?.[1]?.trim();
}

/** Strips trailing LLM-generated annotations (e.g. "⚠️ ...note...") from a metadata value. */
function stripAnnotation(value: string): string {
return value.replace(/\s*[\u26A0\u2705\u274C\u2139\u{1F4A1}\u{1F6A8}]\uFE0F?\s.*/su, '').trim() || value;
}

function isWorkspaceTable(table: DeploymentPlanTable): boolean {
const headers = normalizedHeaders(table);
return headers.includes('component')
Expand Down
Loading