diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 3441ca67ee4..ac2e26fc74c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "maps" "net/http" @@ -87,11 +88,12 @@ type InitAction struct { flags *initFlags models *modelSelector - deploymentDetails []project.Deployment - containerSettings *project.ContainerSettings - isCodeDeploy bool // true when user selects code deploy mode; skips ACR config - httpClient *http.Client - serviceNameOverride string // when set, addToProject uses this instead of the manifest name + deploymentDetails []project.Deployment + containerSettings *project.ContainerSettings + isCodeDeploy bool // true when user selects code deploy mode; skips ACR config + httpClient *http.Client + serviceNameOverride string // when set, addToProject uses this instead of the manifest name + createdFolderDisplay string // pre-computed relative display path for the created folder } // modelSelector encapsulates the dependencies needed for model selection and @@ -304,6 +306,13 @@ func setAgentNameOnTemplate(agentManifest *agent_yaml.AgentManifest, agentName s return nil } +func folderNameStrippingParenSuffix(title string) string { + if idx := strings.IndexByte(title, '('); idx >= 0 { + title = strings.TrimSpace(title[:idx]) + } + return sanitizeAgentName(title) +} + func updateAgentDefinition( template any, update func(*agent_yaml.AgentDefinition), @@ -530,9 +539,11 @@ func runInitFromManifest( flags *initFlags, azdClient *azdext.AzdClient, httpClient *http.Client, + targetDir string, + createdFolderDisplay string, ) error { // Ensure project and environment exist (no subscription/location prompting yet) - projectConfig, err := ensureProject(ctx, flags, azdClient) + projectConfig, err := ensureProject(ctx, flags, azdClient, targetDir) if err != nil { return err } @@ -582,14 +593,15 @@ func runInitFromManifest( ) action := &InitAction{ - azdClient: azdClient, - azureContext: azureContext, - console: console, - credential: credential, - projectConfig: projectConfig, - environment: env, - flags: flags, - httpClient: httpClient, + azdClient: azdClient, + azureContext: azureContext, + console: console, + credential: credential, + projectConfig: projectConfig, + environment: env, + flags: flags, + httpClient: httpClient, + createdFolderDisplay: createdFolderDisplay, } return action.Run(ctx) @@ -669,6 +681,11 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, Timeout: 30 * time.Second, } + // Track whether a project already exists so the cd hint is + // only shown for brand-new top-level project folders, not + // when a template adds a subfolder to an existing project. + existingProject := fileExists("azure.yaml") + // Auto-detect an existing agent manifest in the target directory // when no --manifest flag was provided. // @@ -763,7 +780,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, return err } - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest(ctx, flags, azdClient, httpClient, ".", ""); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -793,17 +810,24 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, switch selectedTemplate.EffectiveType() { case TemplateTypeAzd: // Full azd template - dispatch azd init -t - initArgs := []string{"init", "-t", selectedTemplate.Source, "."} + // Create project in a new subdirectory derived from the template title. + folderName := folderNameStrippingParenSuffix(selectedTemplate.Title) + // Check whether the target directory already exists so we + // only report "created" when a new directory was made. + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) + initArgs := []string{"init", "-t", selectedTemplate.Source, folderName} if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - cwd, err := os.Getwd() - if err == nil { - sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd)) - initArgs = append( - initArgs, "--environment", sanitizedDirectoryName+"-dev", - ) + base := sanitizeAgentName(folderName) + if len(base) > 59 { + base = strings.TrimRight(base[:59], "-") } + defaultEnvName := base + "-dev" + initArgs = append( + initArgs, "--environment", defaultEnvName, + ) } workflow := &azdext.Workflow{ @@ -834,6 +858,22 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, selectedTemplate.Title, ) + // Sync the extension process into the new project directory. + // The azd host already chdir'd when it processed the init command. + if err := os.Chdir(folderName); err != nil { + return fmt.Errorf( + "changing to project directory %q: %w", + folderName, err, + ) + } + // Compute display path for created folder (used in nextstep). + // Only show cd hint for brand-new projects, not when adding + // a template subfolder to an existing project. + var folderDisplay string + if newlyCreated && !existingProject { + folderDisplay = filepath.ToSlash(folderName) + } + // Search for an agent manifest in the scaffolded project cwd, err := os.Getwd() if err != nil { @@ -847,7 +887,9 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, if manifestPath != "" { flags.manifestPointer = manifestPath - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest( + ctx, flags, azdClient, httpClient, ".", folderDisplay, + ); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -858,9 +900,21 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, } default: - // Agent manifest template - use existing -m flow + // Agent manifest template - use existing -m flow. + // Create project in a new subdirectory derived from the template title. + folderName := folderNameStrippingParenSuffix(selectedTemplate.Title) + // Check whether the target directory already exists so we + // only report "created" when a new directory was made. + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) + var folderDisplay string + if newlyCreated && !existingProject { + folderDisplay = filepath.ToSlash(folderName) + } flags.manifestPointer = selectedTemplate.Source - if err := runInitFromManifest(ctx, flags, azdClient, httpClient); err != nil { + if err := runInitFromManifest( + ctx, flags, azdClient, httpClient, folderName, folderDisplay, + ); err != nil { if exterrors.IsCancellation(err) { return exterrors.Cancelled("initialization was cancelled") } @@ -1093,21 +1147,37 @@ func (a *InitAction) Run(ctx context.Context) error { return nil } -func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdClient) (*azdext.ProjectConfig, error) { +func ensureProject( + ctx context.Context, + flags *initFlags, + azdClient *azdext.AzdClient, + targetDir string, +) (*azdext.ProjectConfig, error) { projectResponse, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { fmt.Println("Let's get your project initialized.") // Environment creation is handled separately in ensureEnvironment - initArgs := []string{"init", "-t", "Azure-Samples/azd-ai-starter-basic", "."} + initArgs := []string{ + "init", "-t", "Azure-Samples/azd-ai-starter-basic", targetDir, + } if flags.env != "" { initArgs = append(initArgs, "--environment", flags.env) } else { - cwd, err := os.Getwd() - if err == nil { - sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd)) - initArgs = append(initArgs, "--environment", sanitizedDirectoryName+"-dev") + // Derive environment name from target folder + envBase := targetDir + if targetDir == "." { + cwd, cwdErr := os.Getwd() + if cwdErr == nil { + envBase = filepath.Base(cwd) + } + } + base := sanitizeAgentName(envBase) + if len(base) > 59 { + base = strings.TrimRight(base[:59], "-") } + envName := base + "-dev" + initArgs = append(initArgs, "--environment", envName) } // We don't have a project yet @@ -1134,6 +1204,17 @@ func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdC ) } + // Sync the extension process into the new project directory so that + // subsequent local file operations see the scaffolded project. + if targetDir != "." { + if chdirErr := os.Chdir(targetDir); chdirErr != nil { + return nil, fmt.Errorf( + "changing to project directory %q: %w", + targetDir, chdirErr, + ) + } + } + projectResponse, err = azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { return nil, exterrors.Dependency( @@ -2208,7 +2289,11 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa // everything is configured. All paths append the deploy hint as the // trailing line. State-assembly errors are intentionally ignored: the // resolver degrades gracefully on partial state per the design spec. - state, _ := nextstep.AssembleState(ctx, a.azdClient) + var stateOpts []nextstep.Option + if a.createdFolderDisplay != "" { + stateOpts = append(stateOpts, nextstep.WithCreatedFolder(a.createdFolderDisplay)) + } + state, _ := nextstep.AssembleState(ctx, a.azdClient, stateOpts...) _ = printAllNextIfTerminal(os.Stdout, nextstep.ResolveAfterInit(state)) return nil } @@ -3116,3 +3201,21 @@ func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint, dep } return nil } + +// formatCreatedFolderMessage builds the user-facing message shown after a new +// project folder is created. It computes a cross-platform relative display path +// and optionally notes the original template title when the folder name differs. +func formatCreatedFolderMessage(originalCwd, createdFolder, createdFromTitle string) string { + displayPath := createdFolder + if relPath, err := filepath.Rel(originalCwd, createdFolder); err == nil { + displayPath = filepath.ToSlash(relPath) + } + + msg := fmt.Sprintf("\nYour project has been created in %s", displayPath) + if createdFromTitle != "" && filepath.Base(createdFolder) != createdFromTitle { + msg += fmt.Sprintf(" (from template %q)", createdFromTitle) + } + msg += fmt.Sprintf("\n cd %s\n", displayPath) + + return msg +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go index 1694f25f0c7..d528889d09c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_reuse.go @@ -85,7 +85,7 @@ func runReuseDefinition( displayPath, def.Name, )) - projectConfig, err := ensureProject(ctx, flags, azdClient) + projectConfig, err := ensureProject(ctx, flags, azdClient, ".") if err != nil { return err } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go index 84e6d151669..14629b0028c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go @@ -100,6 +100,16 @@ func TestSanitizeAgentName(t *testing.T) { input: "---", expected: "my-agent", }, + { + name: "non-ASCII characters stripped", + input: "Ünö Ägent", + expected: "n-gent", + }, + { + name: "all non-ASCII falls back to default", + input: "日本語エージェント", + expected: "my-agent", + }, } for _, tt := range tests { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index ee9a619f43a..273dbfad3da 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -6,6 +6,7 @@ package cmd import ( "context" "errors" + "io/fs" "net/http" "os" "path/filepath" @@ -2335,3 +2336,196 @@ func TestCodeDeployFlagValidation(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// createdFolder path computation after Chdir +// (covers PR review — directory creation tracking and message accuracy) +// --------------------------------------------------------------------------- + +// TestCreatedFolderPath_AfterChdir verifies that formatCreatedFolderMessage +// produces the correct relative display path even after the process has +// chdir'd into the new project directory. +func TestCreatedFolderPath_AfterChdir(t *testing.T) { + tests := []struct { + name string + folder string + wantPath string + }{ + { + name: "simple folder name", + folder: "my-agent", + wantPath: "my-agent", + }, + { + name: "sanitized folder name", + folder: folderNameStrippingParenSuffix("Hello World (Python)"), + wantPath: "hello-world", + }, + { + name: "folder with numbers", + folder: "agent-v2", + wantPath: "agent-v2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalCwd := t.TempDir() + + // Create the subdirectory (simulates azd init creating it) + folderPath := filepath.Join(originalCwd, tt.folder) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(folderPath, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + + // Simulate the chdir that happens after azd init + t.Chdir(folderPath) + + msg := formatCreatedFolderMessage(originalCwd, folderPath, "") + wantSuffix := "cd " + tt.wantPath + "\n" + if !strings.Contains(msg, tt.wantPath) { + t.Errorf("message missing display path %q:\n%s", tt.wantPath, msg) + } + if !strings.HasSuffix(msg, wantSuffix) { + t.Errorf("message should end with %q, got:\n%s", wantSuffix, msg) + } + }) + } +} + +// TestCreatedFolderPath_NotSetWhenDirectoryExists verifies that the +// newlyCreated check correctly identifies an existing directory. +func TestCreatedFolderPath_NotSetWhenDirectoryExists(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "existing-project" + + // Pre-create the directory (simulates an existing project) + existingDir := filepath.Join(originalCwd, folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(existingDir, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + + // Mirror production logic: stat + errors.Is + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) + + if newlyCreated { + t.Error("newlyCreated should be false when directory already exists") + } +} + +// TestCreatedFolderPath_SetWhenDirectoryDoesNotExist verifies that the +// newlyCreated check correctly identifies a missing directory. +func TestCreatedFolderPath_SetWhenDirectoryDoesNotExist(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "new-agent-project" + + // Do NOT create the directory — simulates fresh init + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) + + if !newlyCreated { + t.Error("newlyCreated should be true when directory does not exist") + } + + // Verify formatCreatedFolderMessage produces valid output + createdFolder := filepath.Join(originalCwd, folderName) + msg := formatCreatedFolderMessage(originalCwd, createdFolder, "") + if !strings.Contains(msg, folderName) { + t.Errorf("message should contain %q:\n%s", folderName, msg) + } +} + +// TestCreatedFolderPath_AzdTemplateCase verifies the full flow for the +// TemplateTypeAzd case: folderNameFromTitle derives the name, and the message +// includes a template-title notice when the name changed. +func TestCreatedFolderPath_AzdTemplateCase(t *testing.T) { + originalCwd := t.TempDir() + + templateTitle := "Basic Agent (Python)" + folderName := folderNameStrippingParenSuffix(templateTitle) + + // folderNameFromTitle should strip parenthetical suffix + if strings.Contains(folderName, "python") { + t.Errorf("folderName should not contain parenthetical suffix, got %q", folderName) + } + + createdFolder := filepath.Join(originalCwd, folderName) + msg := formatCreatedFolderMessage(originalCwd, createdFolder, templateTitle) + + // Should contain the template notice since name differs from title + if !strings.Contains(msg, templateTitle) { + t.Errorf("message should reference original title %q:\n%s", templateTitle, msg) + } + // Should contain the cd hint + if !strings.Contains(msg, "cd "+folderName) { + t.Errorf("message should contain cd hint:\n%s", msg) + } +} + +// TestCreatedFolderPath_ManifestTemplateExistingProject verifies that no +// "created" message is produced when an existing project is found for the +// agent manifest template flow. +func TestCreatedFolderPath_ManifestTemplateExistingProject(t *testing.T) { + originalCwd := t.TempDir() + t.Chdir(originalCwd) + + folderName := "my-agent" + + // Pre-create directory and azure.yaml to simulate existing project + projectDir := filepath.Join(originalCwd, folderName) + //nolint:gosec // test fixture directory permissions are intentional + if err := os.MkdirAll(projectDir, 0o755); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + //nolint:gosec // test fixture file permissions are intentional + if err := os.WriteFile( + filepath.Join(projectDir, "azure.yaml"), + []byte("name: my-agent\n"), + 0o644, + ); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + // Mirror production logic: directory exists, so newlyCreated is false + _, statErr := os.Stat(folderName) + newlyCreated := errors.Is(statErr, fs.ErrNotExist) + + if newlyCreated { + t.Error("newlyCreated should be false for existing project directory") + } +} + +func TestFolderNameFromTitle(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + title string + want string + }{ + {name: "strips parenthetical suffix", title: "Basic Agent (Python)", want: "basic-agent"}, + {name: "no parenthetical", title: "My Cool Agent", want: "my-cool-agent"}, + {name: "parenthetical with spaces", title: "Agent ( Preview )", want: "agent"}, + {name: "non-ASCII title", title: "Ünö Agent (Test)", want: "n-agent"}, + {name: "all non-ASCII before paren", title: "日本語 (Python)", want: "my-agent"}, + {name: "empty title", title: "", want: "my-agent"}, + {name: "only parenthetical", title: "(Python)", want: "my-agent"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := folderNameStrippingParenSuffix(tt.title) + if got != tt.want { + t.Errorf("folderNameFromTitle(%q) = %q, want %q", tt.title, got, tt.want) + } + }) + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go index e571094b12e..b7274b19812 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver.go @@ -93,6 +93,17 @@ func ResolveAfterInit(state *State) []Suggestion { out := make([]Suggestion, 0, 4) priority := 5 + // When init created a new project folder, the user's shell is still + // in the original directory. A leading `cd` suggestion lets them + // navigate before running any subsequent commands. + if state.CreatedFolderDisplay != "" { + out = append(out, Suggestion{ + Command: fmt.Sprintf("cd %s", state.CreatedFolderDisplay), + Description: "enter your new project folder", + Priority: 0, + }) + } + // Placeholder fix-ups always come first when present: they are broken // state in agent.yaml itself and block both `run` and `deploy`. The // user has to edit agent.yaml (or define a matching parameter in diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go index 41972ff6b21..5c15a6e23f5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/resolver_test.go @@ -181,6 +181,48 @@ func TestResolveAfterInit_NilState(t *testing.T) { assert.Nil(t, ResolveAfterInit(nil)) } +func TestResolveAfterInit_CreatedFolder(t *testing.T) { + t.Parallel() + + t.Run("cd suggestion prepended when folder was created", func(t *testing.T) { + t.Parallel() + state := &State{ + HasProjectEndpoint: true, + CreatedFolderDisplay: "my-agent", + } + out := ResolveAfterInit(state) + require.NotEmpty(t, out) + assert.Equal(t, "cd my-agent", out[0].Command) + assert.Equal(t, "enter your new project folder", out[0].Description) + assert.Equal(t, 0, out[0].Priority, "cd suggestion should have highest priority") + // Next primary is run, trailing is deploy + assert.Contains(t, out[1].Command, "azd ai agent run") + assert.Equal(t, "azd deploy", out[len(out)-1].Command) + }) + + t.Run("no cd suggestion when no folder created", func(t *testing.T) { + t.Parallel() + state := &State{HasProjectEndpoint: true} + out := ResolveAfterInit(state) + require.NotEmpty(t, out) + for _, s := range out { + assert.False(t, strings.HasPrefix(s.Command, "cd "), + "should not contain cd suggestion, got %q", s.Command) + } + }) + + t.Run("cd suggestion before provision when infra missing", func(t *testing.T) { + t.Parallel() + state := &State{ + CreatedFolderDisplay: "hello-world", + } + out := ResolveAfterInit(state) + require.True(t, len(out) >= 2) + assert.Equal(t, "cd hello-world", out[0].Command) + assert.Equal(t, "azd provision", out[1].Command) + }) +} + // TestResolveAfterInit_ManualVarsSingleEmitsEnrichedShape locks the // single-missing-manual-var case end-to-end. Three asserts: the env-set // line has the enriched "referenced by agent.yaml but not set in azd diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go index 49a05eeb092..79e2c4f5590 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go @@ -159,6 +159,11 @@ type config struct { // `azd ai agent run` to surface a fresh sample without making the // on-disk cache the source of truth. openAPILiveFetch func(context.Context) ([]byte, error) + + // createdFolderDisplay is a pre-computed relative display path for + // the folder created during init (e.g., "my-agent"). Empty when + // init did not create a new directory. + createdFolderDisplay string } // WithOpenAPIProbe enables a cache-only OpenAPI lookup for (agentName, suffix). @@ -186,6 +191,14 @@ func WithLiveOpenAPIProbe(fetch func(context.Context) ([]byte, error)) Option { return func(c *config) { c.openAPILiveFetch = fetch } } +// WithCreatedFolder passes a pre-computed display path for the folder +// created during init (e.g., "my-agent"). The resolver prepends a +// `cd ` suggestion when this is non-empty. The caller is +// responsible for computing the relative/slash-normalized path. +func WithCreatedFolder(displayPath string) Option { + return func(c *config) { c.createdFolderDisplay = displayPath } +} + // AssembleState builds a State snapshot for the current azd environment. // // All probes are best-effort: transport or parse errors are collected @@ -222,6 +235,7 @@ func assembleState(ctx context.Context, src Source, opts ...Option) (*State, []e } state := &State{} + state.CreatedFolderDisplay = cfg.createdFolderDisplay var errs []error envName, err := src.CurrentEnvName(ctx) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go index 1f6ddacc69e..fb6262c56c2 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/types.go @@ -109,6 +109,12 @@ type State struct { // example. Empty when HasOpenAPI is false. OpenAPIPayload string + // CreatedFolderDisplay is a pre-computed, user-friendly relative path + // to the project folder created during init (e.g., "my-agent"). Empty + // when init did not create a new directory. The resolver uses it to + // prepend a `cd ` suggestion to the Next: block. + CreatedFolderDisplay string + // HasModels, HasToolboxes, HasConnections are aggregate flags // derived from each azure.ai.agent service's agent.manifest.yaml // (when present). They are true when at least one resource of the