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
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@
"category": "Azure",
"icon": "$(refresh)"
},
{
"command": "azureProject.refresh",
"title": "%azureResourceGroups.refresh%",
"category": "Azure",
"icon": "$(refresh)"
},
{
"command": "azureResourceGroups.refresh",
"title": "%azureResourceGroups.refresh%",
Expand Down Expand Up @@ -568,6 +574,11 @@
"when": "view == azureResourceGroups",
"group": "navigation@3"
},
{
"command": "azureProject.refresh",
"when": "view == azureProject",
"group": "navigation@1"
},
{
"command": "azureResourceGroups.askAgentAboutActivityLog",
"when": "view == azureActivityLog",
Expand Down
2 changes: 1 addition & 1 deletion resources/agents/azure-project-integrate/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Activate when the user (or the scaffold hand-off) wants to:

Requires a scaffolded project. Verify before starting:
- `.azure/integration-plan.md` exists (the scaffold hand-off artifact). **This is your primary brief.**
- `.azure/project-plan.md` exists (the original plan — source of truth for routes, services, entities). Its `Status:` should be `Awaiting Integration` — the signal that the scaffold built clean and integration is the next required step. (If it already reads `Integrated`, integration has run before — re-verify rather than redo blindly.)
- `.azure/project-plan.md` exists (the original plan — source of truth for routes, services, entities). Its `Status:` should be `Integrating` — the signal that the user approved the UI preview and integration is the next required step. (Legacy runs may still read `Awaiting Integration`; treat it the same. If it already reads `Integrated`, integration has run before — re-verify rather than redo blindly.)
- Production code builds (the scaffold's final gate passed).

> If `.azure/integration-plan.md` is missing: do **not** fail. Fall back to `.azure/project-plan.md` plus a workspace scan to reconstruct the same facts (backend run command, frontend folder, routes, DB type, mock-data files, shared-types location). But always look for the artifact first.
Expand Down
27 changes: 18 additions & 9 deletions src/commands/copilotOnRails/openChatWithAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as vscode from 'vscode';
import { projectSubmissionState } from '../../tree/project/projectSubmissionState';
import { openLoadingView } from '../../webviews/copilotOnRails/extension/openLoadingView';
import { recordAgentLaunch } from '../../webviews/copilotOnRails/extension/projectSession';
import { type LoadingViewConfiguration } from '../../webviews/copilotOnRails/views/utils/viewConfigTypes';
import { ensureAgentInstructions } from './agentInstructions';

Expand Down Expand Up @@ -34,6 +35,22 @@ export async function ensureCopilotChatReady(): Promise<boolean> {
return true;
}

/**
* A fresh session is started for each phase hand-off because agents communicate
* through the `.azure/*` plan files on disk, not chat history, so a clean session
* keeps each agent's context window focused on its own phase instead of
* accumulating the entire plan → scaffold → debug conversation.
*/
export async function launchAgentChat(agentName: string, query: string): Promise<void> {
await vscode.commands.executeCommand('workbench.action.chat.newChat');
await vscode.commands.executeCommand('workbench.action.chat.open', {
mode: agentName,
query,
});
// Record the phase we just launched so an interrupted run can be resumed.
await recordAgentLaunch(agentName);
}

export async function openChatWithAgent(agentName: string, prompt: string, loading?: LoadingViewConfiguration): Promise<void> {
if (!(await ensureCopilotChatReady())) {
return;
Expand All @@ -42,15 +59,7 @@ export async function openChatWithAgent(agentName: string, prompt: string, loadi
if (!(await ensureAgentInstructions(agentName))) {
return;
}
// Start a fresh chat session for each phase hand-off. Agents communicate through the
// `.azure/*.md` plan files on disk, not chat history, so a clean session keeps each
// agent's context window focused on its own phase instead of accumulating the entire
// plan → scaffold → debug conversation.
await vscode.commands.executeCommand('workbench.action.chat.newChat');
await vscode.commands.executeCommand('workbench.action.chat.open', {
mode: agentName,
query: prompt,
});
await launchAgentChat(agentName, prompt);

if (loading) {
projectSubmissionState.setPending(loading.stage);
Expand Down
3 changes: 3 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { openLocalPlanViewFromWorkspace } from '../webviews/copilotOnRails/exten
import { openRequirementsViewFromWorkspace } from '../webviews/copilotOnRails/extension/openRequirementsView';
import { openScaffoldNextStepsView } from '../webviews/copilotOnRails/extension/openScaffoldNextStepsView';
import { openPlanViewFromWorkspace } from '../webviews/copilotOnRails/extension/openScaffoldPlanView';
import { resumeProjectWithCopilot } from '../webviews/copilotOnRails/extension/resumeProjectWithCopilot';
import { logIn } from './accounts/logIn';
import { SelectSubscriptionOptions, selectSubscriptions } from './accounts/selectSubscriptions';
import { clearActivities } from './activities/clearActivities';
Expand Down Expand Up @@ -169,6 +170,8 @@ export function registerCommands(): void {
registerCommandWithTreeNodeUnwrapping<{ id?: string }>("azureResourceGroups.askAgentAboutResource", (context, node) => askAgentAboutResource(context, node));

registerCommand('azureResourceGroups.createProjectWithCopilot', createProjectWithCopilot);
registerCommand('azureResourceGroups.resumeProjectWithCopilot', resumeProjectWithCopilot);
registerCommand('azureProject.refresh', () => ext.actions.refreshProjectTree());
registerCommand('azureResourceGroups.openPlanView', openPlanViewFromWorkspace);
registerCommand('azureResourceGroups.openLocalPlanView', openLocalPlanViewFromWorkspace);
registerCommand('azureResourceGroups.openDeployPlanView', openDeploymentPlanViewFromWorkspace);
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ export const isEmptyWorkspaceContextKey = 'ms-azuretools.vscode-azureresourcegro
export const hasPendingProjectSubmissionContextKey = 'ms-azuretools.vscode-azureresourcegroups.hasPendingProjectSubmission';
export const canFocusContextValue = 'canFocus';

export const azureProjectPlanAgent = 'azure-project-plan';
export const azureProjectScaffoldAgent = 'azure-project-scaffold';
export const azureProjectIntegrateAgent = 'azure-project-integrate';
export const azureDebugPlanAgent = 'azure-debug-plan';
export const azureDebugGenerateAgent = 'azure-debug-generate';
export const azureDeployAgent = 'azure-deploy';
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import { createResourceClient } from './utils/azureClients';
import { disableAutopilot, registerAutopilot } from './webviews/copilotOnRails/extension/autopilot';
import { resumePendingCreateWithCopilot } from './webviews/copilotOnRails/extension/createProjectWithCopilot';
import { registerRequirementsAutoOpen } from './webviews/copilotOnRails/extension/openRequirementsView';
import { registerResumeAffordances } from './webviews/copilotOnRails/extension/resumeAffordances';
import { registerViewHostDisposal } from './webviews/copilotOnRails/extension/utils/singletonViewHost';

export async function activate(context: vscode.ExtensionContext, perfStats: { loadStartTime: number; loadEndTime: number }): Promise<apiUtils.AzureExtensionApiProvider> {
// the entry point for vscode.dev is this activate, not main.js, so we need to instantiate perfStats here
Expand All @@ -80,6 +82,7 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
await registerProjectPlanFilesContext(context, projectPlanFilesWatcher);
registerRequirementsAutoOpen(context);
registerAutopilot(context);
registerViewHostDisposal(context);

const refreshAzureTreeEmitter = new vscode.EventEmitter<void | TreeDataItem | TreeDataItem[] | null | undefined>();
context.subscriptions.push(refreshAzureTreeEmitter);
Expand Down Expand Up @@ -180,6 +183,9 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo

const azureProjectProgressTreeDataProvider = new AzureProjectProgressTreeDataProvider(context, projectPlanFilesWatcher);
context.subscriptions.push(vscode.window.registerTreeDataProvider('azureProject', azureProjectProgressTreeDataProvider));
ext.actions.refreshProjectTree = () => azureProjectProgressTreeDataProvider.refresh();

registerResumeAffordances(context, projectPlanFilesWatcher);

const tenantResourcesBranchDataItemCache = new BranchDataItemCache();
registerTenantTree(context, {
Expand Down
1 change: 1 addition & 0 deletions src/extensionVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export namespace extActions {
export let refreshFocusTree: (data?: TreeDataItem | TreeDataItem[] | null | undefined | void) => void;
export let refreshTenantTree: (data?: TreeDataItem | TreeDataItem[] | null | undefined | void) => void;
export let refreshActivityLogTree: (data?: TreeDataItem | TreeDataItem[] | null | undefined | void) => void;
export let refreshProjectTree: () => void;
}

/**
Expand Down
35 changes: 27 additions & 8 deletions src/tree/project/projectPlanFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,44 @@ import * as vscode from 'vscode';
export type ProjectStage = 0 | 1 | 2;

export interface ProjectPlanFiles {
hasRequirements: boolean;
hasProjectPlan: boolean;
hasLocalDevelopmentPlan: boolean;
hasDeploymentPlan: boolean;
/** True when any of the plan files exist. */
/** True when any project artifact exists (requirements or a plan file). */
hasAny: boolean;
/** The furthest stage reached. */
currentStage: ProjectStage;
}

/**
* Workspace-relative globs for each Copilot-on-Rails project artifact. These are
* the single source of truth for the `.azure/*` file locations; other modules
* import these constants instead of re-hard-coding the paths.
*/
/** The requirements file marks the very start of a project, before any plan. */
export const REQUIREMENTS_FILE_GLOB = '.azure/requirements.json';
export const PROJECT_PLAN_FILE_GLOB = '.azure/project-plan.md';
export const INTEGRATION_PLAN_FILE_GLOB = '.azure/integration-plan.md';
export const DEBUG_PLAN_FILE_GLOB = '.azure/vscode-debug-plan.md';
export const DEPLOYMENT_PLAN_FILE_GLOB = '.azure/deployment-plan.md';

const PLAN_FILE_GLOBS = [
'.azure/project-plan.md',
'.azure/vscode-debug-plan.md',
'.azure/deployment-plan.md',
PROJECT_PLAN_FILE_GLOB,
INTEGRATION_PLAN_FILE_GLOB,
DEBUG_PLAN_FILE_GLOB,
DEPLOYMENT_PLAN_FILE_GLOB,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the integration plan file left out intentionally?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nah, added

] as const;

/** All artifacts that indicate an in-progress project, for watching. */
const ALL_PROJECT_FILE_GLOBS = [REQUIREMENTS_FILE_GLOB, ...PLAN_FILE_GLOBS] as const;

export async function getProjectPlanFiles(): Promise<ProjectPlanFiles> {
const [projectPlanFiles, localDevelopmentPlanFiles, deploymentPlanFiles] = await Promise.all(
PLAN_FILE_GLOBS.map((glob) => vscode.workspace.findFiles(glob, undefined, 1)),
const [requirementsFiles, projectPlanFiles, localDevelopmentPlanFiles, deploymentPlanFiles] = await Promise.all(
ALL_PROJECT_FILE_GLOBS.map((glob) => vscode.workspace.findFiles(glob, undefined, 1)),
);

const hasRequirements = requirementsFiles.length > 0;
const hasProjectPlan = projectPlanFiles.length > 0;
const hasLocalDevelopmentPlan = localDevelopmentPlanFiles.length > 0;
const hasDeploymentPlan = deploymentPlanFiles.length > 0;
Expand All @@ -41,10 +59,11 @@ export async function getProjectPlanFiles(): Promise<ProjectPlanFiles> {
}

return {
hasRequirements,
hasProjectPlan,
hasLocalDevelopmentPlan,
hasDeploymentPlan,
hasAny: hasProjectPlan || hasLocalDevelopmentPlan || hasDeploymentPlan,
hasAny: hasRequirements || hasProjectPlan || hasLocalDevelopmentPlan || hasDeploymentPlan,
currentStage,
};
}
Expand All @@ -71,7 +90,7 @@ export class ProjectPlanFilesWatcher implements vscode.Disposable {
vscode.workspace.onDidRenameFiles(fire),
);

for (const glob of PLAN_FILE_GLOBS) {
for (const glob of ALL_PROJECT_FILE_GLOBS) {
const watcher = vscode.workspace.createFileSystemWatcher(glob);
watcher.onDidCreate(fire);
watcher.onDidDelete(fire);
Expand Down
6 changes: 2 additions & 4 deletions src/webviews/copilotOnRails/extension/autopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { AzExtFsExtra } from "@microsoft/vscode-azext-utils";
import * as vscode from "vscode";
import { DEBUG_PLAN_FILE_GLOB } from "../../../tree/project/projectPlanFiles";
import { settingUtils } from "../../../utils/settingUtils";

/**
Expand Down Expand Up @@ -32,9 +33,6 @@ const MAX_RUN_DURATION_MS = 2 * 60 * 60 * 1000; // 2 hours
/** Marker embedded in the chat query so agents can detect an autopilot run. */
export const AUTOPILOT_QUERY_MARKER = '[AUTOPILOT MODE]';

/** Glob for the local debug plan whose completion ends an autopilot run. */
export const DEBUG_PLAN_GLOB = '.azure/vscode-debug-plan.md';

/** globalState keys used to survive window reloads mid-run. */
const STATE_ACTIVE = 'azureResourceGroups.autopilot.active';
const STATE_PRIOR_VALUE = 'azureResourceGroups.autopilot.priorAutoApprove';
Expand Down Expand Up @@ -135,7 +133,7 @@ export function isDebugPlanImplemented(content: string): boolean {

function registerCompletionWatcher(): void {
disposeCompletionWatcher();
completionWatcher = vscode.workspace.createFileSystemWatcher(DEBUG_PLAN_GLOB);
completionWatcher = vscode.workspace.createFileSystemWatcher(DEBUG_PLAN_FILE_GLOB);
const check = async (uri: vscode.Uri): Promise<void> => {
let content: string;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { WebviewController } from "@microsoft/vscode-azext-webview";
import * as vscode from "vscode";
import { ViewColumn } from "vscode";
import { ensureAgentInstructions } from "../../../../commands/copilotOnRails/agentInstructions";
import { ensureCopilotChatReady } from "../../../../commands/copilotOnRails/openChatWithAgent";
import { ensureCopilotChatReady, launchAgentChat } from "../../../../commands/copilotOnRails/openChatWithAgent";
import { azureProjectPlanAgent } from "../../../../constants";
import { ext } from "../../../../extensionVariables";
import { projectSubmissionState } from "../../../../tree/project/projectSubmissionState";
import { type CreateProjectViewControllerType } from "../../views/utils/viewConfigTypes";
Expand Down Expand Up @@ -43,11 +44,7 @@ export class CreateProjectViewController extends WebviewController<CreateProject
return;
}
this.panel.dispose();
await vscode.commands.executeCommand('workbench.action.chat.newChat');
await vscode.commands.executeCommand("workbench.action.chat.open", {
mode: 'azure-project-plan',
query,
});
await launchAgentChat(azureProjectPlanAgent, query);
projectSubmissionState.setPending();
openLoadingView({
stage: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import { WebviewController } from "@microsoft/vscode-azext-webview";
import * as vscode from "vscode";
import { ViewColumn } from "vscode";
import { ensureAgentInstructions } from "../../../../commands/copilotOnRails/agentInstructions";
import { azureProjectScaffoldAgent } from "../../../../constants";
import { ext } from "../../../../extensionVariables";
import { PROJECT_PLAN_FILE_GLOB } from "../../../../tree/project/projectPlanFiles";
import { getCopilotOnRailsBundleLocation } from "../copilotOnRailsBundleLocation";
import { type RunningDevServer, startDevServer } from "../utils/devServerManager";
import { writeProjectPlanStatus } from "../utils/planStatus";
import { ProjectPlanStatus } from "../../views/utils/projectPlanStatus";

/** State pushed to the webview to drive the preview surface. */
type PreviewState =
Expand Down Expand Up @@ -127,7 +131,7 @@ export class FrontendPreviewViewController extends WebviewController<Record<stri
// Keep the dev server running so the scaffold agent's edits hot-reload
// in the iframe while the user watches.
void vscode.commands.executeCommand('workbench.action.chat.open', {
mode: 'azure-project-scaffold',
mode: azureProjectScaffoldAgent,
query,
});
void this.panel.webview.postMessage({ command: 'feedbackSubmitted' });
Expand All @@ -137,6 +141,11 @@ export class FrontendPreviewViewController extends WebviewController<Record<stri
if (!(await ensureAgentInstructions('azure-project-integrate'))) {
return;
}
// Approving the final UX preview moves the plan into the integration
// phase, so flip the plan status before handing off. This is a
// deterministic UI signal, so record it in extension code rather than
// relying on the integrate agent to update it.
await writeProjectPlanStatus(PROJECT_PLAN_FILE_GLOB, ProjectPlanStatus.integrating);
// Stop the preview server before the integrate agent takes over so it
// can start its own runtime without port contention.
this.devServer?.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WebviewController } from "@microsoft/vscode-azext-webview";
import * as vscode from "vscode";
import { ViewColumn } from "vscode";
import { ensureCopilotChatReady } from "../../../../commands/copilotOnRails/openChatWithAgent";
import { azureDebugGenerateAgent } from "../../../../constants";
import { ext } from "../../../../extensionVariables";
import { type LocalDevNextStepsViewConfiguration } from "../../views/utils/viewConfigTypes";
import { getCopilotOnRailsBundleLocation } from "../copilotOnRailsBundleLocation";
Expand Down Expand Up @@ -51,7 +52,7 @@ export class LocalDevNextStepsViewController extends WebviewController<LocalDevN
}
this.panel.dispose();
await vscode.commands.executeCommand('workbench.action.chat.open', {
mode: 'azure-debug-generate',
mode: azureDebugGenerateAgent,
query: vscode.l10n.t('Run the API tests to verify my endpoints.'),
});
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ext } from "../../../../extensionVariables";
import { type LocalPlanData } from "../../views/utils/parseLocalPlanMarkdown";
import { getCopilotOnRailsBundleLocation } from "../copilotOnRailsBundleLocation";
import { openLoadingView } from "../openLoadingView";
import { suppressTrackedViewCloseOnce } from "../projectSession";
import { openSourceFileOrWarn } from "../utils/singletonViewHost";

export class LocalPlanViewController extends WebviewController<Record<string, never>> {
Expand Down Expand Up @@ -54,6 +55,8 @@ export class LocalPlanViewController extends WebviewController<Record<string, ne
if (!(await this.openDebugPlanChat('I approve the debug setup plan.', false))) {
return;
}
// Programmatic hand-off to the local-dev setup phase — don't treat this close as the user abandoning the flow.
suppressTrackedViewCloseOnce();
this.panel.dispose();
openLoadingView({
stage: 1,
Expand Down
Loading
Loading