diff --git a/apps/aevatar-console-web/src/pages/studio/components/StudioBuildPanels.tsx b/apps/aevatar-console-web/src/pages/studio/components/StudioBuildPanels.tsx index 4e72ea5f8..3ab9742da 100644 --- a/apps/aevatar-console-web/src/pages/studio/components/StudioBuildPanels.tsx +++ b/apps/aevatar-console-web/src/pages/studio/components/StudioBuildPanels.tsx @@ -1716,6 +1716,19 @@ export type StudioScriptBuildPanelProps = { readonly onRefreshScripts?: () => Promise | unknown; readonly onContinueToBind: () => void; readonly onRegisterLeaveGuard?: (guard: (() => Promise) | null) => void; + readonly onScriptBuildStateChange?: (state: StudioScriptBuildState | null) => void; +}; + +export type StudioScriptBuildState = { + readonly scriptId: string; + readonly displayName: string; + readonly scriptRevision: string; + readonly revisionId?: string; + readonly sourceHash?: string; + readonly definitionActorId?: string; + readonly dirty: boolean; + readonly validationStatus: 'unknown' | 'valid' | 'invalid'; + readonly saveStatus: 'idle' | 'accepted' | 'applied' | 'failed'; }; export const StudioScriptBuildPanel: React.FC = ({ @@ -1726,6 +1739,7 @@ export const StudioScriptBuildPanel: React.FC = ({ onRefreshScripts, onContinueToBind, onRegisterLeaveGuard, + onScriptBuildStateChange, }) => { const [scriptPackage, setScriptPackage] = React.useState(() => deserializePersistedSource(''), @@ -1737,6 +1751,8 @@ export const StudioScriptBuildPanel: React.FC = ({ const [validationError, setValidationError] = React.useState(''); const [savePending, setSavePending] = React.useState(false); const [saveNotice, setSaveNotice] = React.useState(''); + const [saveStatus, setSaveStatus] = + React.useState('idle'); const [runPending, setRunPending] = React.useState(false); const [runInput, setRunInput] = React.useState( JSON.stringify( @@ -1804,6 +1820,7 @@ export const StudioScriptBuildPanel: React.FC = ({ setValidationResult(null); setValidationError(''); setSaveNotice(''); + setSaveStatus('idle'); }, [activeScript?.script?.scriptId, activeScript?.source?.sourceText]); React.useEffect(() => { @@ -1833,6 +1850,84 @@ export const StudioScriptBuildPanel: React.FC = ({ }; }, [isDirty, onRegisterLeaveGuard]); + const validationStatus: StudioScriptBuildState['validationStatus'] = + validationResult + ? validationResult.success + ? 'valid' + : 'invalid' + : validationError + ? 'invalid' + : 'unknown'; + const effectiveSaveStatus: StudioScriptBuildState['saveStatus'] = + isDirty + ? saveStatus === 'failed' + ? 'failed' + : 'idle' + : saveStatus === 'accepted' || saveStatus === 'failed' + ? saveStatus + : activeScript?.script?.activeRevision + ? 'applied' + : saveStatus; + const scriptReadyToBind = Boolean( + activeScript?.script?.scriptId && + !isDirty && + effectiveSaveStatus === 'applied', + ); + const primaryActionLabel = + !activeScript?.script?.scriptId + ? 'Select Script' + : isDirty || validationStatus === 'unknown' || validationStatus === 'invalid' + ? 'Validate' + : effectiveSaveStatus === 'accepted' + ? 'Waiting for catalog' + : effectiveSaveStatus === 'applied' + ? 'Continue to Bind' + : 'Save revision'; + + React.useEffect(() => { + if (!activeScript?.script?.scriptId) { + onScriptBuildStateChange?.(null); + return; + } + + onScriptBuildStateChange?.({ + scriptId: activeScript.script.scriptId, + displayName: activeScript.script.scriptId, + scriptRevision: + activeScript.source?.revision || + activeScript.script.activeRevision || + currentRevision, + revisionId: + activeScript.source?.revision || + activeScript.script.activeRevision || + currentRevision, + sourceHash: + activeScript.source?.sourceHash || + activeScript.script.activeSourceHash || + '', + definitionActorId: + activeScript.source?.definitionActorId || + activeScript.script.definitionActorId || + '', + dirty: isDirty, + validationStatus, + saveStatus: effectiveSaveStatus, + }); + }, [ + activeScript?.script?.activeRevision, + activeScript?.script?.activeSourceHash, + activeScript?.script?.definitionActorId, + activeScript?.script?.scriptId, + activeScript?.source?.definitionActorId, + activeScript?.source?.revision, + activeScript?.source?.sourceHash, + currentRevision, + effectiveSaveStatus, + isDirty, + onScriptBuildStateChange, + validationStatus, + ]); + const resolveLeave = React.useCallback((value: boolean) => { leaveResolverRef.current?.(value); leaveResolverRef.current = null; @@ -1854,13 +1949,16 @@ export const StudioScriptBuildPanel: React.FC = ({ package: scriptPackage, }); setValidationResult(result); + if (result.success && isDirty) { + setSaveStatus('idle'); + } } catch (error) { setValidationError(describeError(error)); setValidationResult(null); } finally { setValidationPending(false); } - }, [activeScript?.script?.scriptId, currentRevision, scriptPackage]); + }, [activeScript?.script?.scriptId, currentRevision, isDirty, scriptPackage]); const handleSave = React.useCallback(async () => { if (!scopeId || !activeScript?.script?.scriptId) { @@ -1877,11 +1975,38 @@ export const StudioScriptBuildPanel: React.FC = ({ expectedBaseRevision: activeScript.script.activeRevision || undefined, sourceText: serializePersistedSource(scriptPackage), }); - await onRefreshScripts?.(); - setSaveNotice( - `Save accepted for ${accepted.acceptedScript.scriptId} · revision ${accepted.acceptedScript.revisionId}.`, + setSaveStatus('accepted'); + const observation = await scriptsApi.observeSaveScript( + scopeId, + activeScript.script.scriptId, + { + revisionId: accepted.acceptedScript.revisionId, + definitionActorId: accepted.acceptedScript.definitionActorId, + sourceHash: accepted.acceptedScript.sourceHash, + proposalId: accepted.acceptedScript.proposalId, + expectedBaseRevision: accepted.acceptedScript.expectedBaseRevision, + acceptedAt: accepted.acceptedScript.acceptedAt, + }, ); + await onRefreshScripts?.(); + if (observation.status === 'applied') { + setSaveStatus('applied'); + setSaveNotice( + `Save applied for ${accepted.acceptedScript.scriptId} · revision ${accepted.acceptedScript.revisionId}.`, + ); + } else if (observation.status === 'rejected') { + setSaveStatus('failed'); + setSaveNotice( + observation.message || + `Save rejected for ${accepted.acceptedScript.scriptId} · revision ${accepted.acceptedScript.revisionId}.`, + ); + } else { + setSaveNotice( + `Save accepted for ${accepted.acceptedScript.scriptId} · revision ${accepted.acceptedScript.revisionId}. Waiting for catalog.`, + ); + } } catch (error) { + setSaveStatus('failed'); setSaveNotice(describeError(error)); } finally { setSavePending(false); @@ -1966,6 +2091,9 @@ export const StudioScriptBuildPanel: React.FC = ({
lints · partial + + {primaryActionLabel} + setCreateMemberName(event.target.value)} - placeholder={suggestedCreateWorkflowName} - style={inventoryCreateInputStyle} - type="text" - value={createMemberName} - /> - + {createMemberKind === 'workflow' ? ( + + ) : null}
{createMemberKind === 'workflow' ? 'Workflow members currently start from a blank workflow draft with an empty canvas, so you can name it first and then continue editing inside Build.' : createMemberKind === 'script' - ? 'Script member creation still relies on the upcoming member API. For now, continue in Build > Script to inspect or edit script implementations.' - : 'GAgent member creation still relies on the upcoming member API. For now, continue in Build > GAgent to inspect or edit GAgent implementations.'} + ? 'Script member creation still relies on the upcoming member API. Open Build > Script to inspect, edit, save, and prepare script implementations for binding.' + : 'GAgent member creation still relies on the upcoming member API. Open Build > GAgent to select, inspect, and prepare GAgent implementations for binding.'}
{createMemberKind === 'workflow' ? (