From bf439dd1b514a66575b27ce848e57afc6f21f002 Mon Sep 17 00:00:00 2001 From: IAnMove <216241348+IAnMove@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:46:25 +0200 Subject: [PATCH 1/3] fix: let genera el video queue the prepared Studio form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare “hazme un video” still asks for a topic. “genera el video” reuses the current Studio prompt and emits prepare+start in the same turn so the runner will actually enqueue. --- ui/src/features/agent/agentActions.ts | 35 +++++++++++++++++++++++++++ ui/tests/agentActions.test.mjs | 18 ++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/ui/src/features/agent/agentActions.ts b/ui/src/features/agent/agentActions.ts index afcee475..0f27a4dc 100644 --- a/ui/src/features/agent/agentActions.ts +++ b/ui/src/features/agent/agentActions.ts @@ -1697,6 +1697,16 @@ export function isExplicitVideoGenerationRequest(request: string): boolean { return EXPLICIT_VIDEO_REQUESTS.some(pattern => pattern.test(text)) } +const RESUME_PREPARED_VIDEO_REQUESTS = [ + /\b(?:genera|generad|lanza|lanzad|encola|encolad|env[ií]a|enviad|start|queue|launch)\b[^.!?\n]{0,24}\b(?:el|la|este|esta|the)\s+(?:video|v[ií]deo|clip)\b/i, +] + +export function isResumePreparedStudioVideoRequest(request: string): boolean { + const text = request.trim() + if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false + return RESUME_PREPARED_VIDEO_REQUESTS.some(pattern => pattern.test(text)) +} + const EXPLICIT_IMAGE_REQUESTS = [ /\b(?:hazme|hacedme|generame|genérame|creame|créame)\b[^.!?\n]*\b(?:imagen|im[aá]genes|foto|fotos|retrato|ilustraci[oó]n)\b/i, /\b(?:haz|haced|genera|generad|crea|cread|lanza|lanzad|encola|encolad)\b[^.!?\n]*\b(?:imagen|im[aá]genes|foto|fotos|retrato|ilustraci[oó]n)\b/i, @@ -1843,6 +1853,31 @@ export async function reconcileAgentTurnWithRequest( history: ExampleConversation[] = [], ): Promise { const { maybeExampleTurn } = await import('./agentExamples') + if (isResumePreparedStudioVideoRequest(request)) { + const existing = turn.actions.find( + (action): action is AgentPrepareVideoAction => action.type === 'prepare_video', + ) + const state = useStore.getState() + const prompt = existing?.prompt.trim() || String(state.params.prompt || '').trim() + if (!prompt) { + return { + reply: 'Studio no tiene un prompt preparado. Dime la escena y la encolo.', + actions: [{ type: 'open_tab', tab: 'studio' }], + } + } + const prepare: AgentPrepareVideoAction = existing || { + type: 'prepare_video', + prompt, + modelType: typeof state.params.model_type === 'string' ? state.params.model_type : undefined, + durationSeconds: state.durationSeconds, + resolutionPreset: state.resolutionPreset, + aspectRatio: state.aspectRatio, + } + return { + reply: 'Lanzo a la cola el vídeo ya preparado en Studio. 🪄', + actions: [prepare, { type: 'start_generation', confirm: true }], + } + } const exampleTurn = maybeExampleTurn(request, turn, history) if (exampleTurn) return exampleTurn const rhythmic3dWorkflow = turn.actions.find( diff --git a/ui/tests/agentActions.test.mjs b/ui/tests/agentActions.test.mjs index 8d6934d1..168d83fe 100644 --- a/ui/tests/agentActions.test.mjs +++ b/ui/tests/agentActions.test.mjs @@ -1447,3 +1447,21 @@ test('a video example fills a real prompt instead of asking, and a topical video assert.deepEqual(topical.actions.map(action => action.type), ['prepare_video', 'start_generation']) assert.ok(topical.actions[0].prompt.includes('mapache')) }) + +test('genera el video reuses a prepared Studio prompt instead of asking for a topic', async () => { + const { useStore } = await import('../src/stores/useStore.ts') + const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts') + useStore.setState({ + params: { ...useStore.getState().params, prompt: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' }, + durationSeconds: 5.2, + }) + const resumed = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) + assert.deepEqual(resumed.actions.map(action => action.type), ['prepare_video', 'start_generation']) + assert.equal(resumed.actions[0].prompt.includes('Sheldon'), true) + assert.equal(resumed.actions[1].confirm, true) + + useStore.setState({ params: { ...useStore.getState().params, prompt: '' } }) + const asked = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) + assert.equal(asked.actions[0].type, 'open_tab') + assert.equal(asked.actions.some(action => action.type === 'start_generation'), false) +}) From b1e9b66a294b329cb7639ad1918b62588cf6f7c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 10:57:50 +0000 Subject: [PATCH 2/3] fix: keep topical video requests off the Studio resume path Bare "genera el video" still queues the prepared form, but leftover subject text now falls through to example/topical generation. Resume also omits the current model id and reads the saved Video prompt after a mode switch so I2V or Image leftovers do not fail prepareVideo. --- ui/src/features/agent/agentActions.ts | 29 +++++++++++++-------- ui/tests/agentActions.test.mjs | 36 ++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/ui/src/features/agent/agentActions.ts b/ui/src/features/agent/agentActions.ts index 0f27a4dc..571f47d6 100644 --- a/ui/src/features/agent/agentActions.ts +++ b/ui/src/features/agent/agentActions.ts @@ -1697,14 +1697,21 @@ export function isExplicitVideoGenerationRequest(request: string): boolean { return EXPLICIT_VIDEO_REQUESTS.some(pattern => pattern.test(text)) } -const RESUME_PREPARED_VIDEO_REQUESTS = [ - /\b(?:genera|generad|lanza|lanzad|encola|encolad|env[ií]a|enviad|start|queue|launch)\b[^.!?\n]{0,24}\b(?:el|la|este|esta|the)\s+(?:video|v[ií]deo|clip)\b/i, -] - export function isResumePreparedStudioVideoRequest(request: string): boolean { const text = request.trim() if (!text || NEGATED_VIDEO_REQUEST.test(text)) return false - return RESUME_PREPARED_VIDEO_REQUESTS.some(pattern => pattern.test(text)) + const match = text.match( + /\b(?:genera|generad|lanza|lanzad|encola|encolad|env[ií]a|enviad|start|queue|launch)\b[^.!?\n]{0,24}\b(?:el|la|este|esta|the)\s+(?:video|v[ií]deo|clip)\b(.*)$/i, + ) + if (!match) return false + const leftover = match[1] + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[¿?¡!.,:;]/g, ' ') + .replace(/\b(?:por favor|please|ya|ahora|now)\b/g, ' ') + .replace(/\s+/g, '') + return leftover.length === 0 } const EXPLICIT_IMAGE_REQUESTS = [ @@ -1858,7 +1865,10 @@ export async function reconcileAgentTurnWithRequest( (action): action is AgentPrepareVideoAction => action.type === 'prepare_video', ) const state = useStore.getState() - const prompt = existing?.prompt.trim() || String(state.params.prompt || '').trim() + const studioPrompt = state.generationMode === 'video' + ? String(state.params.prompt || '').trim() + : String(state.savedPromptPerMode?.video || '').trim() + const prompt = existing?.prompt.trim() || studioPrompt if (!prompt) { return { reply: 'Studio no tiene un prompt preparado. Dime la escena y la encolo.', @@ -1868,10 +1878,9 @@ export async function reconcileAgentTurnWithRequest( const prepare: AgentPrepareVideoAction = existing || { type: 'prepare_video', prompt, - modelType: typeof state.params.model_type === 'string' ? state.params.model_type : undefined, - durationSeconds: state.durationSeconds, - resolutionPreset: state.resolutionPreset, - aspectRatio: state.aspectRatio, + durationSeconds: state.generationMode === 'video' ? state.durationSeconds : undefined, + resolutionPreset: state.generationMode === 'video' ? state.resolutionPreset : undefined, + aspectRatio: state.generationMode === 'video' ? state.aspectRatio : undefined, } return { reply: 'Lanzo a la cola el vídeo ya preparado en Studio. 🪄', diff --git a/ui/tests/agentActions.test.mjs b/ui/tests/agentActions.test.mjs index 168d83fe..07ae030f 100644 --- a/ui/tests/agentActions.test.mjs +++ b/ui/tests/agentActions.test.mjs @@ -1451,17 +1451,51 @@ test('a video example fills a real prompt instead of asking, and a topical video test('genera el video reuses a prepared Studio prompt instead of asking for a topic', async () => { const { useStore } = await import('../src/stores/useStore.ts') const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts') + const original = { + generationMode: useStore.getState().generationMode, + params: useStore.getState().params, + durationSeconds: useStore.getState().durationSeconds, + savedPromptPerMode: useStore.getState().savedPromptPerMode, + } useStore.setState({ + generationMode: 'video', params: { ...useStore.getState().params, prompt: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' }, durationSeconds: 5.2, }) const resumed = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) assert.deepEqual(resumed.actions.map(action => action.type), ['prepare_video', 'start_generation']) assert.equal(resumed.actions[0].prompt.includes('Sheldon'), true) + assert.equal(resumed.actions[0].modelType, undefined) assert.equal(resumed.actions[1].confirm, true) - useStore.setState({ params: { ...useStore.getState().params, prompt: '' } }) + const topical = await reconcileAgentTurnWithRequest('genera el video de un mapache', { reply: '¿De qué?', actions: [] }) + assert.deepEqual(topical.actions.map(action => action.type), ['prepare_video', 'start_generation']) + assert.ok(topical.actions[0].prompt.includes('mapache')) + assert.equal(topical.actions[0].prompt.includes('Sheldon'), false) + + const example = await reconcileAgentTurnWithRequest('genera el video de ejemplo', { reply: '¿De qué?', actions: [] }) + assert.deepEqual(example.actions.map(action => action.type), ['prepare_video', 'start_generation']) + assert.ok(example.actions[0].prompt.length > 40) + assert.equal(example.actions[0].prompt.includes('genera el video'), false) + assert.equal(example.actions[0].prompt.includes('Sheldon'), false) + + useStore.setState({ + generationMode: 'image', + params: { ...useStore.getState().params, model_type: 'image-only-model', prompt: 'un retrato de estudio' }, + savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' }, + }) + const afterModeSwitch = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) + assert.deepEqual(afterModeSwitch.actions.map(action => action.type), ['prepare_video', 'start_generation']) + assert.equal(afterModeSwitch.actions[0].prompt.includes('Sheldon'), true) + assert.equal(afterModeSwitch.actions[0].modelType, undefined) + + useStore.setState({ + generationMode: 'video', + params: { ...useStore.getState().params, prompt: '' }, + savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: '' }, + }) const asked = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) assert.equal(asked.actions[0].type, 'open_tab') assert.equal(asked.actions.some(action => action.type === 'start_generation'), false) + useStore.setState(original) }) From 37bb43b72d9842797692512f2198c882494dae7f Mon Sep 17 00:00:00 2001 From: IAnMove <216241348+IAnMove@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:01:11 +0200 Subject: [PATCH 3/3] fix: keep topical video requests off the Studio resume path Cherry-pick Cursor Autofix, then split the tests: resume the prepared form, use a new topic when one is named, and never copy an I2V/audio/3D model as T2V. --- ui/tests/agentActions.test.mjs | 69 +++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/ui/tests/agentActions.test.mjs b/ui/tests/agentActions.test.mjs index 07ae030f..273e6c3b 100644 --- a/ui/tests/agentActions.test.mjs +++ b/ui/tests/agentActions.test.mjs @@ -1468,6 +1468,29 @@ test('genera el video reuses a prepared Studio prompt instead of asking for a to assert.equal(resumed.actions[0].modelType, undefined) assert.equal(resumed.actions[1].confirm, true) + useStore.setState({ + generationMode: 'video', + params: { ...useStore.getState().params, prompt: '' }, + savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: '' }, + }) + const asked = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) + assert.equal(asked.actions[0].type, 'open_tab') + assert.equal(asked.actions.some(action => action.type === 'start_generation'), false) + useStore.setState(original) +}) + +test('genera el video de un mapache uses the new topic instead of the prepared form', async () => { + const { useStore } = await import('../src/stores/useStore.ts') + const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts') + const original = { + generationMode: useStore.getState().generationMode, + params: useStore.getState().params, + savedPromptPerMode: useStore.getState().savedPromptPerMode, + } + useStore.setState({ + generationMode: 'video', + params: { ...useStore.getState().params, prompt: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' }, + }) const topical = await reconcileAgentTurnWithRequest('genera el video de un mapache', { reply: '¿De qué?', actions: [] }) assert.deepEqual(topical.actions.map(action => action.type), ['prepare_video', 'start_generation']) assert.ok(topical.actions[0].prompt.includes('mapache')) @@ -1478,24 +1501,34 @@ test('genera el video reuses a prepared Studio prompt instead of asking for a to assert.ok(example.actions[0].prompt.length > 40) assert.equal(example.actions[0].prompt.includes('genera el video'), false) assert.equal(example.actions[0].prompt.includes('Sheldon'), false) + useStore.setState(original) +}) - useStore.setState({ - generationMode: 'image', - params: { ...useStore.getState().params, model_type: 'image-only-model', prompt: 'un retrato de estudio' }, - savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' }, - }) - const afterModeSwitch = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) - assert.deepEqual(afterModeSwitch.actions.map(action => action.type), ['prepare_video', 'start_generation']) - assert.equal(afterModeSwitch.actions[0].prompt.includes('Sheldon'), true) - assert.equal(afterModeSwitch.actions[0].modelType, undefined) - - useStore.setState({ - generationMode: 'video', - params: { ...useStore.getState().params, prompt: '' }, - savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: '' }, - }) - const asked = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) - assert.equal(asked.actions[0].type, 'open_tab') - assert.equal(asked.actions.some(action => action.type === 'start_generation'), false) +test('genera el video does not copy an incompatible I2V, audio or 3D model as T2V', async () => { + const { useStore } = await import('../src/stores/useStore.ts') + const { reconcileAgentTurnWithRequest } = await import('../src/features/agent/agentActions.ts') + const original = { + generationMode: useStore.getState().generationMode, + params: useStore.getState().params, + savedPromptPerMode: useStore.getState().savedPromptPerMode, + } + const prepared = 'Sheldon Cooper cuenta un chiste en su salón, bata verde, Bazinga.' + + for (const [generationMode, modelType] of [ + ['image', 'image-only-model'], + ['audio', 'ace_step_music'], + ['model3d', 'hunyuan3d'], + ['video', 'i2v-only-model'], + ]) { + useStore.setState({ + generationMode, + params: { ...useStore.getState().params, model_type: modelType, prompt: generationMode === 'video' ? prepared : 'otro modo' }, + savedPromptPerMode: { ...useStore.getState().savedPromptPerMode, video: prepared }, + }) + const turn = await reconcileAgentTurnWithRequest('genera el video', { reply: '¿De qué?', actions: [] }) + assert.deepEqual(turn.actions.map(action => action.type), ['prepare_video', 'start_generation']) + assert.equal(turn.actions[0].modelType, undefined, `${generationMode}/${modelType} must not be copied as T2V`) + assert.equal(turn.actions[0].prompt.includes('Sheldon'), true) + } useStore.setState(original) })