Skip to content

Commit e28a2aa

Browse files
authored
Merge pull request #29 from OpenCortexIDE/apply-engine-v2
Apply engine v2
2 parents 7cd4a3b + 93b4845 commit e28a2aa

File tree

13 files changed

+1756
-162
lines changed

13 files changed

+1756
-162
lines changed

src/vs/workbench/contrib/chat/browser/chatEditing/composerPanel.ts

Lines changed: 123 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ import { ITextModelService } from '../../../../../editor/common/services/resolve
5050
import { diffComposerAudit } from '../../../cortexide/common/diffComposerAudit.js';
5151
import { IAuditLogService } from '../../../cortexide/common/auditLogService.js';
5252
import { 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';
5456
import './composerPanel.css';
5557

5658
type 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

Comments
 (0)