{
  "id": "google-flow-edit-image",
  "name": "Google Flow — Edit Image",
  "description": "Open a Flow project, switch to image assets, open a matching image edit URL, and apply a prompt-based image edit through Flow's internal Slate editor and onSubmit handler. PROMPT mode is fully automated. CROP, SELECT, and DRAW can also be staged, but they usually still need drag or region input unless {AUTO_SUBMIT} is set to 1.",
  "platform": "google-flow",
  "version": "1.0.0",
  "urlPatterns": [
    "*labs.google/fx/tools/flow*"
  ],
  "tags": [
    "google-flow",
    "browser",
    "cdp",
    "image",
    "edit"
  ],
  "successCount": 0,
  "failCount": 0,
  "steps": [
    {
      "action": "navigate",
      "url": "https://labs.google/fx/tools/flow",
      "description": "Open the Flow dashboard"
    },
    {
      "action": "wait",
      "ms": 1500,
      "description": "Wait for dashboard project cards"
    },
    {
      "action": "browser_js",
      "code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawWanted = `{PROJECT_NAME}`.trim(); const wanted = rawWanted.toLowerCase(); const hasWanted = !!rawWanted && rawWanted !== '{PROJECT_NAME}'; const links = [...document.querySelectorAll(\"a[href*='/fx/tools/flow/project/']\")].filter(visible); if (!links.length) throw new Error('No Flow project cards found'); const pick = hasWanted ? links.find(link => ((link.closest('div')?.textContent || link.parentElement?.textContent || '')).toLowerCase().includes(wanted)) : links[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error(hasWanted ? `Flow project not found: ${rawWanted}` : 'No clickable Flow project found'); const url = pick.href || pick.getAttribute('href'); if (!url) throw new Error('Project card is missing href'); window.location.assign(url); return url; })()",
      "description": "Open the matching project card",
      "verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
      "verifyTimeoutMs": 15000
    },
    {
      "action": "wait",
      "ms": 800,
      "description": "Allow the project editor to settle"
    },
    {
      "action": "browser_js",
      "code": "(() => { const button = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('View images')); if (button instanceof HTMLElement) button.click(); return !!button; })()",
      "description": "Switch the asset rail to images when the filter is present"
    },
    {
      "action": "wait",
      "ms": 400,
      "description": "Wait for the image grid filter to apply"
    },
    {
      "action": "browser_js",
      "code": "(() => { const visible = el => { const r = el.getBoundingClientRect(); const s = getComputedStyle(el); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }; const rawUrl = `{ASSET_URL}`.trim(); const rawId = `{ASSET_ID}`.trim(); const rawIndex = `{ASSET_INDEX}`.trim(); const wantedIndex = rawIndex && rawIndex !== '{ASSET_INDEX}' ? Number(rawIndex) : 0; const anchors = [...document.querySelectorAll(\"a[href*='/edit/']\")].filter(visible).filter(anchor => { const tile = anchor.closest(\"[id^='fe_id_']\") || anchor.parentElement; return !tile?.querySelector('video'); }); let pick = null; if (rawUrl && rawUrl !== '{ASSET_URL}') pick = anchors.find(anchor => anchor.href === rawUrl); else if (rawId && rawId !== '{ASSET_ID}') pick = anchors.find(anchor => anchor.href.includes(`/edit/${rawId}`) || anchor.closest(`[id='fe_id_${rawId}']`)); else pick = anchors[Math.max(0, wantedIndex)] || anchors[0]; if (!(pick instanceof HTMLAnchorElement)) throw new Error('No Flow image edit link found'); window.location.assign(pick.href); return pick.href; })()",
      "description": "Open the requested image asset in edit view",
      "verify": "[role='textbox'][contenteditable='true'][data-slate-editor='true']",
      "verifyTimeoutMs": 15000
    },
    {
      "action": "wait",
      "ms": 600,
      "description": "Allow the image editor to settle"
    },
    {
      "action": "browser_js",
      "code": "(() => { const modeRaw = `{EDIT_MODE}`.trim().toUpperCase(); const mode = !modeRaw || modeRaw === '{EDIT_MODE}' ? 'PROMPT' : modeRaw; const autoSubmit = `{AUTO_SUBMIT}`.trim() === '1'; const buttonText = { CROP: 'Crop', SELECT: 'Select', DRAW: 'Draw' }; const clickButton = text => { const button = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes(text)); if (!(button instanceof HTMLElement)) throw new Error(`Flow image tool not found: ${text}`); button.click(); return text; }; let selectedTool = 'PROMPT'; if (buttonText[mode]) selectedTool = clickButton(buttonText[mode]); if (mode === 'CROP') { const aspectRaw = `{CROP_ASPECT}`.trim(); const aspect = !aspectRaw || aspectRaw === '{CROP_ASPECT}' ? '' : aspectRaw; if (aspect) { const aspectButton = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes(aspect)); if (aspectButton instanceof HTMLElement) aspectButton.click(); } } window.__flowImageEditMode = mode; window.__flowImageEditShouldSubmit = mode === 'PROMPT' || autoSubmit; return JSON.stringify({ mode, selectedTool, shouldSubmit: window.__flowImageEditShouldSubmit }); })()",
      "description": "Choose the image edit mode and stage any tool-specific UI"
    },
    {
      "action": "browser_js",
      "code": "(() => { if (!window.__flowImageEditShouldSubmit) return JSON.stringify({ skipped: true, reason: 'staged_only' }); const prompt = `{PROMPT_TEXT}`.trim(); if (!prompt || prompt === '{PROMPT_TEXT}') throw new Error('PROMPT_TEXT variable is required for image edit submission'); const editorEl = document.querySelector(\"[role='textbox'][contenteditable='true'][data-slate-editor='true']\"); if (!(editorEl instanceof HTMLElement)) throw new Error('Flow image edit prompt box not found'); const fiberKey = Object.keys(editorEl).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? editorEl[fiberKey] : null; let store = null; for (let i = 0; cur && i < 9; i++, cur = cur.return) { if (i === 7) { store = cur.memoizedProps?.promptBoxStore; break; } } const state = store?.getState?.(); const actions = state?.actions; const textEditor = state?.inputs?.textEditor; if (!actions || !textEditor) throw new Error('Flow image edit store not found'); if (typeof actions.setMode === 'function') actions.setMode('IMAGE'); const currentText = textEditor.children?.[0]?.children?.[0]?.text ?? ''; try { textEditor.select({ anchor: { path: [0, 0], offset: 0 }, focus: { path: [0, 0], offset: currentText.length } }); } catch {} try { textEditor.deleteFragment(); } catch {} textEditor.insertText(prompt); try { textEditor.onChange(); } catch {} const text = (textEditor.children || []).map(node => (node.children || []).map(child => child.text || '').join('')).join('\\n'); window.__flowImageEditPrompt = text; return JSON.stringify({ prompt: text, mode: store.getState().mode, editMode: window.__flowImageEditMode || 'PROMPT' }); })()",
      "description": "Insert the image edit prompt through Flow's internal Slate editor"
    },
    {
      "action": "browser_js",
      "code": "(() => { if (!window.__flowImageEditShouldSubmit) return JSON.stringify({ submitted: false, reason: 'staged_only' }); const submitButton = [...document.querySelectorAll('button')].find(el => (el.textContent || '').includes('arrow_forward') && (el.textContent || '').includes('Create')); if (!(submitButton instanceof HTMLElement)) throw new Error('Flow image edit submit button not found'); const fiberKey = Object.keys(submitButton).find(k => k.startsWith('__reactFiber')); let cur = fiberKey ? submitButton[fiberKey] : null; let onSubmit = null; for (let i = 0; cur && i < 6; i++, cur = cur.return) { if (i === 5) { onSubmit = cur.memoizedProps?.onSubmit; break; } } if (typeof onSubmit !== 'function') throw new Error('Flow image edit onSubmit handler not found'); window.__flowImageEditBaseline = { resourceCount: performance.getEntriesByType('resource').length }; onSubmit(); return JSON.stringify({ submitted: true, baseline: window.__flowImageEditBaseline }); })()",
      "description": "Submit the image edit through Flow's internal handler"
    },
    {
      "action": "browser_js",
      "code": "(() => { if (!window.__flowImageEditShouldSubmit) return JSON.stringify({ staged: true }); const baseline = window.__flowImageEditBaseline || { resourceCount: 0 }; const seenRequest = () => performance.getEntriesByType('resource').slice(baseline.resourceCount).some(entry => /flowMedia:batchGenerateImages|flowWorkflows\\//.test(entry.name)); const start = Date.now(); return new Promise(resolve => { const check = () => { if (seenRequest()) { resolve(JSON.stringify({ submitted: true, generationRequestSeen: true })); return; } if (Date.now() - start > 30000) { resolve(JSON.stringify({ submitted: false, generationRequestSeen: false })); return; } setTimeout(check, 1000); }; check(); }); })()",
      "description": "Wait for Flow to issue the image edit request"
    },
    {
      "action": "screenshot",
      "description": "Capture the image editor after staging or submission",
      "optional": true
    }
  ]
}
