surf-cli 2.4.1 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/grok-client.cjs +120 -52
- package/package.json +1 -1
package/native/grok-client.cjs
CHANGED
|
@@ -460,28 +460,76 @@ function extractGrokResponse(bodyText, userPrompt = '') {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
463
|
-
// Grok
|
|
464
|
-
//
|
|
463
|
+
// Grok can take a long time:
|
|
464
|
+
// - Thinking models (Grok 4.1 Thinking): 40-60+ seconds to think, then streams
|
|
465
|
+
// - Fast/Auto models: No thinking phase, just streams directly
|
|
465
466
|
|
|
466
467
|
const deadline = Date.now() + timeoutMs;
|
|
467
468
|
let previousText = '';
|
|
468
|
-
let
|
|
469
|
-
const requiredStableCycles = 4; // 4 cycles at 300ms = 1.2s stable
|
|
469
|
+
let previousLength = 0;
|
|
470
470
|
let lastChangeAt = Date.now();
|
|
471
|
-
const minStableMs = 1500; // 1.5 seconds stable
|
|
472
471
|
let thinkingTime = null;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const initialLength = initialSnapshot || 0;
|
|
472
|
+
let thinkingComplete = false;
|
|
473
|
+
let lastResponseText = '';
|
|
474
|
+
let responseStableCycles = 0;
|
|
477
475
|
|
|
478
476
|
while (Date.now() < deadline) {
|
|
479
|
-
//
|
|
480
|
-
const snapshot = await evaluate(cdp, `({
|
|
481
|
-
bodyText
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
477
|
+
// Get page state with multiple completion indicators
|
|
478
|
+
const snapshot = await evaluate(cdp, `(function() {
|
|
479
|
+
const bodyText = document.body.innerText || '';
|
|
480
|
+
|
|
481
|
+
// Check for stop/cancel button (indicates still generating)
|
|
482
|
+
const hasStopBtn = !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"], button[aria-label*="Cancel"]');
|
|
483
|
+
|
|
484
|
+
// Check for "Thought for Xs" which indicates thinking model completed thinking
|
|
485
|
+
const thinkMatch = bodyText.match(/Thought for (\\d+)s/i);
|
|
486
|
+
const thinkingDone = !!thinkMatch;
|
|
487
|
+
const thinkingSecs = thinkMatch ? parseInt(thinkMatch[1], 10) : null;
|
|
488
|
+
|
|
489
|
+
// Check if actively showing "thinking..." or similar loading state
|
|
490
|
+
const isThinking = /\\bthinking\\.\\.\\./i.test(bodyText) ||
|
|
491
|
+
/\\bSearching\\.\\.\\./i.test(bodyText) ||
|
|
492
|
+
bodyText.includes('Grok is thinking') ||
|
|
493
|
+
bodyText.includes('is thinking...');
|
|
494
|
+
|
|
495
|
+
// Try to find the actual Grok response in the DOM
|
|
496
|
+
// Look for the main content area - Grok responses appear in the conversation area
|
|
497
|
+
let responseText = '';
|
|
498
|
+
|
|
499
|
+
// Strategy 1: Look for article elements or main content containers
|
|
500
|
+
const articles = document.querySelectorAll('article');
|
|
501
|
+
if (articles.length > 0) {
|
|
502
|
+
// Get the last article which should be the response
|
|
503
|
+
const lastArticle = articles[articles.length - 1];
|
|
504
|
+
responseText = lastArticle.innerText || '';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Strategy 2: If no articles, look for the conversation container
|
|
508
|
+
if (!responseText) {
|
|
509
|
+
const convArea = document.querySelector('[data-testid="conversation"], [role="main"] > div > div');
|
|
510
|
+
if (convArea) {
|
|
511
|
+
responseText = convArea.innerText || '';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Strategy 3: Fallback to looking for text after common Grok UI patterns
|
|
516
|
+
if (!responseText || responseText.length < 10) {
|
|
517
|
+
// Find content between user question and follow-up suggestions
|
|
518
|
+
const mainArea = document.querySelector('main') || document.body;
|
|
519
|
+
responseText = mainArea.innerText || bodyText;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
bodyText: bodyText,
|
|
524
|
+
responseText: responseText,
|
|
525
|
+
bodyLength: bodyText.length,
|
|
526
|
+
hasStopBtn: hasStopBtn,
|
|
527
|
+
thinkingDone: thinkingDone,
|
|
528
|
+
thinkingSecs: thinkingSecs,
|
|
529
|
+
isThinking: isThinking,
|
|
530
|
+
url: location.href
|
|
531
|
+
};
|
|
532
|
+
})()`);
|
|
485
533
|
|
|
486
534
|
if (!snapshot || !snapshot.bodyText) {
|
|
487
535
|
await delay(300);
|
|
@@ -489,62 +537,82 @@ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
|
|
|
489
537
|
}
|
|
490
538
|
|
|
491
539
|
const bodyText = snapshot.bodyText;
|
|
540
|
+
const bodyLength = snapshot.bodyLength;
|
|
492
541
|
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
542
|
+
// Track thinking time (for thinking models)
|
|
543
|
+
if (snapshot.thinkingSecs) {
|
|
544
|
+
if (!thinkingTime || snapshot.thinkingSecs > thinkingTime) {
|
|
545
|
+
thinkingTime = snapshot.thinkingSecs;
|
|
546
|
+
}
|
|
498
547
|
}
|
|
499
548
|
|
|
500
|
-
//
|
|
501
|
-
|
|
549
|
+
// Detect when thinking completes (thinking models only)
|
|
550
|
+
// "Thought for Xs" is a DEFINITIVE signal that thinking AND response generation is done
|
|
551
|
+
if (snapshot.thinkingDone && !thinkingComplete) {
|
|
552
|
+
thinkingComplete = true;
|
|
553
|
+
// Give a brief moment for final render, then we're done
|
|
554
|
+
await delay(500);
|
|
555
|
+
}
|
|
502
556
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
557
|
+
// Extract the actual response text - try DOM-extracted first, fall back to body parsing
|
|
558
|
+
let currentResponseText = '';
|
|
559
|
+
if (snapshot.responseText && snapshot.responseText.length > 10) {
|
|
560
|
+
currentResponseText = extractGrokResponse(snapshot.responseText, userPrompt) || '';
|
|
561
|
+
}
|
|
562
|
+
if (!currentResponseText || currentResponseText.length < 5) {
|
|
563
|
+
currentResponseText = extractGrokResponse(bodyText, userPrompt) || '';
|
|
564
|
+
}
|
|
506
565
|
|
|
507
|
-
// Track
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
566
|
+
// Track RESPONSE text stability (more reliable than body text)
|
|
567
|
+
if (currentResponseText !== lastResponseText) {
|
|
568
|
+
lastResponseText = currentResponseText;
|
|
569
|
+
responseStableCycles = 0;
|
|
511
570
|
lastChangeAt = Date.now();
|
|
512
|
-
} else {
|
|
513
|
-
|
|
571
|
+
} else if (currentResponseText.length > 0) {
|
|
572
|
+
responseStableCycles++;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Track body text for timeout fallback
|
|
576
|
+
if (bodyLength !== previousLength) {
|
|
577
|
+
previousText = bodyText;
|
|
578
|
+
previousLength = bodyLength;
|
|
514
579
|
}
|
|
515
580
|
|
|
516
581
|
const stableMs = Date.now() - lastChangeAt;
|
|
517
|
-
const
|
|
582
|
+
const noStopButton = !snapshot.hasStopBtn;
|
|
583
|
+
|
|
584
|
+
// Response is stable if the extracted response text hasn't changed
|
|
585
|
+
// Use shorter thresholds since we're checking actual content, not noisy body text
|
|
586
|
+
// 4 cycles (1.2s) + 1.5s minimum is enough for response stability
|
|
587
|
+
const responseIsStable = responseStableCycles >= 4 && stableMs >= 1500 && currentResponseText.length > 10;
|
|
588
|
+
|
|
589
|
+
// "Thought for Xs" is the strongest completion signal - response is definitely done
|
|
590
|
+
const thinkingModelDone = snapshot.thinkingDone && noStopButton;
|
|
591
|
+
|
|
592
|
+
// SIMPLE CHECK: If we have response content, no stop button, and stable for 3+ cycles
|
|
593
|
+
const hasResponseNoStop = currentResponseText.length > 5 && noStopButton && responseStableCycles >= 3;
|
|
518
594
|
|
|
519
595
|
// Response is complete when:
|
|
520
|
-
// 1.
|
|
521
|
-
// 2.
|
|
522
|
-
// 3.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
(hasFollowUps || isStable) &&
|
|
526
|
-
bodyText.length > initialLength;
|
|
596
|
+
// 1. Has meaningful response content (> 5 chars)
|
|
597
|
+
// 2. No stop button
|
|
598
|
+
// 3. Either: thinking done, response stable for 3+ cycles, OR stable for 4+ cycles with 1.5s
|
|
599
|
+
const isDone = currentResponseText.length > 5 && noStopButton &&
|
|
600
|
+
(thinkingModelDone || hasResponseNoStop || responseIsStable);
|
|
527
601
|
|
|
528
602
|
if (isDone) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
text: responseText,
|
|
536
|
-
thinkingTime: thinkingTime,
|
|
537
|
-
url: snapshot.url,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
603
|
+
return {
|
|
604
|
+
text: currentResponseText,
|
|
605
|
+
thinkingTime: thinkingTime,
|
|
606
|
+
url: snapshot.url,
|
|
607
|
+
};
|
|
540
608
|
}
|
|
541
609
|
|
|
542
610
|
await delay(300);
|
|
543
611
|
}
|
|
544
612
|
|
|
545
|
-
// Timeout - return whatever we have
|
|
613
|
+
// Timeout - return whatever we have (partial response is better than nothing)
|
|
546
614
|
const finalText = extractGrokResponse(previousText, userPrompt);
|
|
547
|
-
if (finalText) {
|
|
615
|
+
if (finalText && finalText.length > 10) {
|
|
548
616
|
return {
|
|
549
617
|
text: finalText,
|
|
550
618
|
thinkingTime: thinkingTime,
|