From bfe439117316a52ef5018eaf3a15c96acdf991fd Mon Sep 17 00:00:00 2001 From: Avni2000 Date: Tue, 10 Feb 2026 09:47:49 -0600 Subject: [PATCH 1/7] [ENHANCEMENT] Add showBaseColumn setting for toggling base branch visibility in merge view --- package.json | 7 +++++- src/resolver.ts | 43 ++++++++++++++++++----------------- src/settings.ts | 10 ++++++--- src/web/WebConflictPanel.ts | 45 +++++++++++++++++++------------------ src/web/client/types.ts | 3 ++- src/web/webTypes.ts | 30 +++++++++++++------------ 6 files changed, 76 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 538041a..ee6bbfc 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,11 @@ "type": "boolean", "default": true, "description": "Enable Ctrl+Z / Ctrl+Shift+Z undo and redo shortcuts in the conflict resolution web UI" + }, + "mergeNB.ui.showBaseColumn": { + "type": "boolean", + "default": false, + "description": "Show the base (common ancestor) branch column in the conflict resolution view. When false, only current and incoming columns are shown." } } } @@ -151,4 +156,4 @@ "markdown-it": "^14.1.0", "ws": "^8.19.0" } -} +} \ No newline at end of file diff --git a/src/resolver.ts b/src/resolver.ts index 111b7b4..e9736bd 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -11,7 +11,7 @@ * 6. Stages the resolved file in Git */ -import * as path from 'path'; +import * as path from 'path'; import * as vscode from 'vscode'; import { detectSemanticConflicts, applyAutoResolutions, AutoResolveResult } from './conflictDetector'; import { serializeNotebook, renumberExecutionCounts } from './notebookParser'; @@ -56,7 +56,7 @@ export interface ConflictedNotebook { * Main service for handling notebook merge conflict resolution. */ export class NotebookConflictResolver { - constructor(private readonly extensionUri: vscode.Uri) {} + constructor(private readonly extensionUri: vscode.Uri) { } /** * Check if a file has semantic conflicts (Git UU status). @@ -75,7 +75,7 @@ export class NotebookConflictResolver { async hasAnyConflicts(uri: vscode.Uri): Promise { try { const isUnmerged = await gitIntegration.isUnmergedFile(uri.fsPath); - + if (isUnmerged) { return { uri, @@ -94,24 +94,24 @@ export class NotebookConflictResolver { */ async findNotebooksWithConflicts(): Promise { const withConflicts: ConflictedNotebook[] = []; - + // Get unmerged files from Git status const unmergedFiles = await gitIntegration.getUnmergedFiles(); - + for (const file of unmergedFiles) { // Only process .ipynb files if (!file.path.endsWith('.ipynb')) { continue; } - + const uri = vscode.Uri.file(file.path); - + withConflicts.push({ uri, hasSemanticConflicts: true }); } - + return withConflicts; } @@ -134,7 +134,7 @@ export class NotebookConflictResolver { */ async resolveSemanticConflicts(uri: vscode.Uri): Promise { const semanticConflict = await detectSemanticConflicts(uri.fsPath); - + if (!semanticConflict) { vscode.window.showInformationMessage('No semantic conflicts detected.'); return; @@ -197,7 +197,8 @@ export class NotebookConflictResolver { semanticConflict: filteredSemanticConflict, autoResolveResult: autoResolveResult, hideNonConflictOutputs: settings.hideNonConflictOutputs, - enableUndoRedoHotkeys: settings.enableUndoRedoHotkeys + enableUndoRedoHotkeys: settings.enableUndoRedoHotkeys, + showBaseColumn: settings.showBaseColumn }; const resolutionCallback = async (resolution: UnifiedResolution): Promise => { @@ -227,12 +228,12 @@ export class NotebookConflictResolver { } const resolvedRows = resolution.resolvedRows; - + if (!resolvedRows || resolvedRows.length === 0) { // No resolutions provided if (autoResolveResult) { let resolvedNotebook = autoResolveResult.resolvedNotebook; - + const renumber = await vscode.window.showQuickPick( ['Yes', 'No'], { @@ -259,9 +260,9 @@ export class NotebookConflictResolver { } await this.applySemanticResolutionsFromRows( - uri, - semanticConflict, - resolvedRows, + uri, + semanticConflict, + resolvedRows, resolution.markAsResolved, resolution.renumberExecutionCounts, autoResolveResult @@ -320,7 +321,7 @@ export class NotebookConflictResolver { for (const row of rowsForResolution) { const { baseCell, currentCell, incomingCell, resolution: res } = row; - + let cellToUse: NotebookCell | undefined; if (res) { @@ -352,7 +353,7 @@ export class NotebookConflictResolver { metadata: referenceCell?.metadata ? JSON.parse(JSON.stringify(referenceCell.metadata)) : {}, source: resolvedContent.split(/(?<=\n)/) } as NotebookCell; - + if (cellType === 'code') { (cellToUse as any).execution_count = null; (cellToUse as any).outputs = []; @@ -394,7 +395,7 @@ export class NotebookConflictResolver { markAsResolved, renumberExecutionCounts: shouldRenumber }); - + // Show success notification (non-blocking, fire and forget) vscode.window.showInformationMessage( `Resolved conflicts in ${path.basename(uri.fsPath)}` @@ -408,12 +409,12 @@ export class NotebookConflictResolver { const content = serializeNotebook(notebook); const encoder = new TextEncoder(); await vscode.workspace.fs.writeFile(uri, encoder.encode(content)); - + // Mark as resolved with git add if requested if (markAsResolved) { await this.markFileAsResolved(uri); } - + // Fire event to notify extension (for status bar, decorations, etc.) onDidResolveConflict.fire(uri); } @@ -433,7 +434,7 @@ export class NotebookConflictResolver { vscode.window.showWarningMessage('Cannot mark file as resolved: not in a workspace'); return; } - + const relativePath = vscode.workspace.asRelativePath(uri, false); await exec(`git add "${relativePath}"`, { cwd: workspaceFolder.uri.fsPath }); vscode.window.showInformationMessage(`Marked ${relativePath} as resolved`); diff --git a/src/settings.ts b/src/settings.ts index 1c824b0..ae767b1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -8,6 +8,7 @@ * - stripOutputs: Clear cell outputs during merge (default: true) * - hideNonConflictOutputs: Hide outputs for non-conflicted cells in UI (default: true) * - enableUndoRedoHotkeys: Enable Ctrl+Z / Ctrl+Shift+Z in web UI (default: true) + * - showBaseColumn: Show base branch column in 3-column view (default: false, true in headless/testing) * * These reduce manual conflict resolution for common non-semantic differences. */ @@ -26,6 +27,7 @@ export interface MergeNBSettings { stripOutputs: boolean; hideNonConflictOutputs: boolean; enableUndoRedoHotkeys: boolean; + showBaseColumn: boolean; } /** Default settings used in headless mode */ @@ -34,7 +36,8 @@ const DEFAULT_SETTINGS: MergeNBSettings = { autoResolveKernelVersion: true, stripOutputs: true, hideNonConflictOutputs: true, - enableUndoRedoHotkeys: true + enableUndoRedoHotkeys: true, + showBaseColumn: true }; /** @@ -45,15 +48,16 @@ export function getSettings(): MergeNBSettings { if (!vscode) { return { ...DEFAULT_SETTINGS }; } - + const config = vscode.workspace.getConfiguration('mergeNB'); - + return { autoResolveExecutionCount: config.get('autoResolve.executionCount', true), autoResolveKernelVersion: config.get('autoResolve.kernelVersion', true), stripOutputs: config.get('autoResolve.stripOutputs', true), hideNonConflictOutputs: config.get('ui.hideNonConflictOutputs', true), enableUndoRedoHotkeys: config.get('ui.enableUndoRedoHotkeys', true), + showBaseColumn: config.get('ui.showBaseColumn', false), }; } diff --git a/src/web/WebConflictPanel.ts b/src/web/WebConflictPanel.ts index b7b732e..b982659 100644 --- a/src/web/WebConflictPanel.ts +++ b/src/web/WebConflictPanel.ts @@ -28,7 +28,7 @@ import { UnifiedConflict, UnifiedResolution, ResolvedRow } from './webTypes'; */ export class WebConflictPanel { public static currentPanel: WebConflictPanel | undefined; - + private readonly _extensionUri: vscode.Uri; private _conflict: UnifiedConflict | undefined; private _onResolutionComplete: ((resolution: UnifiedResolution) => Promise) | undefined; @@ -47,7 +47,7 @@ export class WebConflictPanel { const panel = new WebConflictPanel(extensionUri, conflict, onResolutionComplete); WebConflictPanel.currentPanel = panel; - + await panel._openInBrowser(); } @@ -72,7 +72,7 @@ export class WebConflictPanel { private async _openInBrowser(): Promise { const server = getWebServer(); server.setExtensionUri(this._extensionUri); - + // Start server if not running if (!server.isRunning()) { await server.start(); @@ -105,7 +105,7 @@ export class WebConflictPanel { if (!this._sessionId || !this._conflict) return; const server = getWebServer(); - + // Build the data payload for the React app const data = { filePath: this._conflict.filePath, @@ -114,6 +114,7 @@ export class WebConflictPanel { autoResolveResult: this._conflict.autoResolveResult, hideNonConflictOutputs: this._conflict.hideNonConflictOutputs, enableUndoRedoHotkeys: this._conflict.enableUndoRedoHotkeys, + showBaseColumn: this._conflict.showBaseColumn, currentBranch: this._conflict.semanticConflict?.currentBranch, incomingBranch: this._conflict.semanticConflict?.incomingBranch, }; @@ -123,16 +124,16 @@ export class WebConflictPanel { private _handleMessage(message: unknown): void { if (this._isDisposed) return; - - const msg = message as { - command?: string; - type?: string; - resolutions?: Array<{ index: number; choice: string; resolvedContent: string }>; + + const msg = message as { + command?: string; + type?: string; + resolutions?: Array<{ index: number; choice: string; resolvedContent: string }>; resolvedRows?: ResolvedRow[]; - semanticChoice?: string; - markAsResolved?: boolean + semanticChoice?: string; + markAsResolved?: boolean }; - + logger.debug('[WebConflictPanel] Received message:', msg.command || msg.type); switch (msg.command) { @@ -150,11 +151,11 @@ export class WebConflictPanel { } } - private async _handleResolution(message: { - type?: string; - resolutions?: Array<{ index: number; choice: string; resolvedContent: string }>; + private async _handleResolution(message: { + type?: string; + resolutions?: Array<{ index: number; choice: string; resolvedContent: string }>; resolvedRows?: ResolvedRow[]; - semanticChoice?: string; + semanticChoice?: string; markAsResolved?: boolean; renumberExecutionCounts?: boolean; }): Promise { @@ -176,7 +177,7 @@ export class WebConflictPanel { markAsResolved: message.markAsResolved ?? false, renumberExecutionCounts: message.renumberExecutionCounts ?? false }); - + // Send success message to browser if (this._sessionId) { const server = getWebServer(); @@ -184,14 +185,14 @@ export class WebConflictPanel { type: 'resolution-success', message: 'Conflicts resolved successfully!' }); - + // Wait to ensure message is delivered to browser await new Promise(resolve => setTimeout(resolve, 500)); } } catch (error) { logger.error('[WebConflictPanel] Error applying semantic resolutions:', error); vscode.window.showErrorMessage(`Failed to apply resolutions: ${error}`); - + // Send error message to browser if (this._sessionId) { const server = getWebServer(); @@ -210,14 +211,14 @@ export class WebConflictPanel { public dispose(): void { if (this._isDisposed) return; this._isDisposed = true; - + WebConflictPanel.currentPanel = undefined; - + if (this._sessionId) { const server = getWebServer(); server.closeSession(this._sessionId); } - + logger.debug('[WebConflictPanel] Disposed'); } } diff --git a/src/web/client/types.ts b/src/web/client/types.ts index 91410a4..ef3071d 100644 --- a/src/web/client/types.ts +++ b/src/web/client/types.ts @@ -18,7 +18,7 @@ export type { NotebookSemanticConflict, ResolutionChoice, } from '../../types'; -import type { AutoResolveResult } from '../webTypes'; +import type { AutoResolveResult } from '../webTypes'; export type { AutoResolveResult } from '../webTypes'; /** @@ -55,6 +55,7 @@ export interface UnifiedConflictData { currentBranch?: string; incomingBranch?: string; enableUndoRedoHotkeys?: boolean; + showBaseColumn?: boolean; } /** diff --git a/src/web/webTypes.ts b/src/web/webTypes.ts index 1f7bc50..ec4c24d 100644 --- a/src/web/webTypes.ts +++ b/src/web/webTypes.ts @@ -7,12 +7,12 @@ * */ -import type { - NotebookCell, - Notebook, - CellMapping, +import type { + NotebookCell, + Notebook, + CellMapping, NotebookSemanticConflict, - ResolutionChoice + ResolutionChoice } from '../types'; import type { AutoResolveResult } from '../conflictDetector'; @@ -31,6 +31,8 @@ export interface UnifiedConflict { hideNonConflictOutputs?: boolean; /** Whether undo/redo hotkeys are enabled in the web UI */ enableUndoRedoHotkeys?: boolean; + /** Whether to show the base column in the 3-way merge view */ + showBaseColumn?: boolean; } /** @@ -83,17 +85,17 @@ export interface WebConflictData { filePath: string; fileName: string; type: 'semantic'; - + // For semantic conflicts semanticConflict?: WebSemanticConflict; - + // Auto-resolution result if any autoResolveResult?: AutoResolveResult; - + // Display options hideNonConflictOutputs?: boolean; showCellHeaders?: boolean; - + // Branch information currentBranch?: string; incomingBranch?: string; @@ -106,12 +108,12 @@ export interface WebSemanticConflict { filePath: string; semanticConflicts: WebSemanticConflictItem[]; cellMappings: CellMapping[]; - + // Full notebook versions base?: Notebook; current?: Notebook; incoming?: Notebook; - + // Branch information currentBranch?: string; incomingBranch?: string; @@ -152,7 +154,7 @@ export interface WebMergeRow { /** * Messages sent from the extension to the browser. */ -export type ExtensionToBrowserMessage = +export type ExtensionToBrowserMessage = | { type: 'connected'; sessionId: string } | { type: 'conflict-data'; data: WebConflictData } | { type: 'error'; message: string } @@ -162,8 +164,8 @@ export type ExtensionToBrowserMessage = * Messages sent from the browser to the extension. */ export type BrowserToExtensionMessage = - | { - command: 'resolve'; + | { + command: 'resolve'; type: 'semantic'; resolutions: Array<{ index: number; From 59ceab8171d8a15ed6ecdca175055735368117a8 Mon Sep 17 00:00:00 2001 From: Avni2000 Date: Tue, 10 Feb 2026 09:52:33 -0600 Subject: [PATCH 2/7] [ENHANCEMENT] Implement 2-column/3-column toggle for base branch visibility in merge UI --- src/web/client/ConflictResolver.tsx | 42 ++++++++++-------- src/web/client/MergeRow.tsx | 66 +++++++++++++++-------------- src/web/client/styles.ts | 8 ++++ 3 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/web/client/ConflictResolver.tsx b/src/web/client/ConflictResolver.tsx index 9ce6c64..db30a9d 100644 --- a/src/web/client/ConflictResolver.tsx +++ b/src/web/client/ConflictResolver.tsx @@ -205,6 +205,7 @@ export function ConflictResolver({ const canUndo = history.index > 0; const canRedo = history.index < history.entries.length - 1; const enableUndoRedoHotkeys = conflict.enableUndoRedoHotkeys ?? true; + const showBaseColumn = conflict.showBaseColumn ?? false; const isMac = useMemo(() => /Mac|iPod|iPhone|iPad/.test(navigator.platform), []); const undoShortcutLabel = isMac ? 'Cmd+Z' : 'Ctrl+Z'; const redoShortcutLabel = isMac ? 'Cmd+Shift+Z' : 'Ctrl+Shift+Z'; @@ -811,20 +812,22 @@ export function ConflictResolver({
- + {showBaseColumn && ( + + )}