fix(wizard): Use configured Auto Run folder path instead of default

The /wizard command now respects the user's configured Auto Run folder
path instead of always creating "Auto Run Docs" at the repository root.

Fixes #169
This commit is contained in:
Pedram Amini
2026-01-10 08:08:29 -06:00
parent 7240a1cbf6
commit 35827c392f
3 changed files with 254 additions and 80 deletions

View File

@@ -108,7 +108,12 @@ describe('useInlineWizard', () => {
describe('startWizard - intent parsing flow', () => {
describe('when no input is provided', () => {
it('should set mode to "ask" when existing docs exist', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1', 'phase-2'],
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
@@ -118,11 +123,16 @@ describe('useInlineWizard', () => {
expect(result.current.isWizardActive).toBe(true);
expect(result.current.wizardMode).toBe('ask');
expect(mockHasExistingAutoRunDocs).toHaveBeenCalledWith('/test/project');
expect(mockListDocs).toHaveBeenCalledWith('/test/project/Auto Run Docs');
});
it('should set mode to "new" when no existing docs', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(false);
// Mock listDocs to return empty files (no docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: [],
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
@@ -140,15 +150,19 @@ describe('useInlineWizard', () => {
await result.current.startWizard();
});
// Without a project path, hasExistingDocs defaults to false → new mode
// Without a project path, effectiveAutoRunFolderPath is null → no docs check → new mode
expect(result.current.wizardMode).toBe('new');
expect(mockHasExistingAutoRunDocs).not.toHaveBeenCalled();
});
});
describe('when input is provided', () => {
it('should call parseWizardIntent with input and hasExistingDocs', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1', 'phase-2'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'iterate', goal: 'add auth' });
const { result } = renderHook(() => useInlineWizard());
@@ -163,7 +177,12 @@ describe('useInlineWizard', () => {
});
it('should handle new mode from intent parser', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'new' });
const { result } = renderHook(() => useInlineWizard());
@@ -177,7 +196,12 @@ describe('useInlineWizard', () => {
});
it('should handle ask mode from intent parser', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'ask' });
const { result } = renderHook(() => useInlineWizard());
@@ -204,12 +228,12 @@ describe('useInlineWizard', () => {
describe('loading existing docs for iterate mode', () => {
it('should load existing docs when mode is iterate', async () => {
const mockDocs = [
{ name: 'phase-1', filename: 'phase-1.md', path: '/test/Auto Run Docs/phase-1.md' },
{ name: 'phase-2', filename: 'phase-2.md', path: '/test/Auto Run Docs/phase-2.md' },
];
mockHasExistingAutoRunDocs.mockResolvedValue(true);
mockGetExistingAutoRunDocs.mockResolvedValue(mockDocs);
// Mock listDocs to return files
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1', 'phase-2'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'iterate', goal: 'add feature' });
const { result } = renderHook(() => useInlineWizard());
@@ -218,12 +242,19 @@ describe('useInlineWizard', () => {
await result.current.startWizard('add new feature', undefined, '/test/project');
});
expect(mockGetExistingAutoRunDocs).toHaveBeenCalledWith('/test/project');
expect(result.current.existingDocuments).toEqual(mockDocs);
// The new implementation constructs existingDocs from listDocs result
expect(result.current.existingDocuments).toHaveLength(2);
expect(result.current.existingDocuments[0].name).toBe('phase-1');
expect(result.current.existingDocuments[1].name).toBe('phase-2');
});
it('should not load existing docs when mode is new', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'new' });
const { result } = renderHook(() => useInlineWizard());
@@ -232,12 +263,17 @@ describe('useInlineWizard', () => {
await result.current.startWizard('start fresh', undefined, '/test/project');
});
expect(mockGetExistingAutoRunDocs).not.toHaveBeenCalled();
// existingDocuments should be empty for new mode (docs only loaded for iterate)
expect(result.current.existingDocuments).toEqual([]);
});
it('should not load existing docs when mode is ask', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
// Mock listDocs to return files (existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1'],
});
window.maestro.autorun.listDocs = mockListDocs;
mockParseWizardIntent.mockReturnValue({ mode: 'ask' });
const { result } = renderHook(() => useInlineWizard());
@@ -246,13 +282,19 @@ describe('useInlineWizard', () => {
await result.current.startWizard('do something', undefined, '/test/project');
});
expect(mockGetExistingAutoRunDocs).not.toHaveBeenCalled();
// existingDocuments should be empty for ask mode
expect(result.current.existingDocuments).toEqual([]);
});
});
describe('isInitializing state', () => {
it('should set isInitializing to false after async operations complete', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(false);
// Mock listDocs to return empty (no existing docs)
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: [],
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
@@ -267,27 +309,34 @@ describe('useInlineWizard', () => {
});
describe('error handling', () => {
it('should handle errors from hasExistingAutoRunDocs', async () => {
mockHasExistingAutoRunDocs.mockRejectedValue(new Error('Failed to check docs'));
it('should silently handle listDocs errors and default to new mode', async () => {
// When listDocs fails, we treat it as no existing docs (folder doesn't exist)
const mockListDocs = vi.fn().mockRejectedValue(new Error('Folder not found'));
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await act(async () => {
await result.current.startWizard('test', undefined, '/test/project');
await result.current.startWizard(undefined, undefined, '/test/project');
});
expect(result.current.error).toBe('Failed to check docs');
expect(result.current.wizardMode).toBe('new'); // Fallback to new mode
// Error should NOT be set - we silently catch listDocs errors
expect(result.current.error).toBe(null);
expect(result.current.wizardMode).toBe('new'); // Default to new when can't check docs
expect(result.current.isInitializing).toBe(false);
consoleSpy.mockRestore();
});
it('should handle errors from getExistingAutoRunDocs', async () => {
mockHasExistingAutoRunDocs.mockResolvedValue(true);
it('should handle errors from loadDocumentContents in iterate mode', async () => {
// Setup: listDocs returns files, but loading content fails
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1', 'phase-2'],
});
const mockReadDoc = vi.fn().mockRejectedValue(new Error('Failed to read file'));
window.maestro.autorun.listDocs = mockListDocs;
window.maestro.autorun.readDoc = mockReadDoc;
mockParseWizardIntent.mockReturnValue({ mode: 'iterate', goal: 'add feature' });
mockGetExistingAutoRunDocs.mockRejectedValue(new Error('Failed to load docs'));
const { result } = renderHook(() => useInlineWizard());
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -296,26 +345,29 @@ describe('useInlineWizard', () => {
await result.current.startWizard('add feature', undefined, '/test/project');
});
expect(result.current.error).toBe('Failed to load docs');
expect(result.current.wizardMode).toBe('new'); // Fallback to new mode
// readDoc failure causes loadDocumentContents to fail
// This should set an error since it's a real loading failure
expect(result.current.isInitializing).toBe(false);
consoleSpy.mockRestore();
});
it('should handle non-Error exceptions', async () => {
mockHasExistingAutoRunDocs.mockRejectedValue('String error');
it('should treat empty listDocs response as no docs', async () => {
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: [],
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await act(async () => {
await result.current.startWizard('test', undefined, '/test/project');
await result.current.startWizard(undefined, undefined, '/test/project');
});
expect(result.current.error).toBe('Failed to initialize wizard');
consoleSpy.mockRestore();
// Empty files means no existing docs → new mode
expect(result.current.error).toBe(null);
expect(result.current.wizardMode).toBe('new');
});
});
@@ -361,6 +413,76 @@ describe('useInlineWizard', () => {
expect(result.current.state.projectPath).toBe(null);
});
});
describe('autoRunFolderPath parameter', () => {
it('should use configured autoRunFolderPath when provided', async () => {
const { result } = renderHook(() => useInlineWizard());
await act(async () => {
await result.current.startWizard(
'test',
undefined,
'/my/project',
'claude-code',
'Test Project',
'tab-1',
'session-1',
'/custom/auto-run/folder' // User-configured Auto Run folder
);
});
expect(result.current.state.autoRunFolderPath).toBe('/custom/auto-run/folder');
});
it('should fall back to default path when autoRunFolderPath not provided', async () => {
const { result } = renderHook(() => useInlineWizard());
await act(async () => {
await result.current.startWizard(
'test',
undefined,
'/my/project',
'claude-code',
'Test Project',
'tab-1',
'session-1'
// No autoRunFolderPath provided
);
});
// Should fall back to projectPath/Auto Run Docs
expect(result.current.state.autoRunFolderPath).toBe('/my/project/Auto Run Docs');
});
it('should check for existing docs in configured folder', async () => {
// Mock the direct listDocs call
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: ['phase-1', 'phase-2'],
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
await act(async () => {
await result.current.startWizard(
undefined, // No input, so it checks for existing docs
undefined,
'/my/project',
'claude-code',
'Test Project',
'tab-1',
'session-1',
'/custom/auto-run/folder'
);
});
// Should check the configured folder, not the default
expect(mockListDocs).toHaveBeenCalledWith('/custom/auto-run/folder');
// Should be 'ask' mode since docs exist
expect(result.current.wizardMode).toBe('ask');
});
});
});
describe('endWizard', () => {
@@ -563,8 +685,12 @@ describe('useInlineWizard', () => {
const mockStartConversation = vi.mocked(startInlineWizardConversation);
mockStartConversation.mockClear();
// Setup: no existing docs, so we get 'new' mode directly
mockHasExistingAutoRunDocs.mockResolvedValue(false);
// Setup: no existing docs via listDocs, so we get 'new' mode directly
const mockListDocs = vi.fn().mockResolvedValue({
success: true,
files: [], // Empty = no existing docs
});
window.maestro.autorun.listDocs = mockListDocs;
const { result } = renderHook(() => useInlineWizard());
@@ -712,15 +838,15 @@ describe('useInlineWizard', () => {
});
describe('generateDocuments', () => {
it('should return error when agent type is missing', async () => {
it('should return error when agent type or Auto Run folder path is missing', async () => {
const { result } = renderHook(() => useInlineWizard());
// Don't start wizard (no agentType or projectPath)
// Don't start wizard (no agentType or autoRunFolderPath)
await act(async () => {
await result.current.generateDocuments();
});
expect(result.current.error).toBe('Cannot generate documents: missing agent type or project path');
expect(result.current.error).toBe('Cannot generate documents: missing agent type or Auto Run folder path');
expect(result.current.isGeneratingDocs).toBe(false);
});

View File

@@ -3648,7 +3648,7 @@ You are taking over this conversation. Based on the context above, provide a bri
addToast({
type: 'warning',
title: 'Cannot Compact',
message: `Context too small. Need at least ${minContextUsagePercent}% usage or ~10k tokens to compact.`,
message: `Context too small. Need at least ${minContextUsagePercent}% usage, ~2k tokens, or 8+ messages to compact.`,
});
return;
}
@@ -4960,7 +4960,8 @@ You are taking over this conversation. Based on the context above, provide a bri
activeSession.toolType, // Agent type for AI conversation
activeSession.name, // Session/project name
activeTab.id, // Tab ID for per-tab isolation
activeSession.id // Session ID for playbook creation
activeSession.id, // Session ID for playbook creation
activeSession.autoRunFolderPath // User-configured Auto Run folder path (if set)
);
// Rename the tab to "Wizard" immediately when wizard starts
@@ -5037,7 +5038,8 @@ You are taking over this conversation. Based on the context above, provide a bri
activeSession.toolType,
activeSession.name,
newTab.id,
activeSession.id
activeSession.id,
activeSession.autoRunFolderPath // User-configured Auto Run folder path (if set)
);
// Show a system log entry

View File

@@ -141,6 +141,8 @@ export interface InlineWizardState {
subfolderName: string | null;
/** Full path to the subfolder where documents are saved (e.g., "/path/Auto Run Docs/Maestro-Marketing") */
subfolderPath: string | null;
/** User-configured Auto Run folder path (overrides default projectPath/Auto Run Docs) */
autoRunFolderPath: string | null;
}
/**
@@ -196,6 +198,7 @@ export interface UseInlineWizardReturn {
* @param sessionName - The session name (used as project name)
* @param tabId - The tab ID to associate the wizard with
* @param sessionId - The session ID for playbook creation
* @param autoRunFolderPath - User-configured Auto Run folder path (if set, overrides default projectPath/Auto Run Docs)
*/
startWizard: (
naturalLanguageInput?: string,
@@ -204,7 +207,8 @@ export interface UseInlineWizardReturn {
agentType?: ToolType,
sessionName?: string,
tabId?: string,
sessionId?: string
sessionId?: string,
autoRunFolderPath?: string
) => Promise<void>;
/** End the wizard and restore previous UI state */
endWizard: () => Promise<PreviousUIState | null>;
@@ -291,6 +295,7 @@ const initialState: InlineWizardState = {
agentSessionId: null,
subfolderName: null,
subfolderPath: null,
autoRunFolderPath: null,
};
/**
@@ -444,15 +449,28 @@ export function useInlineWizard(): UseInlineWizardReturn {
agentType?: ToolType,
sessionName?: string,
tabId?: string,
sessionId?: string
sessionId?: string,
configuredAutoRunFolderPath?: string
): Promise<void> => {
// Tab ID is required for per-tab wizard management
const effectiveTabId = tabId || 'default';
// Determine the Auto Run folder path to use:
// 1. If user has configured a specific path (configuredAutoRunFolderPath), use it
// 2. Otherwise, fall back to the default: projectPath/Auto Run Docs
const effectiveAutoRunFolderPath = configuredAutoRunFolderPath ||
(projectPath ? getAutoRunFolderPath(projectPath) : null);
logger.info(
`Starting inline wizard on tab ${effectiveTabId}`,
'[InlineWizard]',
{ projectPath, agentType, sessionName, hasInput: !!naturalLanguageInput }
{
projectPath,
agentType,
sessionName,
hasInput: !!naturalLanguageInput,
autoRunFolderPath: effectiveAutoRunFolderPath,
}
);
// Store current UI state for later restoration (per-tab)
@@ -491,13 +509,22 @@ export function useInlineWizard(): UseInlineWizardReturn {
agentSessionId: null,
subfolderName: null,
subfolderPath: null,
autoRunFolderPath: effectiveAutoRunFolderPath,
}));
try {
// Step 1: Check for existing Auto Run documents
const hasExistingDocs = projectPath
? await hasExistingAutoRunDocs(projectPath)
: false;
// Step 1: Check for existing Auto Run documents in the configured folder
// Use the effective Auto Run folder path (user-configured or default)
let hasExistingDocs = false;
if (effectiveAutoRunFolderPath) {
try {
const result = await window.maestro.autorun.listDocs(effectiveAutoRunFolderPath);
hasExistingDocs = result.success && result.files && result.files.length > 0;
} catch {
// Folder doesn't exist or can't be read - no existing docs
hasExistingDocs = false;
}
}
// Step 2: Determine mode based on input and existing docs
let mode: InlineWizardMode;
@@ -524,23 +551,33 @@ export function useInlineWizard(): UseInlineWizardReturn {
// Step 3: If iterate mode, load existing docs with content for context
let docsWithContent: ExistingDocumentWithContent[] = [];
if (mode === 'iterate' && projectPath) {
existingDocs = await getExistingAutoRunDocs(projectPath);
const autoRunFolderPath = getAutoRunFolderPath(projectPath);
docsWithContent = await loadDocumentContents(existingDocs, autoRunFolderPath);
if (mode === 'iterate' && effectiveAutoRunFolderPath) {
// List docs from the configured Auto Run folder
try {
const result = await window.maestro.autorun.listDocs(effectiveAutoRunFolderPath);
if (result.success && result.files) {
existingDocs = result.files.map((name: string) => ({
name,
filename: `${name}.md`,
path: `${effectiveAutoRunFolderPath}/${name}.md`,
}));
}
} catch {
existingDocs = [];
}
docsWithContent = await loadDocumentContents(existingDocs, effectiveAutoRunFolderPath);
}
// Step 4: Initialize conversation session (only for 'new' or 'iterate' modes)
if ((mode === 'new' || mode === 'iterate') && agentType && projectPath) {
const autoRunFolderPath = getAutoRunFolderPath(projectPath);
if ((mode === 'new' || mode === 'iterate') && agentType && effectiveAutoRunFolderPath) {
const session = startInlineWizardConversation({
mode,
agentType,
directoryPath: projectPath,
directoryPath: projectPath || effectiveAutoRunFolderPath,
projectName: sessionName || 'Project',
goal: goal || undefined,
existingDocs: docsWithContent.length > 0 ? docsWithContent : undefined,
autoRunFolderPath,
autoRunFolderPath: effectiveAutoRunFolderPath,
});
// Store conversation session per-tab
@@ -555,6 +592,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
mode,
goal: goal || null,
existingDocsCount: docsWithContent.length,
autoRunFolderPath: effectiveAutoRunFolderPath,
}
);
}
@@ -661,17 +699,20 @@ export function useInlineWizard(): UseInlineWizardReturn {
// If we're in 'ask' mode and don't have a session, auto-create one with 'new' mode
// This happens when user types directly instead of using the mode selection modal
const currentState = tabStatesRef.current.get(tabId);
if (currentState?.mode === 'ask' && currentState.agentType && currentState.projectPath) {
// Use stored autoRunFolderPath from state (configured by user or default)
const effectiveAutoRunFolderPath = currentState?.autoRunFolderPath ||
(currentState?.projectPath ? getAutoRunFolderPath(currentState.projectPath) : null);
if (currentState?.mode === 'ask' && currentState.agentType && effectiveAutoRunFolderPath) {
console.log('[useInlineWizard] Auto-creating session for direct message in ask mode');
const autoRunFolderPath = getAutoRunFolderPath(currentState.projectPath);
session = startInlineWizardConversation({
mode: 'new',
agentType: currentState.agentType,
directoryPath: currentState.projectPath,
directoryPath: currentState.projectPath || effectiveAutoRunFolderPath,
projectName: currentState.sessionName || 'Project',
goal: currentState.goal || undefined,
existingDocs: undefined,
autoRunFolderPath,
autoRunFolderPath: effectiveAutoRunFolderPath,
});
conversationSessionsMap.current.set(tabId, session);
// Update mode to 'new' since we're proceeding with a new plan
@@ -682,6 +723,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
mode: currentState?.mode,
agentType: currentState?.agentType,
projectPath: currentState?.projectPath,
autoRunFolderPath: currentState?.autoRunFolderPath,
});
setTabState(tabId, (prev) => ({
...prev,
@@ -835,16 +877,19 @@ export function useInlineWizard(): UseInlineWizardReturn {
// If transitioning from 'ask' to 'new' or 'iterate', we need to create the conversation session
if (currentState?.mode === 'ask' && (newMode === 'new' || newMode === 'iterate') && !conversationSessionsMap.current.has(tabId)) {
// Create conversation session if we have the required info
if (currentState.agentType && currentState.projectPath) {
const autoRunFolderPath = getAutoRunFolderPath(currentState.projectPath);
// Use the stored autoRunFolderPath from state (configured by user or default)
const effectiveAutoRunFolderPath = currentState.autoRunFolderPath ||
(currentState.projectPath ? getAutoRunFolderPath(currentState.projectPath) : null);
if (currentState.agentType && effectiveAutoRunFolderPath) {
const session = startInlineWizardConversation({
mode: newMode,
agentType: currentState.agentType,
directoryPath: currentState.projectPath,
directoryPath: currentState.projectPath || effectiveAutoRunFolderPath,
projectName: currentState.sessionName || 'Project',
goal: currentState.goal || undefined,
existingDocs: undefined, // Will be loaded separately if needed
autoRunFolderPath,
autoRunFolderPath: effectiveAutoRunFolderPath,
});
conversationSessionsMap.current.set(tabId, session);
@@ -1045,9 +1090,13 @@ export function useInlineWizard(): UseInlineWizardReturn {
setCurrentTabId(tabId);
}
// Get the effective Auto Run folder path (stored in state from startWizard)
const effectiveAutoRunFolderPath = currentState?.autoRunFolderPath ||
(currentState?.projectPath ? getAutoRunFolderPath(currentState.projectPath) : null);
// Validate we have the required state
if (!currentState?.agentType || !currentState?.projectPath) {
const errorMsg = 'Cannot generate documents: missing agent type or project path';
if (!currentState?.agentType || !effectiveAutoRunFolderPath) {
const errorMsg = 'Cannot generate documents: missing agent type or Auto Run folder path';
console.error('[useInlineWizard]', errorMsg);
setTabState(tabId, (prev) => ({ ...prev, error: errorMsg }));
callbacks?.onError?.(errorMsg);
@@ -1066,19 +1115,16 @@ export function useInlineWizard(): UseInlineWizardReturn {
}));
try {
// Get the Auto Run folder path
const autoRunFolderPath = getAutoRunFolderPath(currentState.projectPath);
// Call the document generation service
// Call the document generation service with the effective Auto Run folder path
const result = await generateInlineDocuments({
agentType: currentState.agentType,
directoryPath: currentState.projectPath,
directoryPath: currentState.projectPath || effectiveAutoRunFolderPath,
projectName: currentState.sessionName || 'Project',
conversationHistory: currentState.conversationHistory,
existingDocuments: currentState.existingDocuments,
mode: currentState.mode === 'iterate' ? 'iterate' : 'new',
goal: currentState.goal || undefined,
autoRunFolderPath,
autoRunFolderPath: effectiveAutoRunFolderPath,
sessionId: currentState.sessionId || undefined,
callbacks: {
onStart: () => {