@@ -8,9 +8,10 @@ import { useTextTo3dPreview } from '../funnel/hooks/useTextTo3dPreview';
88import { inAppAgentEnabled } from './agentAvailability' ;
99import { ConceptResult } from './components/ConceptResult' ;
1010import { useCode } from './context/CodeContext' ;
11- import { useGeometry } from './context/GeometryContext' ;
1211import { useFeatureSelection } from './hooks/useFeatureSelection' ;
1312import { useShellStore , shellStore } from './store/useShellStore' ;
13+ import type { AgentRepairWorkflow } from './store/shellStore' ;
14+ import type { SelectedFeatureId } from './types' ;
1415
1516/** Web-only gate. No hooks here, so the conditional return is safe. */
1617export const StudioGenerate : React . FC = ( ) => {
@@ -32,21 +33,28 @@ function stepLabel(e: GenerateEvent): string | null {
3233 }
3334}
3435
36+ interface GenerationReviewSnapshot {
37+ readonly fromCode : string ;
38+ readonly promptText : string ;
39+ readonly selectedFeatureId : SelectedFeatureId ;
40+ readonly repairWorkflow : AgentRepairWorkflow | null ;
41+ }
42+
3543const StudioGenerateInner : React . FC = ( ) => {
3644 const { phase, events, submit } = useGeneration ( ) ;
37- const { code, setCode } = useCode ( ) ;
45+ const { code } = useCode ( ) ;
3846 const currentCode = code ?? '' ;
39- const { executeGeometry } = useGeometry ( ) ;
4047 const { selectedFeatureId } = useFeatureSelection ( ) ;
4148 const { agentDraftPrompt, agentDraftPromptVersion, agentRepairWorkflow } = useShellStore ( ) ;
4249 const [ editedPrompt , setEditedPrompt ] = useState ( '' ) ;
4350 const [ acknowledgedDraftVersion , setAcknowledgedDraftVersion ] = useState ( - 1 ) ;
44- // The generationId we've already accepted /rejected — gates the review panel
45- // so an applied/dismissed proposal doesn't reappear.
46- const [ resolvedId , setResolvedId ] = useState < string | null > ( null ) ;
51+ // The generationId we've already staged /rejected — gates the review panel
52+ // so a resolved proposal doesn't reappear.
53+ const [ resolution , setResolution ] = useState < { generationId : string ; action : 'staged' | 'discarded' } | null > ( null ) ;
4754 // The editor source captured at submit time — the "before" side of the diff
4855 // (so the diff is stable even though `code` changes once we apply).
4956 const [ baseline , setBaseline ] = useState ( '' ) ;
57+ const [ reviewSnapshot , setReviewSnapshot ] = useState < GenerationReviewSnapshot | null > ( null ) ;
5058
5159 const prompt =
5260 agentDraftPrompt !== null && agentDraftPromptVersion !== acknowledgedDraftVersion
@@ -69,7 +77,7 @@ const StudioGenerateInner: React.FC = () => {
6977 // One operation at a time: the rail is too narrow to narrate two runs.
7078 const busy = agentBusy || conceptBusy ;
7179 // A finished, not-yet-resolved proposal → show the review (diff + accept/reject).
72- const reviewing = phase . state === 'done' && resolvedId !== phase . generationId ;
80+ const reviewing = phase . state === 'done' && resolution ?. generationId !== phase . generationId ;
7381
7482 const steps = useMemo ( ( ) => events . map ( stepLabel ) . filter ( Boolean ) as string [ ] , [ events ] ) ;
7583
@@ -78,12 +86,13 @@ const StudioGenerateInner: React.FC = () => {
7886 shellStore . setAgentRepairWorkflow ( { ...agentRepairWorkflow , state : 'drafted' } ) ;
7987 } , [ agentRepairWorkflow , phase . state ] ) ;
8088
81- const runAgent = ( text : string ) => {
89+ const runAgent = ( text : string , snapshot : GenerationReviewSnapshot ) => {
8290 // Edit mode: hand the agent the current model so it iterates instead of
8391 // replacing. Empty editor → fresh generation.
84- setBaseline ( currentCode ) ;
85- if ( currentCode . trim ( ) ) {
86- void submit ( text , currentCode ) ;
92+ setBaseline ( snapshot . fromCode ) ;
93+ setReviewSnapshot ( snapshot ) ;
94+ if ( snapshot . fromCode . trim ( ) ) {
95+ void submit ( text , snapshot . fromCode ) ;
8796 return ;
8897 }
8998 void submit ( text ) ;
@@ -94,15 +103,22 @@ const StudioGenerateInner: React.FC = () => {
94103 const trimmed = prompt . trim ( ) ;
95104 if ( ! trimmed || busy ) return ;
96105 const agentPrompt = selectedFeatureId === null ? trimmed : `Edit selected target "${ selectedFeatureId } ": ${ trimmed } ` ;
106+ let repairWorkflowForRun = agentRepairWorkflow ;
97107 if (
98108 agentRepairWorkflow != null &&
99109 agentRepairWorkflow . state === 'drafted' &&
100110 agentRepairWorkflow . promptText === trimmed &&
101111 agentRepairWorkflow . targetId === selectedFeatureId
102112 ) {
103- shellStore . setAgentRepairWorkflow ( { ...agentRepairWorkflow , state : 'running' } ) ;
113+ repairWorkflowForRun = { ...agentRepairWorkflow , state : 'running' } ;
114+ shellStore . setAgentRepairWorkflow ( repairWorkflowForRun ) ;
104115 }
105- runAgent ( agentPrompt ) ;
116+ runAgent ( agentPrompt , {
117+ fromCode : currentCode ,
118+ promptText : trimmed ,
119+ selectedFeatureId,
120+ repairWorkflow : repairWorkflowForRun ,
121+ } ) ;
106122 } ;
107123
108124 const onConcept = ( ) => {
@@ -120,6 +136,12 @@ const StudioGenerateInner: React.FC = () => {
120136 // review diff still uses the current editor code as its baseline, so
121137 // nothing is overwritten without the user accepting.
122138 setBaseline ( currentCode ) ;
139+ setReviewSnapshot ( {
140+ fromCode : currentCode ,
141+ promptText : conceptPrompt ,
142+ selectedFeatureId,
143+ repairWorkflow : agentRepairWorkflow ,
144+ } ) ;
123145 // Read the concept mesh directly from the live preview phase (no mirrored
124146 // state). A done preview with no Tripo render/fingerprint yields
125147 // {renderImageUrl:null, proportions:null} — intentional and distinct from
@@ -130,19 +152,34 @@ const StudioGenerateInner: React.FC = () => {
130152 void submit ( conceptPrompt , undefined , mesh ) ;
131153 } ;
132154
133- const accept = ( ) => {
155+ const stageGeneratedEdit = ( ) => {
134156 if ( phase . state !== 'done' ) return ;
135- setCode ( phase . artifact . code ) ;
136- void executeGeometry ( phase . artifact . code ) ;
137- setResolvedId ( phase . generationId ) ;
157+ const snapshot = reviewSnapshot ?? {
158+ fromCode : currentCode ,
159+ promptText : prompt . trim ( ) ,
160+ selectedFeatureId,
161+ repairWorkflow : agentRepairWorkflow ,
162+ } ;
163+ shellStore . proposeStagedEdit ( {
164+ id : `agent:${ phase . generationId } ` ,
165+ intent : phase . artifact . title ,
166+ fromCode : snapshot . fromCode ,
167+ toCode : phase . artifact . code ,
168+ source : { kind : 'agent' , label : 'Studio Generate' } ,
169+ context : {
170+ promptText : snapshot . promptText ,
171+ selectedFeatureId : snapshot . selectedFeatureId ,
172+ repairWorkflow : snapshot . repairWorkflow ,
173+ generationId : phase . generationId ,
174+ } ,
175+ } ) ;
176+ setResolution ( { generationId : phase . generationId , action : 'staged' } ) ;
138177 } ;
139178 const reject = ( ) => {
140179 if ( phase . state !== 'done' ) return ;
141- setResolvedId ( phase . generationId ) ;
180+ setResolution ( { generationId : phase . generationId , action : 'discarded' } ) ;
142181 } ;
143182
144- const isEdit = baseline . trim ( ) . length > 0 ;
145-
146183 return (
147184 < div className = "p-3 flex flex-col gap-2" >
148185 < div className = "uppercase tracking-wide text-[10px] text-gray-500" > Agent</ div >
@@ -222,10 +259,10 @@ const StudioGenerateInner: React.FC = () => {
222259 < div className = "flex gap-2" >
223260 < button
224261 type = "button"
225- onClick = { accept }
262+ onClick = { stageGeneratedEdit }
226263 className = "flex-1 rounded bg-green-600 hover:bg-green-500 text-white px-3 py-1.5 text-[11px] font-medium transition-colors"
227264 >
228- { isEdit ? 'Apply changes' : 'Use this' }
265+ Stage edit
229266 </ button >
230267 < button
231268 type = "button"
@@ -238,9 +275,14 @@ const StudioGenerateInner: React.FC = () => {
238275 </ div >
239276 ) }
240277
241- { phase . state === 'done' && ! reviewing && (
278+ { phase . state === 'done' && ! reviewing && resolution ?. action === 'staged' && (
242279 < div className = "text-[10px] text-green-500 truncate" aria-live = "polite" >
243- ✓ applied — { phase . artifact . title }
280+ ✓ staged for review — { phase . artifact . title }
281+ </ div >
282+ ) }
283+ { phase . state === 'done' && ! reviewing && resolution ?. action === 'discarded' && (
284+ < div className = "text-[10px] text-gray-500 truncate" aria-live = "polite" >
285+ discarded — { phase . artifact . title }
244286 </ div >
245287 ) }
246288 { phase . state === 'error' && (
0 commit comments