Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions clis/gemini/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,11 @@ function readGeminiSnapshotScript() {
transcriptLines,
composerHasText: composerText.length > 0,
isGenerating,
structuredTurnsTrusted: turns.length > 0 || transcriptLines.length === 0,
// After filtering out zero-state greetings, empty turns means
// "no actual conversation yet" — still trust the structured diff.
// Previously this flipped to false on the /app root because the
// welcome text produced transcriptLines > 0.
structuredTurnsTrusted: true,
};
})()
`;
Expand Down Expand Up @@ -417,9 +421,22 @@ function getTurnsScript() {
];

const roots = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
// Filter out zero-state welcome text ("Hi <user>, Where should we start?")
// which Gemini renders inside .visible-primary-message containers using
// .message-text class. These match our turn selectors but are NOT actual
// conversation turns, breaking the strict prefix match in diffTrustedStructuredTurns.
const isInRealConversation = (el) => {
if (el.closest('[class*="conversation-container"]')) return true;
if (el.closest('chat-history, [class*="chat-history"]')) return true;
// Exclude zero-state greeting areas
if (el.closest('[class*="visible-primary-message"]')) return false;
if (el.closest('[class*="zero-state"], [class*="empty-state"], [class*="zero_state"]')) return false;
return false;
};
const unique = roots
.filter((el, index, all) => all.indexOf(el) === index)
.filter(isVisible)
.filter(isInRealConversation)
.sort((left, right) => {
if (left === right) return 0;
const relation = left.compareDocumentPosition(right);
Expand Down Expand Up @@ -1917,7 +1934,12 @@ export async function waitForGeminiResponse(page, baseline, promptText, timeoutS
lastStructured = structuredCandidate;
structuredStableCount = 1;
}
if (!current.isGenerating && structuredStableCount >= 2) {
// Gemini's send-button aria-label sticks at "Stop response" after
// the response has already finished streaming, making isGenerating
// unreliable as a completion signal. Fall back to: if the candidate
// text has been stable for ~8 seconds (4 polls at 2s each), treat
// it as done regardless of isGenerating.
if ((!current.isGenerating && structuredStableCount >= 2) || structuredStableCount >= 4) {
return structuredCandidate;
}
continue;
Expand Down
46 changes: 37 additions & 9 deletions clis/grok/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,47 @@ async function runDefaultAsk(page, prompt, timeoutMs, newChat) {
await page.wait(3);
}
const promptJson = JSON.stringify(prompt);
// Grok's input is a contenteditable div now, not a <textarea>. Use native
// CDP typing (page.nativeType) so React recognizes the input and enables Submit.
const composerHandle = await page.evaluate(`(() => {
const box = document.querySelector('div[contenteditable="true"]')
|| document.querySelector('textarea');
if (!box) return { ok: false, msg: 'no composer' };
box.focus();
// Clear any existing text
if (box.tagName === 'TEXTAREA') { box.value = ''; }
else { box.textContent = ''; box.dispatchEvent(new InputEvent('input', { bubbles: true })); }
return { ok: true };
})()`);
if (!composerHandle?.ok) {
return [{ response: '[SEND FAILED] ' + JSON.stringify(composerHandle) }];
}
if (page.nativeType) {
try { await page.nativeType(prompt); } catch { }
} else {
await page.evaluate(`(() => {
const box = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea');
if (!box) return;
if (box.tagName === 'TEXTAREA') { box.value = ${promptJson}; }
else { box.textContent = ${promptJson}; }
box.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${promptJson}, inputType: 'insertText' }));
})()`);
}
await page.wait(1.5);
const sendResult = await page.evaluate(`(async () => {
try {
const box = document.querySelector('textarea');
if (!box) return { ok: false, msg: 'no textarea' };
box.focus(); box.value = '';
document.execCommand('selectAll');
document.execCommand('insertText', false, ${promptJson});
await new Promise(r => setTimeout(r, 1500));
const btn = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]');
if (btn && !btn.disabled) { btn.click(); return { ok: true, msg: 'clicked' }; }
// English aria (current Grok UI)
const btnEN = document.querySelector('button[aria-label="Submit"]');
if (btnEN && !btnEN.disabled && btnEN.getAttribute('aria-disabled') !== 'true') {
btnEN.click(); return { ok: true, msg: 'clicked-en' };
}
// Chinese aria (older UI)
const btnCN = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]');
if (btnCN && !btnCN.disabled) { btnCN.click(); return { ok: true, msg: 'clicked-cn' }; }
const sub = [...document.querySelectorAll('button[type="submit"]')].find(b => !b.disabled);
if (sub) { sub.click(); return { ok: true, msg: 'clicked-submit' }; }
box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
const box = document.querySelector('div[contenteditable="true"]') || document.querySelector('textarea');
if (box) box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
return { ok: true, msg: 'enter' };
} catch (e) { return { ok: false, msg: e.toString() }; }
})()`);
Expand Down