From 379a72e52dae34e4a04f811bcdba9afe5e8537b7 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Sat, 6 Jun 2026 05:30:12 +0700 Subject: [PATCH] fix: writing stuck - handle empty model response + no-divider fallback + timeline cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes of writing getting stuck: 1. Model returns empty response for a section → now detected early with actionable error message including model name 2. Model responds but without ~~~ARTICLE~~~ divider (happens with fallback models like Gemini) → now treats entire response as markdown content 3. Stream ends without 'complete' event (error/exit in PHP) → JS timeline entries lingered as 'active' forever. Now deactivated on stream close. 4. Error messages in execution flow now use structured formatAiErrorMessage with retry button instead of raw text Also: deactivateActiveTimelineEntries called in catch block so errors properly clear the 'Writing section X' status indicator. --- assets/js/sidebar.js | 12 ++++++++-- includes/class-gutenberg-sidebar.php | 36 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js index b38d008..21d91ba 100644 --- a/assets/js/sidebar.js +++ b/assets/js/sidebar.js @@ -2912,12 +2912,20 @@ } } clearTimeout(timeout); + // If stream ended without a 'complete' event, deactivate lingering timeline entries + setMessages(prev => { + const hasActive = prev.some(m => m.type === 'timeline' && m.status && !['complete', 'inactive', 'stopped'].includes(m.status)); + if (hasActive) { + return deactivateActiveTimelineEntries(prev); + } + return prev; + }); } catch (error) { setAgentMode(currentPlanRef.current ? 'planning' : 'chat'); - setMessages(prev => [...prev, { + setMessages(prev => [...deactivateActiveTimelineEntries(prev), { role: 'system', type: 'error', - content: 'Error: ' + (error.message || 'Failed to execute outline'), + content: formatAiErrorMessage(error, 'Failed to execute outline'), canRetry: true, retryType: 'execute', }]); diff --git a/includes/class-gutenberg-sidebar.php b/includes/class-gutenberg-sidebar.php index fd9bcff..fc6eb2c 100644 --- a/includes/class-gutenberg-sidebar.php +++ b/includes/class-gutenberg-sidebar.php @@ -3343,6 +3343,42 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati exit; } + // Handle empty response from model + if ( empty( trim( (string) $accumulated_content ) ) ) { + $model_used = $response['model'] ?? 'unknown'; + wpaw_debug_log( "Section writing got empty response from model: {$model_used}" ); + echo "data: " . wp_json_encode( + array( + 'type' => 'error', + 'message' => sprintf( 'Section "%s" got an empty response from the AI model (%s). Please retry.', $heading, $model_used ), + ) + ) . "\n\n"; + flush(); + exit; + } + + // If divider was never found, treat the entire content as markdown + if ( ! $divider_found ) { + wpaw_debug_log( 'No ~~~ARTICLE~~~ divider found in section response. Using full content as markdown.' ); + $markdown_content = $accumulated_content; + // Strip any leading conversational fluff (first line if it looks like a note) + $lines = explode( "\n", $markdown_content ); + if ( ! empty( $lines[0] ) && ! preg_match( '/^#{1,3}\s/', $lines[0] ) && strlen( $lines[0] ) < 200 ) { + // First line might be a brief conversational note, skip it + $first_line = array_shift( $lines ); + if ( ! empty( $first_line ) ) { + echo "data: " . wp_json_encode( + array( + 'type' => 'conversational', + 'content' => trim( $first_line ), + ) + ) . "\n\n"; + flush(); + } + $markdown_content = implode( "\n", $lines ); + } + } + $section_cost = $response['cost'] ?? 0; $total_cost += $section_cost;