mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
## CHANGES
- Introduced explicit read/write access rules: Auto Run writes only! 🔒 - Updated all wizard prompt templates with clearer safety constraints 🧭 - Refined tests to validate new “WRITE Limited / READ Unrestricted” wording 🧪 - Synced inline wizard context into `session.wizardState` for stable UI 🧷 - Scoped inline wizard to the tab it started on for isolation 🧩 - Auto-sent an initial wizard greeting to kickstart discovery instantly 🚀 - Added “Exit wizard” support (Escape/pill) via new `onExitWizard` callback 🚪 - Fixed wizard loading indicator to use `wizardState.isWaiting` accurately ⏳ - Restricted wizard conversation tools to read-only operations for safety 📚 - Deduplicated parsing by reusing shared structured-output + confidence utilities 🧰
This commit is contained in:
@@ -528,14 +528,17 @@ describe('wizardPrompts', () => {
|
||||
expect(prompt).toContain('Maestro');
|
||||
});
|
||||
|
||||
it('should include directory restriction instructions', () => {
|
||||
it('should include file access restriction instructions', () => {
|
||||
const config: SystemPromptConfig = {
|
||||
agentName: 'Test',
|
||||
agentPath: '/specific/path',
|
||||
};
|
||||
const prompt = generateSystemPrompt(config);
|
||||
|
||||
expect(prompt).toContain('ONLY create or modify files within this directory');
|
||||
// Check for the new file access restriction format
|
||||
expect(prompt).toContain('WRITE ACCESS (Limited)');
|
||||
expect(prompt).toContain('READ ACCESS (Unrestricted)');
|
||||
expect(prompt).toContain('ONLY create or modify files in the Auto Run folder');
|
||||
expect(prompt).toContain('/specific/path');
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,20 @@ You are an expert project planner creating actionable task documents for "{{PROJ
|
||||
|
||||
Based on the project discovery conversation below, create a series of Auto Run documents that will guide an AI coding assistant through building this project step by step.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
All files will be created in: {{DIRECTORY_PATH}}
|
||||
The documents will be saved to: {{DIRECTORY_PATH}}/{{AUTO_RUN_FOLDER_NAME}}/
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create files in the Auto Run folder:
|
||||
`{{DIRECTORY_PATH}}/{{AUTO_RUN_FOLDER_NAME}}/`
|
||||
|
||||
Do NOT write, create, or modify files anywhere else. The wizard outputs documents using special markers (---BEGIN DOCUMENT--- / ---END DOCUMENT---) which are then saved by the application to the Auto Run folder.
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to inform your planning:
|
||||
- Read any file in: `{{DIRECTORY_PATH}}`
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations.
|
||||
|
||||
## Critical Requirements for Phase 1
|
||||
|
||||
|
||||
@@ -4,10 +4,20 @@ You are an expert project planner creating actionable task documents for "{{PROJ
|
||||
|
||||
Based on the project discovery conversation below, create or update Auto Run documents. The user has existing documents and wants to extend or modify their plans.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
All files will be created or updated in: {{DIRECTORY_PATH}}
|
||||
The documents folder: {{DIRECTORY_PATH}}/{{AUTO_RUN_FOLDER_NAME}}/
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create or update files in the Auto Run folder:
|
||||
`{{DIRECTORY_PATH}}/{{AUTO_RUN_FOLDER_NAME}}/`
|
||||
|
||||
Do NOT write, create, or modify files anywhere else. The wizard outputs documents using special markers (---BEGIN DOCUMENT--- / ---END DOCUMENT---) which are then saved by the application to the Auto Run folder.
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to inform your planning:
|
||||
- Read any file in: `{{DIRECTORY_PATH}}`
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations.
|
||||
|
||||
## Existing Documents
|
||||
|
||||
|
||||
@@ -4,12 +4,24 @@ You are a planning assistant helping extend existing work in "{{PROJECT_NAME}}".
|
||||
|
||||
You are helping iterate on existing plans. The user has Auto Run documents and wants to extend or modify them.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
You will ONLY create or modify files within this directory:
|
||||
{{AGENT_PATH}}
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create or modify files in the Auto Run folder:
|
||||
`{{AUTORUN_FOLDER}}`
|
||||
|
||||
Do not reference, create, or modify files outside this path.
|
||||
Do NOT write, create, or modify files anywhere else. This includes:
|
||||
- No creating files in the working directory
|
||||
- No modifying existing project files
|
||||
- No creating temporary files outside the Auto Run folder
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to understand the project:
|
||||
- Read any file in the working directory: `{{AGENT_PATH}}`
|
||||
- Read any file the user references
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations without file conflicts.
|
||||
|
||||
## Existing Documents
|
||||
|
||||
|
||||
@@ -4,12 +4,24 @@ You are a planning assistant starting fresh in an existing Maestro session for "
|
||||
|
||||
You are helping create a new action plan in an active session. The user has an established project but wants to start fresh with a new plan.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
You will ONLY create or modify files within this directory:
|
||||
{{AGENT_PATH}}
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create or modify files in the Auto Run folder:
|
||||
`{{AUTORUN_FOLDER}}`
|
||||
|
||||
Do not reference, create, or modify files outside this path.
|
||||
Do NOT write, create, or modify files anywhere else. This includes:
|
||||
- No creating files in the working directory
|
||||
- No modifying existing project files
|
||||
- No creating temporary files outside the Auto Run folder
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to understand the project:
|
||||
- Read any file in the working directory: `{{AGENT_PATH}}`
|
||||
- Read any file the user references
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations without file conflicts.
|
||||
|
||||
## Auto-run Documents
|
||||
|
||||
|
||||
@@ -4,12 +4,24 @@ You are a planning assistant helping in an existing Maestro session for "{{PROJE
|
||||
|
||||
You are helping plan work in an active session. The user has an established project context and wants to create or extend an action plan.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
You will ONLY create or modify files within this directory:
|
||||
{{AGENT_PATH}}
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create or modify files in the Auto Run folder:
|
||||
`{{AUTORUN_FOLDER}}`
|
||||
|
||||
Do not reference, create, or modify files outside this path.
|
||||
Do NOT write, create, or modify files anywhere else. This includes:
|
||||
- No creating files in the working directory
|
||||
- No modifying existing project files
|
||||
- No creating temporary files outside the Auto Run folder
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to understand the project:
|
||||
- Read any file in the working directory: `{{AGENT_PATH}}`
|
||||
- Read any file the user references
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations without file conflicts.
|
||||
|
||||
## Auto-run Documents
|
||||
|
||||
|
||||
@@ -4,12 +4,24 @@ You are a friendly project discovery assistant helping to set up "{{PROJECT_NAME
|
||||
|
||||
You are 🎼 Maestro's onboarding assistant, helping the user define their project so we can create an actionable plan.
|
||||
|
||||
## Working Directory
|
||||
## File Access Restrictions
|
||||
|
||||
You will ONLY create or modify files within this directory:
|
||||
{{AGENT_PATH}}
|
||||
**WRITE ACCESS (Limited):**
|
||||
You may ONLY create or modify files in the Auto Run folder:
|
||||
`{{AUTORUN_FOLDER}}`
|
||||
|
||||
Do not reference, create, or modify files outside this path, **except** for the Auto Run folder which may be located elsewhere.
|
||||
Do NOT write, create, or modify files anywhere else. This includes:
|
||||
- No creating files in the working directory
|
||||
- No modifying existing project files
|
||||
- No creating temporary files outside the Auto Run folder
|
||||
|
||||
**READ ACCESS (Unrestricted):**
|
||||
You may READ files from anywhere to understand the project:
|
||||
- Read any file in the working directory: `{{AGENT_PATH}}`
|
||||
- Read any file the user references
|
||||
- Examine project structure, code, and configuration
|
||||
|
||||
This restriction ensures the wizard can safely run in parallel with other AI operations without file conflicts.
|
||||
|
||||
## Auto-run Documents
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import { GroupChatRightPanel, type GroupChatRightTab } from './components/GroupC
|
||||
import {
|
||||
// Batch processing
|
||||
useBatchProcessor,
|
||||
useInlineWizard,
|
||||
type PreviousUIState,
|
||||
// Settings
|
||||
useSettings,
|
||||
@@ -89,7 +88,7 @@ import { InputProvider, useInputContext } from './contexts/InputContext';
|
||||
import { GroupChatProvider, useGroupChat } from './contexts/GroupChatContext';
|
||||
import { AutoRunProvider, useAutoRun } from './contexts/AutoRunContext';
|
||||
import { SessionProvider, useSession } from './contexts/SessionContext';
|
||||
import { InlineWizardProvider } from './contexts/InlineWizardContext';
|
||||
import { InlineWizardProvider, useInlineWizardContext } from './contexts/InlineWizardContext';
|
||||
import { ToastContainer } from './components/Toast';
|
||||
|
||||
// Import services
|
||||
@@ -4239,13 +4238,127 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
return activeSession ? getBatchState(activeSession.id) : getBatchState('');
|
||||
}, [activeBatchSessionIds, activeSession, getBatchState]);
|
||||
|
||||
// Inline wizard hook for /wizard command
|
||||
// Inline wizard context for /wizard command
|
||||
// This manages the state for the inline wizard that creates/iterates on Auto Run documents
|
||||
const {
|
||||
startWizard: startInlineWizard,
|
||||
endWizard: endInlineWizard,
|
||||
clearError: clearInlineWizardError,
|
||||
retryLastMessage: retryInlineWizardMessage,
|
||||
} = useInlineWizard();
|
||||
generateDocuments: generateInlineWizardDocuments,
|
||||
// State for syncing to session.wizardState
|
||||
isWizardActive: inlineWizardActive,
|
||||
isWaiting: inlineWizardIsWaiting,
|
||||
wizardMode: inlineWizardMode,
|
||||
wizardGoal: inlineWizardGoal,
|
||||
confidence: inlineWizardConfidence,
|
||||
ready: inlineWizardReady,
|
||||
conversationHistory: inlineWizardConversationHistory,
|
||||
error: inlineWizardError,
|
||||
isGeneratingDocs: inlineWizardIsGeneratingDocs,
|
||||
generatedDocuments: inlineWizardGeneratedDocuments,
|
||||
streamingContent: inlineWizardStreamingContent,
|
||||
generationProgress: inlineWizardGenerationProgress,
|
||||
state: inlineWizardState,
|
||||
wizardTabId: inlineWizardTabId,
|
||||
} = useInlineWizardContext();
|
||||
|
||||
// Sync inline wizard context state to session.wizardState
|
||||
// This bridges the gap between the context-based state and session-based UI rendering
|
||||
// The wizard is per-tab, so only sync when the active tab matches the wizard's tab
|
||||
useEffect(() => {
|
||||
if (!activeSession) return;
|
||||
|
||||
const activeTab = getActiveTab(activeSession);
|
||||
const activeTabId = activeTab?.id;
|
||||
|
||||
// Only sync if the active tab is the wizard's tab
|
||||
const isWizardTab = inlineWizardTabId && activeTabId === inlineWizardTabId;
|
||||
const currentWizardState = activeSession.wizardState;
|
||||
const shouldHaveWizardState = isWizardTab && (inlineWizardActive || inlineWizardIsGeneratingDocs);
|
||||
|
||||
if (!shouldHaveWizardState && !currentWizardState) {
|
||||
// Neither active nor has state - nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldHaveWizardState && currentWizardState) {
|
||||
// Wizard was deactivated or we switched to a different tab - clear the state
|
||||
setSessions(prev => prev.map(s =>
|
||||
s.id === activeSession.id
|
||||
? { ...s, wizardState: undefined }
|
||||
: s
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWizardTab) {
|
||||
// Not the wizard's tab - don't sync
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync the wizard state to the session
|
||||
const newWizardState = {
|
||||
isActive: inlineWizardActive,
|
||||
isWaiting: inlineWizardIsWaiting,
|
||||
mode: inlineWizardMode === 'ask' ? 'new' : inlineWizardMode, // Map 'ask' to 'new' for session state
|
||||
goal: inlineWizardGoal ?? undefined,
|
||||
confidence: inlineWizardConfidence,
|
||||
ready: inlineWizardReady,
|
||||
conversationHistory: inlineWizardConversationHistory.map(msg => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
confidence: msg.confidence,
|
||||
ready: msg.ready,
|
||||
})),
|
||||
previousUIState: inlineWizardState.previousUIState ?? {
|
||||
readOnlyMode: false,
|
||||
saveToHistory: true,
|
||||
showThinking: false,
|
||||
},
|
||||
error: inlineWizardError,
|
||||
isGeneratingDocs: inlineWizardIsGeneratingDocs,
|
||||
generatedDocuments: inlineWizardGeneratedDocuments.map(doc => ({
|
||||
filename: doc.filename,
|
||||
content: doc.content,
|
||||
taskCount: doc.taskCount,
|
||||
savedPath: doc.savedPath,
|
||||
})),
|
||||
streamingContent: inlineWizardStreamingContent,
|
||||
currentGeneratingIndex: inlineWizardGenerationProgress?.current,
|
||||
totalDocuments: inlineWizardGenerationProgress?.total,
|
||||
autoRunFolderPath: inlineWizardState.projectPath
|
||||
? `${inlineWizardState.projectPath}/Auto Run Docs`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
setSessions(prev => prev.map(s =>
|
||||
s.id === activeSession.id
|
||||
? { ...s, wizardState: newWizardState }
|
||||
: s
|
||||
));
|
||||
}, [
|
||||
activeSession?.id,
|
||||
activeSession?.activeTabId,
|
||||
inlineWizardTabId,
|
||||
inlineWizardActive,
|
||||
inlineWizardIsWaiting,
|
||||
inlineWizardMode,
|
||||
inlineWizardGoal,
|
||||
inlineWizardConfidence,
|
||||
inlineWizardReady,
|
||||
inlineWizardConversationHistory,
|
||||
inlineWizardError,
|
||||
inlineWizardIsGeneratingDocs,
|
||||
inlineWizardGeneratedDocuments,
|
||||
inlineWizardStreamingContent,
|
||||
inlineWizardGenerationProgress,
|
||||
inlineWizardState.previousUIState,
|
||||
inlineWizardState.projectPath,
|
||||
setSessions,
|
||||
]);
|
||||
|
||||
// Handler for the built-in /history command
|
||||
// Requests a synopsis from the current agent session and saves to history
|
||||
@@ -4424,7 +4537,14 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
|
||||
// Start the inline wizard with the argument text (natural language input)
|
||||
// The wizard will use the intent parser to determine mode (new/iterate/ask)
|
||||
startInlineWizard(args || undefined, currentUIState);
|
||||
startInlineWizard(
|
||||
args || undefined,
|
||||
currentUIState,
|
||||
activeSession.projectRoot || activeSession.cwd, // Project path for Auto Run folder detection
|
||||
activeSession.toolType, // Agent type for AI conversation
|
||||
activeSession.name, // Session/project name
|
||||
activeTab.id // Tab ID for per-tab isolation
|
||||
);
|
||||
|
||||
// Show a system log entry indicating wizard started
|
||||
const wizardLog: LogEntry = {
|
||||
@@ -10048,9 +10168,12 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
// Refresh the Auto Run panel to show newly generated documents
|
||||
handleAutoRunRefresh();
|
||||
}}
|
||||
// Inline wizard error handling callbacks
|
||||
// Inline wizard callbacks
|
||||
onWizardLetsGo={generateInlineWizardDocuments}
|
||||
onWizardRetry={retryInlineWizardMessage}
|
||||
onWizardClearError={clearInlineWizardError}
|
||||
// Inline wizard exit handler (for WizardInputPanel)
|
||||
onExitWizard={endInlineWizard}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -245,6 +245,8 @@ interface MainPanelProps {
|
||||
onWizardRetry?: () => void;
|
||||
/** Called when user dismisses an error in the wizard */
|
||||
onWizardClearError?: () => void;
|
||||
/** Called when user exits inline wizard mode (Escape or clicks pill) */
|
||||
onExitWizard?: () => void;
|
||||
}
|
||||
|
||||
// PERFORMANCE: Wrap with React.memo to prevent re-renders when parent (App.tsx) re-renders
|
||||
@@ -296,6 +298,8 @@ export const MainPanel = React.memo(forwardRef<MainPanelHandle, MainPanelProps>(
|
||||
mergeSourceName,
|
||||
mergeTargetName,
|
||||
onCancelMerge,
|
||||
// Inline wizard exit handler
|
||||
onExitWizard,
|
||||
} = props;
|
||||
|
||||
// isCurrentSessionAutoMode: THIS session has active batch run (for all UI indicators)
|
||||
@@ -1139,7 +1143,7 @@ export const MainPanel = React.memo(forwardRef<MainPanelHandle, MainPanelProps>(
|
||||
key={`wizard-${activeSession.id}-${activeSession.activeTabId}`}
|
||||
theme={theme}
|
||||
conversationHistory={activeSession.wizardState.conversationHistory}
|
||||
isLoading={activeSession.state === 'busy'}
|
||||
isLoading={activeSession.wizardState.isWaiting ?? false}
|
||||
agentName={activeSession.name}
|
||||
confidence={activeSession.wizardState.confidence}
|
||||
ready={activeSession.wizardState.ready}
|
||||
@@ -1275,6 +1279,8 @@ export const MainPanel = React.memo(forwardRef<MainPanelHandle, MainPanelProps>(
|
||||
mergeSourceName={mergeSourceName}
|
||||
mergeTargetName={mergeTargetName}
|
||||
onCancelMerge={onCancelMerge}
|
||||
// Inline wizard mode
|
||||
onExitWizard={onExitWizard}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -112,6 +112,7 @@ export function InlineWizardProvider({ children }: InlineWizardProviderProps) {
|
||||
wizardState.generateDocuments,
|
||||
wizardState.streamingContent,
|
||||
wizardState.generationProgress,
|
||||
wizardState.wizardTabId,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* runs inline within the existing AI conversation interface.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { parseWizardIntent } from '../services/wizardIntentParser';
|
||||
import {
|
||||
hasExistingAutoRunDocs,
|
||||
@@ -123,6 +123,10 @@ export interface InlineWizardState {
|
||||
agentType: ToolType | null;
|
||||
/** Session name/project name */
|
||||
sessionName: string | null;
|
||||
/** Tab ID the wizard was started on (for per-tab isolation) */
|
||||
tabId: string | null;
|
||||
/** Whether the initial greeting has been sent to kick off the conversation */
|
||||
initialGreetingSent: boolean;
|
||||
/** Streaming content being generated (accumulates as AI outputs) */
|
||||
streamingContent: string;
|
||||
/** Progress tracking for document generation */
|
||||
@@ -163,6 +167,8 @@ export interface UseInlineWizardReturn {
|
||||
streamingContent: string;
|
||||
/** Progress tracking for document generation (e.g., "Phase 1 of 3") */
|
||||
generationProgress: GenerationProgress | null;
|
||||
/** Tab ID the wizard was started on (for per-tab isolation) */
|
||||
wizardTabId: string | null;
|
||||
/** Full wizard state */
|
||||
state: InlineWizardState;
|
||||
/**
|
||||
@@ -172,13 +178,15 @@ export interface UseInlineWizardReturn {
|
||||
* @param projectPath - Project path to check for existing Auto Run documents
|
||||
* @param agentType - The AI agent type to use for conversation
|
||||
* @param sessionName - The session name (used as project name)
|
||||
* @param tabId - The tab ID to associate the wizard with
|
||||
*/
|
||||
startWizard: (
|
||||
naturalLanguageInput?: string,
|
||||
currentUIState?: PreviousUIState,
|
||||
projectPath?: string,
|
||||
agentType?: ToolType,
|
||||
sessionName?: string
|
||||
sessionName?: string,
|
||||
tabId?: string
|
||||
) => Promise<void>;
|
||||
/** End the wizard and restore previous UI state */
|
||||
endWizard: () => Promise<PreviousUIState | null>;
|
||||
@@ -256,6 +264,8 @@ const initialState: InlineWizardState = {
|
||||
projectPath: null,
|
||||
agentType: null,
|
||||
sessionName: null,
|
||||
tabId: null,
|
||||
initialGreetingSent: false,
|
||||
streamingContent: '',
|
||||
generationProgress: null,
|
||||
};
|
||||
@@ -357,7 +367,8 @@ export function useInlineWizard(): UseInlineWizardReturn {
|
||||
currentUIState?: PreviousUIState,
|
||||
projectPath?: string,
|
||||
agentType?: ToolType,
|
||||
sessionName?: string
|
||||
sessionName?: string,
|
||||
tabId?: string
|
||||
): Promise<void> => {
|
||||
// Store current UI state for later restoration
|
||||
if (currentUIState) {
|
||||
@@ -383,6 +394,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
|
||||
projectPath: projectPath || null,
|
||||
agentType: agentType || null,
|
||||
sessionName: sessionName || null,
|
||||
tabId: tabId || null,
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -918,6 +930,55 @@ export function useInlineWizard(): UseInlineWizardReturn {
|
||||
// Compute readyToGenerate based on ready flag and confidence threshold
|
||||
const readyToGenerate = state.ready && state.confidence >= READY_CONFIDENCE_THRESHOLD;
|
||||
|
||||
// Automatically send an initial greeting to start the conversation
|
||||
// This triggers the agent to examine the project and ask opening questions
|
||||
useEffect(() => {
|
||||
// Only send if:
|
||||
// - Wizard is active and not initializing
|
||||
// - Mode is determined (new or iterate)
|
||||
// - Haven't sent the initial greeting yet
|
||||
// - No conversation history yet
|
||||
// - Not currently waiting for a response
|
||||
const shouldSendGreeting =
|
||||
state.isActive &&
|
||||
!state.isInitializing &&
|
||||
(state.mode === 'new' || state.mode === 'iterate') &&
|
||||
!state.initialGreetingSent &&
|
||||
state.conversationHistory.length === 0 &&
|
||||
!state.isWaiting &&
|
||||
conversationSessionRef.current;
|
||||
|
||||
if (shouldSendGreeting) {
|
||||
// Mark as sent immediately to prevent duplicate sends
|
||||
setState((prev) => ({ ...prev, initialGreetingSent: true }));
|
||||
|
||||
// Build an appropriate initial message based on mode and goal
|
||||
let initialMessage: string;
|
||||
if (state.mode === 'iterate' && state.goal) {
|
||||
initialMessage = `I want to ${state.goal}`;
|
||||
} else if (state.mode === 'iterate') {
|
||||
initialMessage = 'I want to iterate on my existing Auto Run documents.';
|
||||
} else {
|
||||
initialMessage = 'Hello! I want to create a new action plan.';
|
||||
}
|
||||
|
||||
// Send the initial message to trigger the agent's greeting
|
||||
// We use a short delay to ensure state is fully updated
|
||||
setTimeout(() => {
|
||||
sendMessage(initialMessage);
|
||||
}, 100);
|
||||
}
|
||||
}, [
|
||||
state.isActive,
|
||||
state.isInitializing,
|
||||
state.mode,
|
||||
state.goal,
|
||||
state.initialGreetingSent,
|
||||
state.conversationHistory.length,
|
||||
state.isWaiting,
|
||||
sendMessage,
|
||||
]);
|
||||
|
||||
return {
|
||||
// Convenience accessors
|
||||
isWizardActive: state.isActive,
|
||||
@@ -935,6 +996,7 @@ export function useInlineWizard(): UseInlineWizardReturn {
|
||||
error: state.error,
|
||||
streamingContent: state.streamingContent,
|
||||
generationProgress: state.generationProgress,
|
||||
wizardTabId: state.tabId,
|
||||
|
||||
// Full state
|
||||
state,
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
wizardInlineIteratePrompt,
|
||||
wizardInlineNewPrompt,
|
||||
} from '../../prompts';
|
||||
import {
|
||||
parseStructuredOutput,
|
||||
getConfidenceColor,
|
||||
} from '../components/Wizard/services/wizardPrompts';
|
||||
|
||||
/**
|
||||
* Extended ExistingDocument interface that includes loaded content.
|
||||
@@ -278,141 +282,35 @@ function buildPromptWithContext(
|
||||
/**
|
||||
* Parse a structured response from the agent.
|
||||
*
|
||||
* Attempts to extract JSON from the response with multiple fallback strategies.
|
||||
* Delegates to the shared parseStructuredOutput from wizardPrompts.ts to avoid
|
||||
* code duplication. The shared implementation handles multiple fallback strategies
|
||||
* for extracting JSON from agent responses.
|
||||
*
|
||||
* @param response The raw response string from the agent
|
||||
* @returns Parsed WizardResponse or null if parsing failed
|
||||
*/
|
||||
export function parseWizardResponse(response: string): WizardResponse | null {
|
||||
const rawText = response.trim();
|
||||
const result = parseStructuredOutput(response);
|
||||
|
||||
// Strategy 1: Direct JSON parse
|
||||
try {
|
||||
const parsed = JSON.parse(rawText);
|
||||
if (isValidWizardResponse(parsed)) {
|
||||
return normalizeResponse(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Continue to next strategy
|
||||
if (result.parseSuccess && result.structured) {
|
||||
// Apply our ready threshold check on top of the shared parsing
|
||||
return {
|
||||
confidence: result.structured.confidence,
|
||||
ready: result.structured.ready && result.structured.confidence >= READY_CONFIDENCE_THRESHOLD,
|
||||
message: result.structured.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Strategy 2: Extract JSON from markdown code blocks
|
||||
const codeBlockMatch = rawText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (codeBlockMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(codeBlockMatch[1].trim());
|
||||
if (isValidWizardResponse(parsed)) {
|
||||
return normalizeResponse(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Continue to next strategy
|
||||
}
|
||||
// If parsing failed but we have a structured response from fallback, use it
|
||||
if (result.structured) {
|
||||
return {
|
||||
confidence: result.structured.confidence,
|
||||
ready: result.structured.ready && result.structured.confidence >= READY_CONFIDENCE_THRESHOLD,
|
||||
message: result.structured.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Strategy 3: Find JSON object pattern with required fields
|
||||
const jsonMatch = rawText.match(/\{[\s\S]*"confidence"[\s\S]*"ready"[\s\S]*"message"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
if (isValidWizardResponse(parsed)) {
|
||||
return normalizeResponse(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Continue to next strategy
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Find any JSON object pattern
|
||||
const anyJsonMatch = rawText.match(/\{[^{}]*\}/);
|
||||
if (anyJsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(anyJsonMatch[0]);
|
||||
if (isValidWizardResponse(parsed)) {
|
||||
return normalizeResponse(parsed);
|
||||
}
|
||||
} catch {
|
||||
// Continue to fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Create a response from raw text with heuristics
|
||||
return createFallbackResponse(rawText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object matches the expected wizard response format.
|
||||
*/
|
||||
function isValidWizardResponse(obj: unknown): obj is WizardResponse {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = obj as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
typeof response.confidence === 'number' &&
|
||||
typeof response.ready === 'boolean' &&
|
||||
typeof response.message === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a response to ensure valid ranges and types.
|
||||
*/
|
||||
function normalizeResponse(response: WizardResponse): WizardResponse {
|
||||
return {
|
||||
confidence: Math.max(0, Math.min(100, Math.round(response.confidence))),
|
||||
ready: response.ready && response.confidence >= READY_CONFIDENCE_THRESHOLD,
|
||||
message: response.message.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fallback response when parsing fails.
|
||||
* Uses heuristics to extract useful information from raw text.
|
||||
*/
|
||||
function createFallbackResponse(rawText: string): WizardResponse {
|
||||
const DEFAULT_CONFIDENCE = 20;
|
||||
|
||||
// Try to extract confidence from text patterns
|
||||
let confidence = DEFAULT_CONFIDENCE;
|
||||
const confidenceMatch = rawText.match(/confidence[:\s]*(\d+)/i) ||
|
||||
rawText.match(/(\d+)\s*%?\s*confiden/i);
|
||||
if (confidenceMatch) {
|
||||
const extractedConfidence = parseInt(confidenceMatch[1], 10);
|
||||
if (extractedConfidence >= 0 && extractedConfidence <= 100) {
|
||||
confidence = extractedConfidence;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect ready status from text
|
||||
const readyPatterns = /\b(ready to proceed|ready to create|let's proceed|shall we proceed|i'm ready)\b/i;
|
||||
const notReadyPatterns = /\b(need more|clarif|question|tell me more|could you explain)\b/i;
|
||||
|
||||
let ready = false;
|
||||
if (confidence >= READY_CONFIDENCE_THRESHOLD && readyPatterns.test(rawText)) {
|
||||
ready = true;
|
||||
}
|
||||
if (notReadyPatterns.test(rawText)) {
|
||||
ready = false;
|
||||
}
|
||||
|
||||
// Clean up the message
|
||||
let message = rawText
|
||||
.replace(/```(?:json)?/g, '')
|
||||
.replace(/```/g, '')
|
||||
.replace(/^\s*\{[\s\S]*?\}\s*$/g, '')
|
||||
.trim();
|
||||
|
||||
if (!message) {
|
||||
message = rawText;
|
||||
}
|
||||
|
||||
return {
|
||||
confidence,
|
||||
ready,
|
||||
message,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,6 +386,8 @@ function extractResultFromStreamJson(output: string, agentType: ToolType): strin
|
||||
|
||||
/**
|
||||
* Build CLI args for the agent based on its type and capabilities.
|
||||
* For wizard conversations, we restrict tool usage to read-only operations
|
||||
* to prevent the agent from making changes during the discovery phase.
|
||||
*/
|
||||
function buildArgsForAgent(agent: any): string[] {
|
||||
const agentId = agent.id;
|
||||
@@ -498,6 +398,12 @@ function buildArgsForAgent(agent: any): string[] {
|
||||
if (!args.includes('--include-partial-messages')) {
|
||||
args.push('--include-partial-messages');
|
||||
}
|
||||
// Restrict to read-only tools during wizard conversation
|
||||
// The agent can read files to understand the project, but cannot write/edit
|
||||
// This ensures the wizard conversation phase doesn't make code changes
|
||||
if (!args.includes('--allowedTools')) {
|
||||
args.push('--allowedTools', 'Read', 'Glob', 'Grep', 'LS');
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -703,21 +609,5 @@ export async function endInlineWizardConversation(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color for the confidence meter based on the level.
|
||||
*
|
||||
* @param confidence The confidence level (0-100)
|
||||
* @returns HSL color string transitioning from red to yellow to green
|
||||
*/
|
||||
export function getConfidenceColor(confidence: number): string {
|
||||
const clampedConfidence = Math.max(0, Math.min(100, confidence));
|
||||
|
||||
let hue: number;
|
||||
if (clampedConfidence <= 50) {
|
||||
hue = (clampedConfidence / 50) * 60; // 0 to 60 (red to yellow)
|
||||
} else {
|
||||
hue = 60 + ((clampedConfidence - 50) / 50) * 60; // 60 to 120 (yellow to green)
|
||||
}
|
||||
|
||||
return `hsl(${hue}, 80%, 45%)`;
|
||||
}
|
||||
// Re-export getConfidenceColor from the shared location for backwards compatibility
|
||||
export { getConfidenceColor };
|
||||
|
||||
@@ -98,6 +98,8 @@ export interface WizardGeneratedDocument {
|
||||
export interface SessionWizardState {
|
||||
/** Whether wizard is currently active */
|
||||
isActive: boolean;
|
||||
/** Whether waiting for AI response */
|
||||
isWaiting?: boolean;
|
||||
/** Current wizard mode: 'new' for creating documents, 'iterate' for modifying existing */
|
||||
mode: WizardMode;
|
||||
/** Goal for iterate mode (what the user wants to add/change) */
|
||||
|
||||
Reference in New Issue
Block a user