From cfc52065c68c33f34044fb8c287e7933e1b9c4f4 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Tue, 24 Mar 2026 09:00:01 +0800 Subject: [PATCH 1/2] fix(grok): preserve conversation across repeated ask calls (#330) The adapter unconditionally navigated to grok.com/ on every invocation, destroying the existing conversation URL even when --new was not passed. Since the browser daemon already reuses the same Chrome tab, skipping navigation lets the tab stay on the current chat thread. - Only navigate to grok.com/ when --new is true or tab is not on grok.com - Add tryStartFreshChat to the default path's --new branch (was dead code) - Add isOnGrok helper with hostname-based domain matching - Add unit tests for isOnGrok --- src/clis/grok/ask.test.ts | 25 +++++++++++++++++++++++++ src/clis/grok/ask.ts | 37 +++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/clis/grok/ask.test.ts b/src/clis/grok/ask.test.ts index 6d0dfeee..3da8c572 100644 --- a/src/clis/grok/ask.test.ts +++ b/src/clis/grok/ask.test.ts @@ -1,7 +1,32 @@ import { describe, expect, it } from 'vitest'; +import type { IPage } from '../../types.js'; import { __test__ } from './ask.js'; describe('grok ask helpers', () => { + describe('isOnGrok', () => { + const fakePage = (url: string | Error): IPage => + ({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) }) as unknown as IPage; + + it('returns true for grok.com URLs', async () => { + expect(await __test__.isOnGrok(fakePage('https://grok.com/'))).toBe(true); + expect(await __test__.isOnGrok(fakePage('https://grok.com/chat/abc123'))).toBe(true); + }); + + it('returns true for grok.com subdomains', async () => { + expect(await __test__.isOnGrok(fakePage('https://api.grok.com/v1'))).toBe(true); + }); + + it('returns false for non-grok domains', async () => { + expect(await __test__.isOnGrok(fakePage('https://fakegrok.com/'))).toBe(false); + expect(await __test__.isOnGrok(fakePage('https://example.com/?next=grok.com'))).toBe(false); + expect(await __test__.isOnGrok(fakePage('about:blank'))).toBe(false); + }); + + it('returns false when evaluate throws (detached tab)', async () => { + expect(await __test__.isOnGrok(fakePage(new Error('detached')))).toBe(false); + }); + }); + it('normalizes boolean flags for explicit web routing', () => { expect(__test__.normalizeBooleanFlag(true)).toBe(true); expect(__test__.normalizeBooleanFlag('true')).toBe(true); diff --git a/src/clis/grok/ask.ts b/src/clis/grok/ask.ts index beae05bf..d8abb1e3 100644 --- a/src/clis/grok/ask.ts +++ b/src/clis/grok/ask.ts @@ -53,6 +53,19 @@ function updateStableState(previousText: string, stableCount: number, nextText: return { previousText: nextText, stableCount: 0 }; } +/** Check whether the tab is already on grok.com (any path). */ +async function isOnGrok(page: IPage): Promise { + // catch handles blank tabs (about:blank) or detached pages + const url = await page.evaluate('window.location.href').catch(() => ''); + if (typeof url !== 'string' || !url) return false; + try { + const hostname = new URL(url).hostname; + return hostname === 'grok.com' || hostname.endsWith('.grok.com'); + } catch { + return false; + } +} + async function runDefaultAsk( page: IPage, prompt: string, @@ -60,21 +73,17 @@ async function runDefaultAsk( newChat: boolean, ) { if (newChat) { + // Explicitly start a fresh conversation via the homepage await page.goto(GROK_URL); await page.wait(2); - await page.evaluate(`(() => { - const btn = [...document.querySelectorAll('a, button')].find(b => { - const t = (b.textContent || '').trim().toLowerCase(); - return t.includes('new') || b.getAttribute('href') === '/'; - }); - if (btn) btn.click(); - })()`); + await tryStartFreshChat(page); await page.wait(2); + } else if (!(await isOnGrok(page))) { + // First invocation or tab was recycled — navigate to Grok + await page.goto(GROK_URL); + await page.wait(3); } - await page.goto(GROK_URL); - await page.wait(3); - const promptJson = JSON.stringify(prompt); const sendResult = await page.evaluate(`(async () => { try { @@ -249,11 +258,14 @@ async function runExplicitWebAsk( timeoutMs: number, newChat: boolean, ) { - await page.goto(GROK_URL, { settleMs: 2000 }); - if (newChat) { + // Navigate to homepage and start a fresh conversation + await page.goto(GROK_URL, { settleMs: 2000 }); await tryStartFreshChat(page); await page.wait(2); + } else if (!(await isOnGrok(page))) { + // First invocation or tab was recycled — navigate to Grok + await page.goto(GROK_URL, { settleMs: 2000 }); } const baselineBubbles = await getBubbleTexts(page); @@ -318,4 +330,5 @@ export const __test__ = { updateStableState, normalizeBooleanFlag, normalizeBubbleText, + isOnGrok, }; From 55d4589845840f79ca9f717aa59d15e499c1c68e Mon Sep 17 00:00:00 2001 From: jackwener Date: Tue, 24 Mar 2026 19:19:25 +0800 Subject: [PATCH 2/2] test(grok): add adapter to vitest project config --- vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.ts b/vitest.config.ts index 1a71c405..63c31da2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ 'src/clis/reddit/**/*.test.ts', 'src/clis/bilibili/**/*.test.ts', 'src/clis/linkedin/**/*.test.ts', + 'src/clis/grok/**/*.test.ts', ], sequence: { groupOrder: 1,