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.
@@ -460,28 +460,76 @@ function extractGrokResponse(bodyText, userPrompt = '') {
460
460
  }
461
461
 
462
462
  async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '') {
463
- // Grok 4.1 Thinking can take a LONG time (47+ seconds in screenshot)
464
- // Default to 5 minutes for thinking models
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 stableCycles = 0;
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
- // Capture initial page state to detect new content
475
- const initialSnapshot = await evaluate(cdp, `document.body.innerText.length`);
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
- // Simple approach: get full body text and parse in Node.js
480
- const snapshot = await evaluate(cdp, `({
481
- bodyText: document.body.innerText || '',
482
- hasStopBtn: !!document.querySelector('button[aria-label*="Stop"], button[aria-label*="stop"]'),
483
- url: location.href
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
- // Parse thinking time from body text
494
- const thinkMatch = bodyText.match(/Thought for (\d+)s/i);
495
- if (thinkMatch) {
496
- const t = parseInt(thinkMatch[1], 10);
497
- if (!thinkingTime || t > thinkingTime) thinkingTime = t;
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
- // Check if still actively thinking
501
- const isThinking = /\bthinking\.{0,3}$/im.test(bodyText);
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
- // Check for completion indicators in body text
504
- // Grok shows follow-up suggestions when done
505
- const hasFollowUps = /Explain|Tell me more|Learn more/i.test(bodyText);
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 body text changes for stability
508
- if (bodyText.length !== previousText.length) {
509
- previousText = bodyText;
510
- stableCycles = 0;
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
- stableCycles++;
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 isStable = stableCycles >= requiredStableCycles && stableMs >= minStableMs;
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. Not generating (no stop button)
521
- // 2. Not actively thinking
522
- // 3. Has follow-up suggestions OR body text is stable
523
- // 4. Body is longer than initial (new content appeared)
524
- const isDone = !snapshot.hasStopBtn && !isThinking &&
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
- // Extract the response from the body text
530
- // The response is typically between the user's question and the follow-up suggestions
531
- const responseText = extractGrokResponse(bodyText, userPrompt);
532
-
533
- if (responseText) {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",