feat(editor): add Run help hint and rename from project list

Show a help icon when Run is disabled, open the relevant instruction section
on click, and position its tooltip below-left. Add Rename project to the home
screen project card menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-03 22:45:19 +08:00
parent 10bb7013e6
commit de9190959c
5 changed files with 133 additions and 43 deletions
+78 -25
View File
@@ -34,6 +34,7 @@ import {
type SceneGraphSceneCard,
type SceneGraphUiStrings,
} from './graph/SceneGraph';
import type { HelpSectionId } from './help/helpSections';
import { useEditorI18n } from './i18n/EditorI18nContext';
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
import type { ProjectNoticeCode } from './state/projectState';
@@ -88,6 +89,7 @@ export function EditorApp() {
const [aboutMenuOpen, setAboutMenuOpen] = useState(false);
const [appAboutOpen, setAppAboutOpen] = useState(false);
const [instructionsOpen, setInstructionsOpen] = useState(false);
const [instructionsSection, setInstructionsSection] = useState<HelpSectionId>('overview');
const [renameOpen, setRenameOpen] = useState(false);
const [exportModalOpen, setExportModalOpen] = useState(false);
const [previewDialogSceneId, setPreviewDialogSceneId] = useState<SceneId | null>(null);
@@ -112,6 +114,7 @@ export function EditorApp() {
[t],
);
const [state, actions] = useProjectState(licenseActive, { onNotice: onProjectNotice });
const renameFromPickerRef = useRef(false);
const sceneCardById = useStableSceneCardById(state.project);
const graphUi = useMemo<SceneGraphUiStrings>(
() => ({
@@ -200,12 +203,6 @@ export function EditorApp() {
return campaignAudioRefs.map((r) => p.assets[r.assetId]).filter((a): a is MediaAsset => Boolean(a));
}, [campaignAudioRefs, state.project]);
const graphStartSceneId = useMemo(() => {
const p = state.project;
if (!p) return null;
const gn = p.sceneGraphNodes.find((n) => n.isStartScene);
return gn?.sceneId ?? null;
}, [state.project]);
const graphStartGraphNodeId = useMemo(() => {
const p = state.project;
if (!p) return null;
@@ -213,6 +210,10 @@ export function EditorApp() {
return gn?.id ?? null;
}, [state.project]);
const runDisabled = !licenseActive || !graphStartGraphNodeId;
const runHelpSection: HelpSectionId = !licenseActive ? 'license' : 'graph';
const runHelpTooltip = !licenseActive ? t('top.afterLicense') : t('top.setStartScene');
const launchFromGraphNode = useCallback(
(graphNodeId: GraphNodeId) => {
if (!licenseActive) return;
@@ -509,23 +510,33 @@ export function EditorApp() {
) : null}
<div className={styles.headerActions}>
{state.project ? (
<Button
variant="primary"
disabled={!licenseActive || !graphStartGraphNodeId}
title={
!licenseActive
? t('top.afterLicense')
: graphStartSceneId
? undefined
: t('top.setStartScene')
}
onClick={() => {
if (!licenseActive || !graphStartGraphNodeId) return;
launchFromGraphNode(graphStartGraphNodeId);
}}
>
{t('top.run')}
</Button>
<>
<Button
variant="primary"
disabled={runDisabled}
onClick={() => {
if (!licenseActive || !graphStartGraphNodeId) return;
launchFromGraphNode(graphStartGraphNodeId);
}}
>
{t('top.run')}
</Button>
{runDisabled ? (
<Button
variant="ghost"
iconOnly
ariaLabel={t('top.runHelpAria')}
title={runHelpTooltip}
tooltipPlacement="bottom-left"
onClick={() => {
setInstructionsSection(runHelpSection);
setInstructionsOpen(true);
}}
>
?
</Button>
) : null}
</>
) : null}
</div>
</div>
@@ -565,6 +576,21 @@ export function EditorApp() {
openingProjectId={state.openingProjectId}
onCreate={actions.createProject}
onOpen={actions.openProject}
onRename={(id) => {
renameFromPickerRef.current = true;
void (async () => {
try {
await actions.openProject(id);
setRenameOpen(true);
} catch (e) {
renameFromPickerRef.current = false;
setAppNotice({
title: t('common.error'),
message: e instanceof Error ? e.message : String(e),
});
}
})();
}}
onDelete={actions.deleteProject}
/>
)}
@@ -831,7 +857,11 @@ export function EditorApp() {
snapshot={licenseSnap}
/>
<AppAboutModal open={appAboutOpen} onClose={() => setAppAboutOpen(false)} appVersion={appVersionText} />
<InstructionsModal open={instructionsOpen} onClose={() => setInstructionsOpen(false)} />
<InstructionsModal
open={instructionsOpen}
initialSection={instructionsSection}
onClose={() => setInstructionsOpen(false)}
/>
{aboutMenuOpen && aboutMenuPos
? createPortal(
<div
@@ -857,6 +887,7 @@ export function EditorApp() {
className={styles.fileMenuItem}
onClick={() => {
setAboutMenuOpen(false);
setInstructionsSection('overview');
setInstructionsOpen(true);
}}
>
@@ -943,7 +974,13 @@ export function EditorApp() {
fileBaseNameInitial={currentFileBaseName}
existingProjectNames={existingProjectNames}
existingFileBaseNames={existingFileBaseNames}
onClose={() => setRenameOpen(false)}
onClose={() => {
setRenameOpen(false);
if (renameFromPickerRef.current) {
renameFromPickerRef.current = false;
void actions.closeProject();
}
}}
onSave={async (name, fileBaseName) => {
await actions.renameProject(name, fileBaseName);
}}
@@ -1538,6 +1575,7 @@ type ProjectPickerProps = {
openingProjectId: ProjectId | null;
onCreate: (name: string) => Promise<void>;
onOpen: (id: ProjectId) => Promise<void>;
onRename: (id: ProjectId) => void;
onDelete: (id: ProjectId) => Promise<void>;
};
@@ -1547,6 +1585,7 @@ function ProjectPicker({
openingProjectId,
onCreate,
onOpen,
onRename,
onDelete,
}: ProjectPickerProps) {
const { t, locale } = useEditorI18n();
@@ -1679,6 +1718,20 @@ function ProjectPicker({
className={styles.fileMenu}
style={{ left: rowMenuPos.left, top: rowMenuPos.top }}
>
<button
type="button"
role="menuitem"
className={styles.fileMenuItem}
onClick={() => {
const id = rowMenuFor;
setRowMenuFor(null);
setRowMenuPos(null);
if (!id) return;
onRename(id);
}}
>
{t('fileMenu.rename')}
</button>
<button
type="button"
role="menuitem"