Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ Right now, that means a VSCode + Web UI flow that works without extra Git config
- IDE integration via extension commands, status bar notifications, and file conflict discovery.
- Extremely active development (issues and feedback are always welcome, and feel free to email me!).

- Light and dark themes (the demo above needs to be updated!):

<div >
<div style="box-shadow:0 12px 30px rgba(0,0,0,0.18);border-radius:8px;overflow:hidden;margin-bottom:32px;">
<img src="readme-assets/light-theme.png" alt="Light theme" style="display:block;width:100%;height:auto;" />
</div>
<div style="box-shadow:0 12px 30px rgba(0,0,0,0.18);border-radius:8px;overflow:hidden;">
<img src="readme-assets/dark-theme.png" alt="Dark theme" style="display:block;width:100%;height:auto;" />
</div>
</div>

> [!IMPORTANT]
> MergeNB is currently **not compatible with nbdime** in the same merge flow.
Expand Down
16 changes: 16 additions & 0 deletions esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ function copyKatex() {
}
}

/**
* Copy local web fonts to dist/web/fonts directory
*/
function copyWebFonts() {
const srcDir = path.join(__dirname, 'src', 'web', 'client', 'fonts');
const destDir = path.join(__dirname, 'dist', 'web', 'fonts');

if (fs.existsSync(srcDir)) {
copyDir(srcDir, destDir);
console.log('[fonts] Copied bundled web fonts');
} else {
console.warn(`[fonts] Warning: ${srcDir} not found`);
}
}

async function main() {
// Extension bundle (Node.js / VSCode)
const extensionCtx = await esbuild.context({
Expand Down Expand Up @@ -119,6 +134,7 @@ async function main() {

// Copy KaTeX after build
copyKatex();
copyWebFonts();
}

main().catch(e => {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
"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."
},
"mergeNB.ui.theme": {
"type": "string",
"enum": ["dark", "light"],
"default": "dark",
"description": "Visual theme for the conflict resolution UI. 'light' provides a light, beige theme inspired by the MergeNB logo. 'dark' provides an inverse dark theme."
}
}
}
Expand Down
Binary file added readme-assets/dark-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme-assets/light-theme.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ export class NotebookConflictResolver {
autoResolveResult: autoResolveResult,
hideNonConflictOutputs: settings.hideNonConflictOutputs,
enableUndoRedoHotkeys: settings.enableUndoRedoHotkeys,
showBaseColumn: settings.showBaseColumn
showBaseColumn: settings.showBaseColumn,
theme: settings.theme
};

const resolutionCallback = async (resolution: UnifiedResolution): Promise<void> => {
Expand Down
14 changes: 11 additions & 3 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* - 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)
* - theme: UI theme selection ('dark' | 'light', default: 'light')
*
* These reduce manual conflict resolution for common non-semantic differences.
*/
Expand All @@ -30,6 +31,7 @@ export interface MergeNBSettings {
hideNonConflictOutputs: boolean;
enableUndoRedoHotkeys: boolean;
showBaseColumn: boolean;
theme: 'dark' | 'light';
}

/** Default settings used in headless mode */
Expand All @@ -40,7 +42,8 @@ const DEFAULT_SETTINGS: MergeNBSettings = {
autoResolveWhitespace: true,
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
showBaseColumn: true
showBaseColumn: true,
theme: 'light'
};

/**
Expand All @@ -64,6 +67,7 @@ export function getSettings(): MergeNBSettings {
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
showBaseColumn: false,
theme: 'light',
};

const config = vscode.workspace.getConfiguration('mergeNB');
Expand All @@ -76,13 +80,17 @@ export function getSettings(): MergeNBSettings {
hideNonConflictOutputs: config.get<boolean>('ui.hideNonConflictOutputs', defaults.hideNonConflictOutputs),
enableUndoRedoHotkeys: config.get<boolean>('ui.enableUndoRedoHotkeys', defaults.enableUndoRedoHotkeys),
showBaseColumn: config.get<boolean>('ui.showBaseColumn', defaults.showBaseColumn),
theme: config.get<'dark' | 'light'>('ui.theme', defaults.theme),
};
}

/**
* Check if a specific auto-resolve setting is enabled
* Check if a specific auto-resolve setting is enabled.
* Only checks actual auto-resolve settings, not UI settings.
*/
export function isAutoResolveEnabled(setting: keyof MergeNBSettings): boolean {
export function isAutoResolveEnabled(
setting: 'autoResolveExecutionCount' | 'autoResolveKernelVersion' | 'stripOutputs' | 'autoResolveWhitespace'
): boolean {
const settings = getSettings();
return settings[setting];
}
1 change: 1 addition & 0 deletions src/tests/logicRegression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export async function run(): Promise<void> {
hideNonConflictOutputs: true,
enableUndoRedoHotkeys: true,
showBaseColumn: true,
theme: 'light',
});
assert.ok(
conflicts.some(c => c.type === 'metadata-changed'),
Expand Down
4 changes: 3 additions & 1 deletion src/web/WebConflictPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export class WebConflictPanel {
void server.openSession(
this._sessionId,
'', // No HTML content needed - server generates shell
(message: unknown) => this._handleMessage(message)
(message: unknown) => this._handleMessage(message),
this._conflict?.theme ?? 'light'
).then(() => {
// Send conflict data to browser once connected
this._sendConflictData();
Expand Down Expand Up @@ -120,6 +121,7 @@ export class WebConflictPanel {
hideNonConflictOutputs: this._conflict.hideNonConflictOutputs,
enableUndoRedoHotkeys: this._conflict.enableUndoRedoHotkeys,
showBaseColumn: this._conflict.showBaseColumn,
theme: this._conflict.theme,
currentBranch: this._conflict.semanticConflict?.currentBranch,
incomingBranch: this._conflict.semanticConflict?.incomingBranch,
};
Expand Down
10 changes: 9 additions & 1 deletion src/web/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
* @description Root React component for the conflict resolver.
*/

import React from 'react';
import React, { useEffect } from 'react';
import { useWebSocket } from './useWebSocket';
import { ConflictResolver } from './ConflictResolver';
import { injectStyles } from './styles';
import type { ConflictChoice, ResolvedRow } from './types';

export function App(): React.ReactElement {
const { connected, conflictData, sendMessage, resolutionStatus, resolutionMessage } = useWebSocket();

// Inject theme styles when conflict data loads
useEffect(() => {
if (conflictData?.theme) {
injectStyles(conflictData.theme);
}
}, [conflictData?.theme]);

const handleResolve = (resolutions: ConflictChoice[], markAsResolved: boolean, renumberExecutionCounts: boolean, resolvedRows: ResolvedRow[]) => {
sendMessage({
command: 'resolve',
Expand Down
9 changes: 8 additions & 1 deletion src/web/client/ConflictResolver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,14 @@ export function ConflictResolver({
<div className="app-container">
<header className="header">
<div className="header-left">
<span className="header-title">MergeNB</span>
<div className="logo-icon">
<div className="logo-card logo-card-left"></div>
<div className="logo-card logo-card-right"></div>
</div>
<div className="header-title">
<span className="header-title-merge">Merge</span>
<span className="header-title-nb">NB</span>
</div>
<span className="file-path">{fileName}</span>
</div>
<div className="header-right">
Expand Down
Binary file added src/web/client/fonts/dm-sans-700-latin.woff2
Binary file not shown.
Binary file not shown.
14 changes: 12 additions & 2 deletions src/web/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ import { createRoot } from 'react-dom/client';
import { App } from './App';
import { injectStyles } from './styles';

// Inject styles into the document
injectStyles();
declare global {
interface Window {
__MERGENB_INITIAL_THEME?: 'dark' | 'light';
}
}

// Use server-provided theme first so loading and app boot with the same palette.
const initialTheme =
window.__MERGENB_INITIAL_THEME ??
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');

injectStyles(initialTheme);

// Mount the React app
const container = document.getElementById('root');
Expand Down
Loading