@@ -413,7 +413,7 @@ function register(ctx) {
413413
414414 sendToolExecutionEvents ( mainWindow , iterationToolResults , playwrightBrowser ) ;
415415 capArray ( allCloudToolResults , 50 ) ;
416- if ( mainWindow ) mainWindow . webContents . send ( 'mcp-tool-results' ) ;
416+ if ( mainWindow ) mainWindow . webContents . send ( 'mcp-tool-results' , iterationToolResults ) ;
417417
418418 // Build tool feedback
419419 const toolSummaryParts = [ ] ;
@@ -505,7 +505,7 @@ function register(ctx) {
505505 if ( ! llmEngine . isReady ) {
506506 if ( mainWindow ) mainWindow . webContents . send ( 'llm-token' , '\n*[Model is still loading — please wait and try again]*\n' ) ;
507507 if ( mainWindow ) mainWindow . webContents . send ( 'llm-done' ) ;
508- return ;
508+ return { success : false , error : 'Model is still loading — please wait and try again' } ;
509509 }
510510 }
511511
@@ -543,7 +543,7 @@ function register(ctx) {
543543 done : true ,
544544 } ) ;
545545 }
546- return ;
546+ return { success : false , error : `Context window too small ( ${ totalCtx } tokens) for tool-assisted generation` } ;
547547 }
548548 }
549549 console . log ( `[AI Chat] Model: ${ modelTier . family } (${ modelTier . paramLabel } ${ modelTier . family } ) \u2014 tools=${ modelProfile . generation ?. maxToolsPerTurn ?? 0 } , grammar=${ modelProfile . generation ?. grammarConstrained ? 'strict' : 'limited' } ` ) ;
@@ -722,6 +722,7 @@ function register(ctx) {
722722 let _contLowProgressCount = 0 ;
723723 let _contRepeatCount = 0 ;
724724 let _lastContText = '' ;
725+ let _contCharSizes = [ ] ; // Track char counts for pattern detection
725726 let _pendingPartialBlock = null ;
726727 let lastIterationResponse = '' ;
727728 let nonContextRetries = 0 ;
@@ -907,7 +908,20 @@ function register(ctx) {
907908 }
908909 if ( _tStart !== - 1 && _tName && mainWindow && ! mainWindow . isDestroyed ( ) ) {
909910 const raw = _tb . slice ( _tStart ) ;
910- const paramsText = raw . length > 4000 ? raw . slice ( 0 , 4000 ) : raw ;
911+ // Smart truncation: ensure "content" key is always visible for preview
912+ let paramsText ;
913+ if ( raw . length <= 8000 ) {
914+ paramsText = raw ;
915+ } else {
916+ // Keep first 1000 chars (covers tool name, filePath) + last 4000 chars (covers content tail)
917+ const contentIdx = raw . indexOf ( '"content"' ) ;
918+ if ( contentIdx !== - 1 && contentIdx < raw . length ) {
919+ // Keep from content key onward (up to 6000 chars) plus the header
920+ paramsText = raw . slice ( 0 , Math . min ( contentIdx + 6000 , raw . length ) ) ;
921+ } else {
922+ paramsText = raw . slice ( 0 , 4000 ) ;
923+ }
924+ }
911925 mainWindow . webContents . send ( 'llm-tool-generating' , {
912926 callIndex : _tIdx , functionName : _tName , paramsText, done : false ,
913927 } ) ;
@@ -1197,16 +1211,58 @@ function register(ctx) {
11971211 // Detect repeated identical content (model stuck in loop)
11981212 if ( responseText . trim ( ) === _lastContText . trim ( ) && responseText . length > 0 ) {
11991213 _contRepeatCount ++ ;
1214+ } else if ( _lastContText . length > 0 && responseText . length > 0 ) {
1215+ // Similarity-based detection: if >80% of chars overlap, count as repeat
1216+ const a = responseText . trim ( ) , b = _lastContText . trim ( ) ;
1217+ const shorter = Math . min ( a . length , b . length ) , longer = Math . max ( a . length , b . length ) ;
1218+ if ( shorter > 50 && longer > 0 ) {
1219+ // Simple char overlap ratio: count matching chars at same positions
1220+ let matches = 0 ;
1221+ for ( let ci = 0 ; ci < shorter ; ci ++ ) { if ( a [ ci ] === b [ ci ] ) matches ++ ; }
1222+ const similarity = matches / longer ;
1223+ if ( similarity > 0.8 ) {
1224+ _contRepeatCount ++ ;
1225+ console . log ( `[AI Chat] Near-identical continuation detected (similarity=${ ( similarity * 100 ) . toFixed ( 1 ) } %)` ) ;
1226+ } else {
1227+ _contRepeatCount = 0 ;
1228+ }
1229+ } else {
1230+ _contRepeatCount = 0 ;
1231+ }
12001232 } else {
12011233 _contRepeatCount = 0 ;
12021234 }
12031235 _lastContText = responseText ;
12041236
1205- if ( _contLowProgressCount >= 3 || _contRepeatCount >= 2 ) {
1206- console . log ( `[AI Chat] Continuation aborted: ${ _contRepeatCount >= 2 ? 'repeated identical content' : 'no forward progress' } ` ) ;
1237+ // Track output sizes for pattern detection (Step 9)
1238+ _contCharSizes . push ( responseText . length ) ;
1239+
1240+ // Hard total accumulated char limit: stop runaway continuation
1241+ const MAX_CONTINUATION_CHARS = 50000 ;
1242+ let _contAbortReason = '' ;
1243+ if ( fullResponseText . length > MAX_CONTINUATION_CHARS ) {
1244+ _contAbortReason = `total output exceeds ${ MAX_CONTINUATION_CHARS } chars` ;
1245+ } else if ( _contLowProgressCount >= 3 ) {
1246+ _contAbortReason = 'no forward progress' ;
1247+ } else if ( _contRepeatCount >= 2 ) {
1248+ _contAbortReason = 'repeated/near-identical content' ;
1249+ }
1250+
1251+ // Forward-progress scoring: after 5 passes, if avg chars per pass varies <10% from first pass, abort
1252+ if ( ! _contAbortReason && _contCharSizes . length >= 5 ) {
1253+ const firstSize = _contCharSizes [ 0 ] ;
1254+ const avgSize = _contCharSizes . reduce ( ( s , v ) => s + v , 0 ) / _contCharSizes . length ;
1255+ if ( firstSize > 0 && Math . abs ( avgSize - firstSize ) / firstSize < 0.10 ) {
1256+ _contAbortReason = `uniform output size (~${ Math . round ( avgSize ) } chars/pass for ${ _contCharSizes . length } passes)` ;
1257+ }
1258+ }
1259+
1260+ if ( _contAbortReason ) {
1261+ console . log ( `[AI Chat] Continuation aborted: ${ _contAbortReason } ` ) ;
12071262 continuationCount = 0 ;
12081263 _contLowProgressCount = 0 ;
12091264 _contRepeatCount = 0 ;
1265+ _contCharSizes = [ ] ;
12101266 // Fall through
12111267 } else {
12121268 const truncReason = _hasUnclosedToolFence ? 'unclosed fence' : 'maxTokens' ;
0 commit comments