import { cli, Strategy } from '@jackwener/opencli/registry'; import { ArgumentError } from '@jackwener/opencli/errors'; import { GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS, GEMINI_APP_URL, GEMINI_DOMAIN, getCurrentGeminiUrl, getLatestGeminiAssistantResponse, readGeminiSnapshot, resolveGeminiLabels, selectGeminiTool, sendGeminiMessage, startNewGeminiChat, waitForGeminiSubmission, waitForGeminiConfirmButton, } from './utils.js'; function isGeminiRootAppUrl(url) { try { const parsed = new URL(url); return parsed.origin + parsed.pathname.replace(/\/+$/, '') === GEMINI_APP_URL; } catch { return false; } } function parseDeepResearchProgress(text) { const isResearching = /\bresearching(?:\s+websites?)?\b|research in progress|working on your research|正在研究|研究中/i.test(text); const waitingForStart = /\bstart(?:\s+deep)?\s+research\b|begin\s+research|generate(?:\s+deep)?\s+research\s+plan|开始研究|开始深度研究|开始调研|生成研究计划|生成调研计划|try again without deep research/i.test(text); return { isResearching, waitingForStart }; } export const deepResearchCommand = cli({ site: 'gemini', name: 'deep-research', access: 'write', description: 'Start a Gemini Deep Research run and confirm it', domain: GEMINI_DOMAIN, strategy: Strategy.COOKIE, browser: true, siteSession: 'persistent', navigateBefore: false, defaultFormat: 'plain', args: [ { name: 'prompt', positional: true, required: true, help: 'Prompt to send' }, { name: 'timeout', type: 'int', required: false, help: 'Max seconds for the overall command (default: 180; confirm-wait clamps internally to 6-20s)', default: 180 }, { name: 'tool', required: false, help: 'Override tool label (default: Deep Research)' }, { name: 'confirm', required: false, help: 'Override confirm button label (default: Start research)' }, ], columns: ['status', 'url'], func: async (page, kwargs) => { const prompt = kwargs.prompt; const timeout = kwargs.timeout; if (!Number.isInteger(timeout) || timeout < 1) { throw new ArgumentError('--timeout must be a positive integer (seconds)'); } const submitTimeout = Math.min(Math.max(timeout, 6), 20); await startNewGeminiChat(page); const toolLabels = resolveGeminiLabels(kwargs.tool, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS); const confirmLabels = resolveGeminiLabels(kwargs.confirm, GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS); const toolMatched = await selectGeminiTool(page, toolLabels); if (!toolMatched) { const url = await getCurrentGeminiUrl(page); return [{ status: 'tool-not-found', url }]; } let baseline = await readGeminiSnapshot(page); await sendGeminiMessage(page, prompt); let submitted = await waitForGeminiSubmission(page, baseline, submitTimeout); if (!submitted) { // Retry once when submit did not stick (e.g. composer swallowed Enter/click in this UI state). await selectGeminiTool(page, toolLabels); baseline = await readGeminiSnapshot(page); await sendGeminiMessage(page, prompt); submitted = await waitForGeminiSubmission(page, baseline, submitTimeout); } if (!submitted) { const url = await getCurrentGeminiUrl(page); return [{ status: 'submit-not-found', url }]; } const confirmed = await waitForGeminiConfirmButton(page, confirmLabels, timeout); let url = await getCurrentGeminiUrl(page); if (confirmed && !isGeminiRootAppUrl(url)) { return [{ status: 'started', url }]; } // false-positive confirm click can happen on generic buttons while still at /app root. { // Retry once when we are still at the root app URL, which usually means submit did not stick. if (isGeminiRootAppUrl(url)) { await selectGeminiTool(page, toolLabels); // Avoid resending prompt here: it can create a duplicate conversation thread. const confirmedRetry = await waitForGeminiConfirmButton(page, confirmLabels, timeout); url = await getCurrentGeminiUrl(page); if (confirmedRetry && !isGeminiRootAppUrl(url)) { return [{ status: 'started', url }]; } } let response = await getLatestGeminiAssistantResponse(page); let { isResearching, waitingForStart } = parseDeepResearchProgress(response); // Some UIs render the plan card first; click confirm one more time without resending prompt. if (!isResearching && waitingForStart) { const fallbackConfirmLabels = Array.from(new Set([ ...confirmLabels, ...GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS, ])); const confirmedFallback = await waitForGeminiConfirmButton(page, fallbackConfirmLabels, Math.min(timeout, 8)); if (confirmedFallback) { url = await getCurrentGeminiUrl(page); response = await getLatestGeminiAssistantResponse(page); ({ isResearching, waitingForStart } = parseDeepResearchProgress(response)); } } if (isResearching && !waitingForStart) { return [{ status: 'started', url }]; } return [{ status: 'confirm-not-found', url }]; } }, });