@@ -476,7 +476,19 @@ function formatSuccessfulToolResult(tr, opts = {}) {
476476 switch ( tr . tool ) {
477477 case 'read_file' :
478478 text += `**File:** ${ tr . params ?. filePath } ${ tr . result . readRange ? ` (lines ${ tr . result . readRange } )` : '' } \n` ;
479- text += `\`\`\`\n${ ( tr . result . content || '' ) . substring ( 0 , 2000 ) } \n\`\`\`\n` ;
479+ {
480+ // Fix 58B: Show head+tail for large files so the model can see both
481+ // the file structure AND where it left off (critical for append workflows)
482+ const content = tr . result . content || '' ;
483+ if ( content . length > 4000 ) {
484+ const lines = content . split ( '\n' ) ;
485+ const head = lines . slice ( 0 , 15 ) . join ( '\n' ) ;
486+ const tail = lines . slice ( - 40 ) . join ( '\n' ) ;
487+ text += `\`\`\`\n${ head } \n... (${ lines . length } lines total, middle omitted) ...\n${ tail } \n\`\`\`\n` ;
488+ } else {
489+ text += `\`\`\`\n${ content . substring ( 0 , 3000 ) } \n\`\`\`\n` ;
490+ }
491+ }
480492 break ;
481493
482494 case 'write_file' :
@@ -516,6 +528,28 @@ function formatSuccessfulToolResult(tr, opts = {}) {
516528 text += `*Content appended successfully.*\n` ;
517529 }
518530 }
531+
532+ // Fix 59C: Post-write structural validation — immediate feedback loop
533+ // Provides IDE-level diagnostics (like LSP Problems panel) so the model
534+ // knows the structural state of the file RIGHT AFTER writing, not just at rotation.
535+ const writtenFilePath = tr . result ?. path || tr . params ?. filePath || '' ;
536+ const fullWrittenContent = ( tr . tool === 'append_to_file' && tr . result ?. fullContent )
537+ ? tr . result . fullContent : ( tr . params ?. content || '' ) ;
538+ if ( writtenFilePath && fullWrittenContent . length > 100 ) {
539+ const digest = buildFileStructureDigest ( writtenFilePath , fullWrittenContent ) ;
540+ if ( digest ) {
541+ // Extract only the structural warnings — skip LAST 3 LINES and full header to stay compact
542+ const digestLines = digest . split ( '\n' ) ;
543+ const structuralNotes = digestLines . filter ( l =>
544+ l . startsWith ( 'HTML TAGS MISSING' ) ||
545+ l . startsWith ( 'CSS SELECTORS ALREADY DEFINED' ) ||
546+ l . startsWith ( 'STATUS:' )
547+ ) ;
548+ if ( structuralNotes . length > 0 ) {
549+ text += `**Structure:** ${ structuralNotes . join ( ' | ' ) } \n` ;
550+ }
551+ }
552+ }
519553 break ;
520554 }
521555
@@ -636,9 +670,6 @@ class ExecutionState {
636670 if ( toolName === 'write_file' && result ?. success && params ?. filePath ) {
637671 this . filesCreated . push ( { path : params . filePath , iteration } ) ;
638672 }
639- if ( toolName === 'append_to_file' && result ?. success && params ?. filePath ) {
640- this . filesCreated . push ( { path : params . filePath , iteration, append : true } ) ;
641- }
642673 if ( toolName === 'edit_file' && result ?. success && params ?. filePath ) {
643674 this . filesEdited . push ( { path : params . filePath , iteration } ) ;
644675 }
@@ -657,22 +688,7 @@ class ExecutionState {
657688 parts . push ( `URLs visited: ${ recent . map ( v => `${ v . success ? 'OK' : 'FAIL' } ${ v . url } ` ) . join ( ', ' ) } ` ) ;
658689 }
659690 if ( this . filesCreated . length > 0 ) {
660- // Fix 61: Show per-file write counts so the model can see when it's looping
661- const fileCounts = { } ;
662- for ( const f of this . filesCreated ) {
663- if ( ! fileCounts [ f . path ] ) fileCounts [ f . path ] = { writes : 0 , appends : 0 } ;
664- if ( f . append ) fileCounts [ f . path ] . appends ++ ;
665- else fileCounts [ f . path ] . writes ++ ;
666- }
667- const fileList = Object . entries ( fileCounts ) . map ( ( [ p , c ] ) => {
668- const total = c . writes + c . appends ;
669- if ( total <= 1 ) return p ;
670- const detail = [ ] ;
671- if ( c . writes > 0 ) detail . push ( `${ c . writes } × written` ) ;
672- if ( c . appends > 0 ) detail . push ( `${ c . appends } × appended` ) ;
673- return `${ p } (${ detail . join ( ', ' ) } )` ;
674- } ) ;
675- parts . push ( `Files created/modified: ${ fileList . join ( ', ' ) } ` ) ;
691+ parts . push ( `Files created: ${ this . filesCreated . map ( f => f . path ) . join ( ', ' ) } ` ) ;
676692 }
677693 if ( this . filesEdited . length > 0 ) {
678694 parts . push ( `Files edited: ${ this . filesEdited . map ( f => f . path ) . join ( ', ' ) } ` ) ;
@@ -698,6 +714,112 @@ class ExecutionState {
698714 }
699715}
700716
717+ /**
718+ * Build a compact structural digest of a file's content.
719+ * This digest survives context rotation and tells the model what's already
720+ * on disk — preventing duplicate CSS selectors, reopened tags, etc.
721+ *
722+ * @param {string } filePath - File path (used for extension detection)
723+ * @param {string } content - Full file content
724+ * @returns {string } Compact multi-line digest for injection into prompts
725+ */
726+ function buildFileStructureDigest ( filePath , content ) {
727+ if ( ! content || content . length < 10 ) return '' ;
728+ const ext = ( filePath || '' ) . split ( '.' ) . pop ( ) . toLowerCase ( ) ;
729+ const lines = content . split ( '\n' ) ;
730+ const totalLines = lines . length ;
731+ const sections = [ ] ;
732+
733+ // --- HTML / CSS structure ---
734+ if ( ext === 'html' || ext === 'htm' || ext === 'css' || ext === 'svelte' || ext === 'vue' ) {
735+ // Detect HTML structural tags present
736+ const htmlTags = [ ] ;
737+ const htmlMissing = [ ] ;
738+ const structChecks = [
739+ [ '<!DOCTYPE' , '<!DOCTYPE>' ] ,
740+ [ '<html' , '<html>' ] ,
741+ [ '<head' , '<head>' ] ,
742+ [ '</head>' , '</head>' ] ,
743+ [ '<style' , '<style>' ] ,
744+ [ '</style>' , '</style>' ] ,
745+ [ '<body' , '<body>' ] ,
746+ [ '</body>' , '</body>' ] ,
747+ [ '<header' , '<header>' ] ,
748+ [ '</header>' , '</header>' ] ,
749+ [ '<main' , '<main>' ] ,
750+ [ '<footer' , '<footer>' ] ,
751+ [ '</footer>' , '</footer>' ] ,
752+ [ '</html>' , '</html>' ] ,
753+ ] ;
754+ for ( const [ search , label ] of structChecks ) {
755+ if ( content . includes ( search ) ) htmlTags . push ( label ) ;
756+ else htmlMissing . push ( label ) ;
757+ }
758+ if ( htmlTags . length > 0 ) sections . push ( `HTML TAGS PRESENT: ${ htmlTags . join ( ', ' ) } ` ) ;
759+ if ( htmlMissing . length > 0 && ext === 'html' ) sections . push ( `HTML TAGS MISSING (still needed): ${ htmlMissing . join ( ', ' ) } ` ) ;
760+
761+ // Extract CSS selectors (anything before { that is a valid selector)
762+ const selectorSet = new Set ( ) ;
763+ const selectorRegex = / ^ [ \t ] * ( [ ^ { } @ / \n * ] [ ^ { ] * ?) \s * \{ / gm;
764+ let m ;
765+ while ( ( m = selectorRegex . exec ( content ) ) !== null ) {
766+ let sel = m [ 1 ] . trim ( ) ;
767+ // Skip CSS property lines that leaked through (contain : before {)
768+ if ( sel . includes ( ':' ) && ! sel . includes ( '::' ) && ! sel . includes ( ':hover' ) &&
769+ ! sel . includes ( ':focus' ) && ! sel . includes ( ':active' ) && ! sel . includes ( ':first' ) &&
770+ ! sel . includes ( ':last' ) && ! sel . includes ( ':nth' ) && ! sel . includes ( ':not' ) &&
771+ ! sel . includes ( ':root' ) ) continue ;
772+ if ( sel . length > 0 && sel . length < 80 ) selectorSet . add ( sel ) ;
773+ }
774+ if ( selectorSet . size > 0 ) {
775+ const selList = [ ...selectorSet ] ;
776+ // Cap to 60 selectors to stay compact
777+ const display = selList . length > 60 ? selList . slice ( 0 , 60 ) . join ( ', ' ) + ` ... (${ selList . length } total)` : selList . join ( ', ' ) ;
778+ sections . push ( `CSS SELECTORS ALREADY DEFINED (do NOT redefine): ${ display } ` ) ;
779+ }
780+
781+ // Detect if <style> is open but not closed
782+ const styleOpens = ( content . match ( / < s t y l e [ \s > ] / gi) || [ ] ) . length ;
783+ const styleCloses = ( content . match ( / < \/ s t y l e > / gi) || [ ] ) . length ;
784+ if ( styleOpens > styleCloses ) {
785+ sections . push ( `STATUS: <style> tag is OPEN (not closed). Close </style> before starting <body>.` ) ;
786+ }
787+ }
788+
789+ // --- JavaScript / TypeScript structure ---
790+ if ( ext === 'js' || ext === 'ts' || ext === 'jsx' || ext === 'tsx' || ext === 'mjs' || ext === 'cjs' ) {
791+ const funcs = new Set ( ) ;
792+ const funcRegex = / (?: f u n c t i o n \s + ( \w + ) | (?: c o n s t | l e t | v a r ) \s + ( \w + ) \s * = \s * (?: a s y n c \s * ) ? (?: \( [ ^ ) ] * \) | [ ^ = ] ) \s * = > | c l a s s \s + ( \w + ) ) / g;
793+ let fm ;
794+ while ( ( fm = funcRegex . exec ( content ) ) !== null ) {
795+ const name = fm [ 1 ] || fm [ 2 ] || fm [ 3 ] ;
796+ if ( name ) funcs . add ( name ) ;
797+ }
798+ if ( funcs . size > 0 ) {
799+ const display = [ ...funcs ] . slice ( 0 , 40 ) . join ( ', ' ) ;
800+ sections . push ( `DEFINED: ${ display } ` ) ;
801+ }
802+ // Detect exports
803+ const expMatch = content . match ( / m o d u l e \. e x p o r t s \s * = | e x p o r t \s + (?: d e f a u l t | { ) / g) ;
804+ if ( expMatch ) sections . push ( `EXPORTS: ${ expMatch . length } export statement(s)` ) ;
805+ }
806+
807+ // --- Python structure ---
808+ if ( ext === 'py' ) {
809+ const pyDefs = new Set ( ) ;
810+ const pyRegex = / ^ (?: c l a s s | d e f ) \s + ( \w + ) / gm;
811+ let pm ;
812+ while ( ( pm = pyRegex . exec ( content ) ) !== null ) pyDefs . add ( pm [ 1 ] ) ;
813+ if ( pyDefs . size > 0 ) sections . push ( `DEFINED: ${ [ ...pyDefs ] . join ( ', ' ) } ` ) ;
814+ }
815+
816+ // --- Universal: last 3 lines for continuation context ---
817+ const lastLines = lines . slice ( - 3 ) . map ( l => l . trimEnd ( ) ) . join ( '\n' ) ;
818+ sections . push ( `LAST 3 LINES:\n${ lastLines } ` ) ;
819+
820+ return `FILE: ${ filePath } (${ totalLines } lines, ${ content . length } chars)\n${ sections . join ( '\n' ) } ` ;
821+ }
822+
701823module . exports = {
702824 isNearDuplicate,
703825 checkFileCompleteness,
@@ -713,5 +835,6 @@ module.exports = {
713835 classifyResponseFailure,
714836 progressiveContextCompaction,
715837 buildToolFeedback,
838+ buildFileStructureDigest,
716839 ExecutionState,
717840} ;
0 commit comments