Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent
568e64a2dc feat: Implement initialize_project editor function
This change adds the `initialize_project` editor function, allowing AI to create new projects from examples. It includes necessary updates to callbacks, runners, and UI components to handle project initialization seamlessly.

Co-authored-by: florian <florian@gdevelop.io>
2025-09-07 15:35:50 +00:00
6 changed files with 173 additions and 42 deletions

View File

@@ -50,11 +50,11 @@ import {
sendAiRequestMessageSent,
sendAiRequestStarted,
} from '../Utils/Analytics/EventSender';
import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
import { prepareAiUserContent } from './PrepareAiUserContent';
import { AiRequestContext } from './AiRequestContext';
import { getAiConfigurationPresetsWithAvailability } from './AiConfiguration';
import useAlertDialog from '../UI/Alert/useAlertDialog';
const gd: libGDevelop = global.gd;
@@ -238,11 +238,11 @@ export const useSelectedAiRequest = ({
}: {|
initialAiRequestId: string | null,
|}) => {
const { profile, getAuthorizationHeader } = React.useContext(
AuthenticatedUserContext
);
const { aiRequestStorage } = React.useContext(AiRequestContext);
const { aiRequests, updateAiRequest } = aiRequestStorage;
const { profile, getAuthorizationHeader } = React.useContext(
AuthenticatedUserContext
);
const { aiRequestStorage } = React.useContext(AiRequestContext);
const { aiRequests, updateAiRequest } = aiRequestStorage;
const [selectedAiRequestId, setSelectedAiRequestId] = React.useState<
string | null
@@ -332,6 +332,7 @@ type Props = {|
resourceManagementProps: ResourceManagementProps,
fileMetadata: ?FileMetadata,
storageProvider: ?StorageProvider,
getStorageProvider: () => StorageProvider,
setToolbar: (?React.Node) => void,
i18n: I18nType,
onCreateEmptyProject: (newProjectSetup: NewProjectSetup) => Promise<void>,
@@ -409,6 +410,7 @@ export const AskAiEditor = React.memo<Props>(
resourceManagementProps,
fileMetadata,
storageProvider,
getStorageProvider,
i18n,
onCreateEmptyProject,
onCreateProjectFromExample,
@@ -425,8 +427,34 @@ export const AskAiEditor = React.memo<Props>(
const editorCallbacks: EditorCallbacks = React.useMemo(
() => ({
onOpenLayout,
onCreateProjectFromExample: async (exampleName: string, exampleSlug: string) => {
// Find the example by slug
const { listAllExamples } = await import('../Utils/GDevelopServices/Example');
const allExamples = await listAllExamples();
const exampleShortHeader = allExamples.exampleShortHeaders.find(
example => example.slug === exampleSlug
);
if (!exampleShortHeader) {
throw new Error(`Example with slug "${exampleSlug}" not found`);
}
const newProjectSetup: NewProjectSetup = {
projectName: exampleName,
storageProvider: project ? getStorageProvider() : null,
saveAsLocation: null,
dontOpenAnySceneOrProjectManager: false,
};
await onCreateProjectFromExample(
exampleShortHeader,
newProjectSetup,
i18n,
false // isQuickCustomization
);
},
}),
[onOpenLayout]
[onOpenLayout, onCreateProjectFromExample, project, i18n, getStorageProvider]
);
const {
@@ -502,10 +530,6 @@ export const AskAiEditor = React.memo<Props>(
setLastSendError,
} = aiRequestStorage;
const {
createAiProject,
renderCreateAiProjectDialog,
} = useCreateAiProjectDialog();
const updateToolbar = React.useCallback(
() => {
@@ -543,6 +567,7 @@ export const AskAiEditor = React.memo<Props>(
const { openCreditsPackageDialog } = React.useContext(
CreditsPackageStoreContext
);
const { showAlert } = useAlertDialog();
const {
profile,
@@ -597,26 +622,12 @@ export const AskAiEditor = React.memo<Props>(
} = newAiRequestOptions;
startNewAiRequest(null);
// If no project is opened, create a new empty one if the request is for
// the AI agent.
// If no project is opened for agent mode, show an error
if (mode === 'agent' && !project) {
try {
console.info(
'No project opened, opening the dialog to create a new project.'
);
const result = await createAiProject();
if (result === 'canceled') {
return;
}
console.info('New project created - starting AI request.');
startNewAiRequest({
mode,
userRequest,
aiConfigurationPresetId,
});
} catch (error) {
console.error('Error creating a new empty project:', error);
}
showAlert({
title: t`No project opened`,
message: t`Please open or create a project before using the AI agent. The AI can help you create a project by using the "initialize_project" command.`,
});
return;
}
@@ -734,9 +745,9 @@ export const AskAiEditor = React.memo<Props>(
setSendingAiRequest,
upToDateSelectedAiRequestId,
updateAiRequest,
createAiProject,
newAiRequestOptions,
onOpenAskAi,
showAlert,
]
);
@@ -1017,10 +1028,6 @@ export const AskAiEditor = React.memo<Props>(
/>
</div>
</Paper>
{renderCreateAiProjectDialog({
onCreateEmptyProject,
onCreateProjectFromExample,
})}
<AskAiHistory
open={isHistoryOpen}
onClose={onCloseHistory}
@@ -1053,6 +1060,7 @@ export const renderAskAiEditorContainer = (
resourceManagementProps={props.resourceManagementProps}
fileMetadata={props.fileMetadata}
storageProvider={props.storageProvider}
getStorageProvider={props.getStorageProvider}
setToolbar={props.setToolbar}
isActive={props.isActive}
onCreateEmptyProject={props.onCreateEmptyProject}

View File

@@ -146,13 +146,35 @@ export const processEditorFunctionCalls = async ({
searchAndInstallAsset,
}
);
const { success, ...output } = result;
results.push({
status: 'finished',
call_id,
success,
output,
});
// Handle special case for initialize_project
if (result._requiresProjectInitialization && editorCallbacks.onCreateProjectFromExample) {
const { name, exampleSlug } = result._projectInitializationData;
try {
await editorCallbacks.onCreateProjectFromExample(name, exampleSlug);
results.push({
status: 'finished',
call_id,
success: true,
output: { message: `Project "${name}" initialized from example "${exampleSlug}".` },
});
} catch (error) {
results.push({
status: 'finished',
call_id,
success: false,
output: { message: `Failed to initialize project: ${error.message}` },
});
}
} else {
const { success, ...output } = result;
results.push({
status: 'finished',
call_id,
success,
output,
});
}
if (success && args) {
if (name === 'create_scene' && typeof args.scene_name === 'string') {

View File

@@ -123,6 +123,10 @@ export type EditorCallbacks = {|
| 'none',
|}
) => void,
onCreateProjectFromExample?: (
exampleName: string,
exampleSlug: string
) => Promise<void>,
|};
export type SceneEventsOutsideEditorChanges = {|
@@ -3461,6 +3465,45 @@ const addOrEditVariable: EditorFunction = {
},
};
const initializeProject: EditorFunction = {
renderForEditor: ({ project, args, editorCallbacks, shouldShowDetails }) => {
const name = SafeExtractor.extractStringProperty(args, 'name');
const exampleSlug = SafeExtractor.extractStringProperty(args, 'example_slug');
if (!name && !exampleSlug) {
return {
text: <Trans>Initialize project (missing required arguments)</Trans>,
};
}
return {
text: (
<Trans>
Initialize project{name && ` "${name}"`}
{exampleSlug && ` from example "${exampleSlug}"`}
</Trans>
),
};
},
launchFunction: async ({ project, args }) => {
const name = extractRequiredString(args, 'name');
const exampleSlug = extractRequiredString(args, 'example_slug');
// This function requires special handling in the AskAiEditorContainer
// because project initialization needs to be done through the MainFrame callbacks
return {
success: true,
message: `Project initialization requested: name="${name}", example_slug="${exampleSlug}"`,
// Signal that this needs special handling
_requiresProjectInitialization: true,
_projectInitializationData: {
name,
exampleSlug,
},
};
},
};
export const editorFunctions: { [string]: EditorFunction } = {
create_object: createObject,
inspect_object_properties: inspectObjectProperties,
@@ -3479,4 +3522,5 @@ export const editorFunctions: { [string]: EditorFunction } = {
inspect_scene_properties_layers_effects: inspectScenePropertiesLayersEffects,
change_scene_properties_layers_effects: changeScenePropertiesLayersEffects,
add_or_edit_variable: addOrEditVariable,
initialize_project: initializeProject,
};

View File

@@ -47,6 +47,7 @@ export type RenderEditorContainerProps = {|
project: ?gdProject,
fileMetadata: ?FileMetadata,
storageProvider: StorageProvider,
getStorageProvider: () => StorageProvider,
setToolbar: (?React.Node) => void,
setGamesPlatformFrameShown: ({| shown: boolean, isMobile: boolean |}) => void,

View File

@@ -578,6 +578,7 @@ const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
project: currentProject,
fileMetadata: currentFileMetadata,
storageProvider: getStorageProvider(),
getStorageProvider,
ref: editorRef => (editorTab.editorRef = editorRef),
setToolbar: editorToolbar =>
setEditorToolbar(editorToolbar, isCurrentTab),

View File

@@ -0,0 +1,55 @@
# Test Initialize Project Command
## Summary of Changes
### 1. Added `initialize_project` function to EditorFunctions/index.js
- Takes `name` and `example_slug` as arguments
- Returns a special response that signals project initialization is needed
### 2. Updated EditorCallbacks type
- Added optional `onCreateProjectFromExample` callback
### 3. Modified EditorFunctionCallRunner
- Handles the special case when `_requiresProjectInitialization` is true
- Calls the `onCreateProjectFromExample` callback with the project name and example slug
### 4. Updated AskAiEditorContainer
- Removed automatic project creation when no project is open
- Added implementation of `onCreateProjectFromExample` callback that:
- Fetches all examples to find the one matching the slug
- Creates appropriate project setup
- Calls the main `onCreateProjectFromExample` from props
- Shows an alert if agent mode is used without a project
### 5. Updated BaseEditor and EditorTabsPane
- Added `getStorageProvider` to props type
- Passed through to editor containers
## How it Works
1. AI sends `initialize_project` command with project name and example slug
2. The command is processed by EditorFunctionCallRunner
3. Special handling detects the initialization request
4. Calls the callback to create project from example
5. The callback in AskAiEditorContainer:
- Finds the example by slug
- Sets up project configuration
- Triggers actual project creation through MainFrame
## Testing Instructions
To test this implementation:
1. Open GDevelop without any project
2. Use AI chat/agent mode
3. AI should be able to use the `initialize_project` command
4. Command should create a new project based on the specified example
## Example AI Command
```json
{
"name": "initialize_project",
"arguments": "{\"name\": \"My Platformer Game\", \"example_slug\": \"geometry-monster\"}"
}
```