@@ -50,7 +50,9 @@ import { ITextModelService } from '../../../../../editor/common/services/resolve
5050import { diffComposerAudit } from '../../../cortexide/common/diffComposerAudit.js' ;
5151import { IAuditLogService } from '../../../cortexide/common/auditLogService.js' ;
5252import { IRollbackSnapshotService } from '../../../cortexide/common/rollbackSnapshotService.js' ;
53- import { IGitAutoStashService } from '../../../cortexide/common/gitAutoStashService.js' ;
53+ import { IApplyEngineV2 , FileEditOperation } from '../../../cortexide/common/applyEngineV2.js' ;
54+ import { EndOfLinePreference } from '../../../../../editor/common/model.js' ;
55+ import { IFileService } from '../../../../../platform/files/common/files.js' ;
5456import './composerPanel.css' ;
5557
5658type TimerHandle = ReturnType < typeof setTimeout > ;
@@ -108,6 +110,7 @@ export class ComposerPanel extends ViewPane {
108110 @IEditorWorkerService private readonly _editorWorkerService : IEditorWorkerService ,
109111 @ITextModelService private readonly _textModelService : ITextModelService ,
110112 @IAuditLogService private readonly _auditLogService : IAuditLogService ,
113+ @IFileService private readonly _fileService : IFileService ,
111114 ) {
112115 super (
113116 { ...options , titleMenuId : MenuId . ViewTitle } ,
@@ -543,8 +546,10 @@ export class ComposerPanel extends ViewPane {
543546 summaryExpanded = ! summaryExpanded ;
544547 summaryToggle . setAttribute ( 'aria-expanded' , summaryExpanded . toString ( ) ) ;
545548 summaryContent . style . display = summaryExpanded ? 'block' : 'none' ;
549+ // allow-any-unicode-next-line
546550 summaryToggle . textContent = summaryExpanded ? '▼' : '▶' ;
547551 } ;
552+ // allow-any-unicode-next-line
548553 summaryToggle . textContent = '▶' ;
549554
550555 // Update summary content reactively
@@ -1475,6 +1480,83 @@ export class ComposerPanel extends ViewPane {
14751480 }
14761481 }
14771482
1483+ /**
1484+ * Converts chat editing entries to FileEditOperations for ApplyEngineV2
1485+ */
1486+ private async _convertEntriesToOperations ( entries : readonly IModifiedFileEntry [ ] ) : Promise < FileEditOperation [ ] > {
1487+ const operations : FileEditOperation [ ] = [ ] ;
1488+
1489+ for ( const entry of entries ) {
1490+ // Get the actual file URI - modifiedURI might be a special URI, so we need to resolve it
1491+ // For now, try to get the real file URI from the model
1492+ let fileUri : URI | undefined ;
1493+ let content : string | undefined ;
1494+
1495+ try {
1496+ // Try to resolve the modifiedURI to get the model
1497+ const modelRef = await this . _textModelService . createModelReference ( entry . modifiedURI ) ;
1498+ try {
1499+ const textModel = modelRef . object . textEditorModel ;
1500+ if ( textModel && ! textModel . isDisposed ( ) ) {
1501+ content = textModel . getValue ( EndOfLinePreference . LF ) ;
1502+ // Try to get the actual file URI - might need to check if it's a special scheme
1503+ // For document entries, the modifiedURI might actually be the file URI in some cases
1504+ // But for safety, we'll try to derive it from originalURI or check the model's URI
1505+ const modelUri = textModel . uri ;
1506+ // If the model URI is a file:// URI, use it; otherwise try originalURI
1507+ if ( modelUri . scheme === 'file' ) {
1508+ fileUri = modelUri ;
1509+ } else {
1510+ // Try to get from originalURI - but need to extract the real file path
1511+ // originalURI is a snapshot URI, so we need the actual file URI
1512+ // For now, check if modifiedURI can be converted
1513+ fileUri = entry . modifiedURI ;
1514+ }
1515+ }
1516+ } finally {
1517+ modelRef . dispose ( ) ;
1518+ }
1519+ } catch {
1520+ // If model resolution fails, try to use modifiedURI directly
1521+ fileUri = entry . modifiedURI ;
1522+ }
1523+
1524+ if ( ! fileUri || ! content ) {
1525+ // Fallback: try to read from file service if modifiedURI is a file URI
1526+ if ( entry . modifiedURI . scheme === 'file' ) {
1527+ fileUri = entry . modifiedURI ;
1528+ try {
1529+ if ( await this . _fileService . exists ( fileUri ) ) {
1530+ const fileContent = await this . _fileService . readFile ( fileUri ) ;
1531+ content = fileContent . value . toString ( ) ;
1532+ } else {
1533+ // File doesn't exist - this is a create operation
1534+ // We still need content, try to get from model again or use empty
1535+ content = '' ;
1536+ }
1537+ } catch {
1538+ content = '' ;
1539+ }
1540+ } else {
1541+ // Skip this entry if we can't get content
1542+ continue ;
1543+ }
1544+ }
1545+
1546+ // Determine operation type: check if file exists
1547+ const fileExists = fileUri . scheme === 'file' && await this . _fileService . exists ( fileUri ) ;
1548+ const operationType : 'edit' | 'create' = fileExists ? 'edit' : 'create' ;
1549+
1550+ operations . push ( {
1551+ uri : fileUri ,
1552+ type : operationType ,
1553+ content : content ,
1554+ } ) ;
1555+ }
1556+
1557+ return operations ;
1558+ }
1559+
14781560 async applyAll ( ) : Promise < void > {
14791561 if ( ! this . _currentSession ) {
14801562 return ;
@@ -1504,41 +1586,50 @@ export class ComposerPanel extends ViewPane {
15041586 return ;
15051587 }
15061588
1507- // P0 SAFETY: Pre-apply snapshot and auto-stash
1508- const rollbackService = this . _instantiationService . invokeFunction ( accessor => accessor . get ( IRollbackSnapshotService ) ) ;
1509- const autostashService = this . _instantiationService . invokeFunction ( accessor => accessor . get ( IGitAutoStashService ) ) ;
1510- let snapshotId : string | undefined ;
1511- let stashRef : string | undefined ;
1589+ // Apply Engine v2: Convert entries to operations and apply atomically
1590+ const applyEngine = this . _instantiationService . invokeFunction ( accessor => accessor . get ( IApplyEngineV2 ) ) ;
15121591
1513- // Filter to only enabled hunks if we have that state
1514- // For now, accept all entries (per-hunk filtering can be added later)
15151592 try {
1516- // 1. Create snapshot if enabled
1517- if ( rollbackService . isEnabled ( ) ) {
1518- const touchedFiles = entries . map ( e => e . modifiedURI . fsPath ) ;
1519- const snapshot = await rollbackService . createSnapshot ( touchedFiles ) ;
1520- snapshotId = snapshot . id ;
1521- }
1593+ // Convert entries to FileEditOperations
1594+ const operations = await this . _convertEntriesToOperations ( entries ) ;
15221595
1523- // 2. Create git stash if enabled
1524- if ( autostashService . isEnabled ( ) ) {
1525- stashRef = await autostashService . createStash ( requestId ) ;
1596+ if ( operations . length === 0 ) {
1597+ diffComposerAudit . markApplyEnd ( requestId , false ) ;
1598+ await this . _dialogService . error ( localize ( 'composer.noOperations' , "No file operations to apply" ) ) ;
1599+ return ;
15261600 }
15271601
1528- await this . _currentSession . accept ( ) ;
1602+ // Apply using ApplyEngineV2 (atomic, verifiable, deterministic)
1603+ const result = await applyEngine . applyTransaction ( operations , { operationId : requestId } ) ;
1604+
1605+ if ( ! result . success ) {
1606+ diffComposerAudit . markApplyEnd ( requestId , false ) ;
15291607
1530- // 3. Success: discard snapshot, keep stash
1531- if ( snapshotId ) {
1532- await rollbackService . discardSnapshot ( snapshotId ) ;
1608+ // Show appropriate error message based on error category
1609+ let errorMessage = result . error || localize ( 'composer.applyError' , "Failed to apply changes" ) ;
1610+ if ( result . errorCategory === 'base_mismatch' ) {
1611+ errorMessage = localize ( 'composer.baseMismatch' , "File changed before apply. Please retry: {0}" , result . error ) ;
1612+ } else if ( result . errorCategory === 'verification_failure' ) {
1613+ errorMessage = localize ( 'composer.verificationFailed' , "Apply verification failed. Changes rolled back: {0}" , result . error ) ;
1614+ }
1615+
1616+ await this . _dialogService . error ( errorMessage ) ;
1617+ return ;
15331618 }
15341619
1620+ // Success: Update session state by calling accept() (this updates UI state but files are already applied)
1621+ // Note: accept() might try to apply again, but ApplyEngineV2 already did it atomically
1622+ // We call accept() to update the session state properly
1623+ await this . _currentSession . accept ( ) ;
1624+
15351625 diffComposerAudit . markApplyEnd ( requestId , true ) ;
15361626 const metrics = diffComposerAudit . getMetrics ( requestId , entries . length ) ;
15371627 if ( metrics && metrics . applyTime > 300 ) {
1628+ // allow-any-unicode-next-line
15381629 console . warn ( `Apply operation took ${ metrics . applyTime . toFixed ( 1 ) } ms (target: ≤300ms)` ) ;
15391630 }
15401631
1541- // Audit log: record apply
1632+ // Audit log: record apply (ApplyEngineV2 already logged, but we log additional context here)
15421633 if ( this . _auditLogService . isEnabled ( ) ) {
15431634 const files = entries . map ( e => this . _labelService . getUriLabel ( e . modifiedURI , { relative : true } ) ) ;
15441635 // Calculate diff stats from entries (use changesCount as hunks)
@@ -1560,60 +1651,34 @@ export class ComposerPanel extends ViewPane {
15601651 meta : {
15611652 requestId,
15621653 applyTime : metrics ?. applyTime ,
1654+ appliedFiles : result . appliedFiles . length ,
1655+ engine : 'v2' ,
15631656 } ,
15641657 } ) ;
15651658 }
15661659
1567- this . _dialogService . info ( localize ( 'composer.applied' , "Applied changes to {0} file(s). Use Undo to revert." , entries . length ) ) ;
1660+ this . _dialogService . info ( localize ( 'composer.applied' , "Applied (verified) to {0} file(s). Use Undo to revert." , result . appliedFiles . length ) ) ;
15681661 } catch ( error ) {
15691662 diffComposerAudit . markApplyEnd ( requestId , false ) ;
15701663
1571- // 4. Failure: restore snapshot first (fast), then git stash (fallback)
1572- let restored = false ;
1573- if ( snapshotId ) {
1574- try {
1575- await rollbackService . restoreSnapshot ( snapshotId ) ;
1576- restored = true ;
1577- } catch ( snapshotError ) {
1578- console . error ( '[ComposerPanel] Snapshot restore failed:' , snapshotError ) ;
1579- }
1580- }
1581-
1582- if ( ! restored && stashRef ) {
1583- try {
1584- await autostashService . restoreStash ( stashRef ) ;
1585- restored = true ;
1586- } catch ( stashError ) {
1587- console . error ( '[ComposerPanel] Stash restore failed:' , stashError ) ;
1588- }
1589- }
1664+ // Error handling: ApplyEngineV2 already handled rollback, but we log additional context
1665+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
15901666
1591- if ( ! restored ) {
1592- // Both failed - show modal with guidance
1593- const fileList = entries . map ( e => this . _labelService . getUriLabel ( e . modifiedURI , { relative : true } ) ) . join ( '\n' ) ;
1594- await this . _dialogService . error (
1595- localize ( 'composer.rollbackFailed' ,
1596- "Apply failed and automatic rollback failed. Please manually restore files:\n\n{0}" ,
1597- fileList )
1598- ) ;
1599- }
1600-
1601- // Audit log: record apply error
1667+ // Audit log: record apply error (ApplyEngineV2 already logged, but we add context)
16021668 if ( this . _auditLogService . isEnabled ( ) ) {
16031669 await this . _auditLogService . append ( {
16041670 ts : Date . now ( ) ,
16051671 action : 'apply' ,
16061672 ok : false ,
16071673 meta : {
16081674 requestId,
1609- error : error instanceof Error ? error . message : String ( error ) ,
1610- rollbackAttempted : true ,
1611- rollbackSuccess : restored ,
1675+ error : errorMessage ,
1676+ engine : 'v2' ,
16121677 } ,
16131678 } ) ;
16141679 }
16151680
1616- this . _dialogService . error ( localize ( 'composer.applyError' , "Failed to apply changes: {0}" , error ) ) ;
1681+ this . _dialogService . error ( localize ( 'composer.applyError' , "Failed to apply changes: {0}" , errorMessage ) ) ;
16171682 }
16181683 }
16191684
@@ -1631,6 +1696,7 @@ export class ComposerPanel extends ViewPane {
16311696 diffComposerAudit . markUndoEnd ( requestId , true ) ;
16321697 const metrics = diffComposerAudit . getMetrics ( requestId , 0 ) ;
16331698 if ( metrics && metrics . undoTime > 300 ) {
1699+ // allow-any-unicode-next-line
16341700 console . warn ( `Undo operation took ${ metrics . undoTime . toFixed ( 1 ) } ms (target: ≤300ms)` ) ;
16351701 }
16361702
0 commit comments