From 5b4a8faf85852c6ec6be080270873588e4e41efb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 10:13:03 +0000 Subject: [PATCH 1/2] Add stickyHeader setting to keep graph header fixed while scrolling Introduce git-graph.stickyHeader (default true) so the control bar and column headers stay visible when scrolling the Graph View, following the approach from upstream PR #394. Co-authored-by: Philipp Litzenberger --- README.md | 1 + package.json | 5 +++++ src/config.ts | 7 +++++++ src/gitGraphView.ts | 8 +++++--- src/types.ts | 1 + tests/config.test.ts | 2 ++ tests/gitGraphView.test.ts | 17 +++++++++++++++++ web/contextMenu.ts | 2 +- web/main.ts | 30 +++++++++++++++++++++++++++++- web/styles/contextMenu.css | 4 ++-- web/styles/main.css | 22 ++++++++++++++++++---- 11 files changed, 88 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 022a5c4f..fa706c53 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ A summary of the Git Graph extension settings are: * **Retain Context When Hidden**: Specifies if the Git Graph view Visual Studio Code context is kept when the panel is no longer visible (e.g. moved to background tab). Enabling this setting will make Git Graph load significantly faster when switching back to the Git Graph tab, however has a higher memory overhead. * **Show Status Bar Item**: Show a Status Bar Item that opens the Git Graph View when clicked. * **Source Code Provider Integration Location**: Specifies where the "View Git Graph" action appears on the title of SCM Providers. +* **Sticky Header**: Keeps the Graph View header (control bar and column headers) visible when the view is scrolled. * **Tab Icon Colour Theme**: Specifies the colour theme of the icon displayed on the Git Graph tab. This extension consumes the following settings: diff --git a/package.json b/package.json index 3d024431..849338db 100644 --- a/package.json +++ b/package.json @@ -1155,6 +1155,11 @@ "default": "Inline", "description": "Specifies where the \"View Git Graph\" action appears on the title of SCM Providers." }, + "git-graph.stickyHeader": { + "type": "boolean", + "default": true, + "description": "Keeps the Graph View header (control bar and column headers) visible when the view is scrolled." + }, "git-graph.tabIconColourTheme": { "type": "string", "enum": [ diff --git a/src/config.ts b/src/config.ts index 8d777ca8..76570bab 100644 --- a/src/config.ts +++ b/src/config.ts @@ -546,6 +546,13 @@ class Config { return !!this.config.get('showStatusBarItem', true); } + /** + * Get the value of the `git-graph.stickyHeader` Extension Setting. + */ + get stickyHeader() { + return !!this.config.get('stickyHeader', true); + } + /** * Get the value of the `git-graph.tabIconColourTheme` Extension Setting. */ diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index 1acba6ff..6430052c 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -692,7 +692,8 @@ export class GitGraphView extends Disposable { repoDropdownOrder: config.repoDropdownOrder, showRemoteBranches: config.showRemoteBranches, showStashes: config.showStashes, - showTags: config.showTags + showTags: config.showTags, + stickyHeader: config.stickyHeader }, lastActiveRepo: this.extensionState.getLastActiveRepo(), loadViewTo: this.loadViewTo, @@ -715,9 +716,10 @@ export class GitGraphView extends Disposable {

${UNABLE_TO_FIND_GIT_MSG}

`; } else if (numRepos > 0) { + const stickyClassAttr = initialState.config.stickyHeader ? ' class="sticky"' : ''; body = `
-
+
Repo: Branches: @@ -732,8 +734,8 @@ export class GitGraphView extends Disposable {
+
-
`; diff --git a/src/types.ts b/src/types.ts index c543fa9f..7235b88d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -260,6 +260,7 @@ export interface GitGraphViewConfig { readonly showRemoteBranches: boolean; readonly showStashes: boolean; readonly showTags: boolean; + readonly stickyHeader: boolean; } export interface GitGraphViewGlobalState { diff --git a/tests/config.test.ts b/tests/config.test.ts index 69924d51..570fe16e 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -2887,6 +2887,8 @@ describe('Config', () => { describe('showStatusBarItem', testBooleanExtensionSetting('showStatusBarItem', 'showStatusBarItem', true)); + describe('stickyHeader', testBooleanExtensionSetting('stickyHeader', 'stickyHeader', true)); + describe('tabIconColourTheme', () => { it('Should return TabIconColourTheme.Colour when the configuration value is "colour"', () => { // Setup diff --git a/tests/gitGraphView.test.ts b/tests/gitGraphView.test.ts index 00dca82d..a11aa84b 100644 --- a/tests/gitGraphView.test.ts +++ b/tests/gitGraphView.test.ts @@ -3668,6 +3668,9 @@ describe('GitGraphView', () => { // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); expect(mockedWebviewPanel.panel.webview.html).toContain('
'); + expect(mockedWebviewPanel.panel.webview.html).toContain('
'); + expect(mockedWebviewPanel.panel.webview.html).toContain('"stickyHeader":true'); + expect(mockedWebviewPanel.panel.webview.html).toContain('
\n\t\t\t
'); expect(mockedWebviewPanel.panel.webview.html).toContain(''); expect(spyOnIsAvatarStorageAvailable).not.toHaveBeenCalled(); @@ -3688,6 +3691,20 @@ describe('GitGraphView', () => { expect(mockedWebviewPanel.panel.webview.html).toContain(''); expect(spyOnIsAvatarStorageAvailable).toHaveBeenCalledWith(); }); + + it('Should omit the sticky class when stickyHeader is disabled', () => { + // Setup + vscode.mockExtensionSettingReturnValue('stickyHeader', false); + + // Run + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + + // Assert + const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); + expect(mockedWebviewPanel.panel.webview.html).toContain('
'); + expect(mockedWebviewPanel.panel.webview.html).not.toContain('
'); + expect(mockedWebviewPanel.panel.webview.html).toContain('"stickyHeader":false'); + }); }); }); diff --git a/web/contextMenu.ts b/web/contextMenu.ts index 5675e492..deb5d65d 100644 --- a/web/contextMenu.ts +++ b/web/contextMenu.ts @@ -83,7 +83,7 @@ class ContextMenu { ? 2 - menuBounds.height // context menu fits above : -2 - (menuBounds.height - (frameBounds.height - (event.pageY - frameBounds.top))); // Overlap the context menu vertically with the cursor menu.style.left = (frameElem.scrollLeft + Math.max(event.pageX - frameBounds.left + relativeX, 2)) + 'px'; - menu.style.top = (frameElem.scrollTop + Math.max(event.pageY - frameBounds.top + relativeY, 2)) + 'px'; + menu.style.top = Math.max(event.clientY - frameBounds.top + relativeY, 2) + 'px'; menu.style.opacity = '1'; this.elem = menu; this.onClose = onClose; diff --git a/web/main.ts b/web/main.ts index 29c23c82..3678ab1a 100644 --- a/web/main.ts +++ b/web/main.ts @@ -49,6 +49,7 @@ class GitGraphView { private readonly viewElem: HTMLElement; private readonly controlsElem: HTMLElement; private readonly tableElem: HTMLElement; + private tableColHeadersElem: HTMLElement | null; private readonly footerElem: HTMLElement; private readonly showRemoteBranchesElem: HTMLInputElement; private readonly refreshBtnElem: HTMLElement; @@ -72,6 +73,7 @@ class GitGraphView { this.controlsElem = document.getElementById('controls')!; this.tableElem = document.getElementById('commitTable')!; + this.tableColHeadersElem = document.getElementById('tableColHeaders'); this.footerElem = document.getElementById('footer')!; this.scrollShadowElem = document.getElementById('scrollShadow')!; @@ -818,7 +820,8 @@ class GitGraphView { markdown: this.config.markdown }); - let html = 'GraphDescription' + + const stickyClassAttr = this.config.stickyHeader ? ' class="sticky"' : ''; + let html = 'GraphDescription' + (colVisibility.date ? 'Date' : '') + (colVisibility.author ? 'Author' : '') + (colVisibility.commit ? 'Commit' : '') + @@ -920,6 +923,11 @@ class GitGraphView { } } } + + if (this.config.stickyHeader) { + this.tableColHeadersElem = document.getElementById('tableColHeaders'); + this.alignTableHeaderToControls(); + } } private renderUncommittedChanges() { @@ -1941,6 +1949,22 @@ class GitGraphView { /* Observers */ + private alignTableHeaderToControls() { + if (!this.tableColHeadersElem) { + return; + } + + const controlsHeight = this.controlsElem.offsetHeight; + const controlsWidth = this.controlsElem.offsetWidth; + const tableColHeadersHeight = this.tableColHeadersElem.offsetHeight; + const bottomBorderWidth = 1; + const shadowYPos = controlsHeight + tableColHeadersHeight + bottomBorderWidth; + + this.tableColHeadersElem.style.top = `${controlsHeight}px`; + this.scrollShadowElem.style.top = `${shadowYPos}px`; + this.scrollShadowElem.style.width = `${controlsWidth}px`; + } + private observeWindowSizeChanges() { let windowWidth = window.outerWidth, windowHeight = window.outerHeight; window.addEventListener('resize', () => { @@ -1950,6 +1974,10 @@ class GitGraphView { windowWidth = window.outerWidth; windowHeight = window.outerHeight; } + + if (this.config.stickyHeader) { + this.alignTableHeaderToControls(); + } }); } diff --git a/web/styles/contextMenu.css b/web/styles/contextMenu.css index d0e42b2f..76886360 100644 --- a/web/styles/contextMenu.css +++ b/web/styles/contextMenu.css @@ -1,13 +1,13 @@ .contextMenu{ display:block; - position:absolute; + position:fixed; background-color:var(--vscode-menu-background); box-shadow:0 1px 4px 1px var(--vscode-widget-shadow); color:var(--vscode-menu-foreground); list-style-type:none; margin:0; padding:4px 0; - z-index:10; + z-index:13; -webkit-user-select:none; user-select:none; } diff --git a/web/styles/main.css b/web/styles/main.css index 8c30d495..37cefd18 100644 --- a/web/styles/main.css +++ b/web/styles/main.css @@ -74,9 +74,10 @@ code{ top:0; left:0; right:0; - height:0; - box-shadow:0 -6px 6px 6px var(--vscode-scrollbar-shadow); - z-index:20; + height:6px; + box-shadow:inset 0 6px 6px -6px var(--vscode-scrollbar-shadow); + z-index:11; + pointer-events:none; } @@ -214,7 +215,8 @@ code{ padding:0 4px; } #commitTable th{ - border-bottom:1px solid rgba(128,128,128,0.5); + border-bottom:1px solid transparent; + box-shadow:0 1px 0 rgba(128,128,128,0.5); line-height:18px; padding:6px 12px; } @@ -277,6 +279,12 @@ code{ .tableColHeader{ position:relative; } +#tableColHeaders.sticky .tableColHeader{ + position:sticky; + top:inherit; + z-index:11; + background-color:var(--vscode-editor-background); +} .resizeCol{ position:absolute; top:0; @@ -781,6 +789,7 @@ body.tagLabelsRightAligned .gitRef.tag{ right:0; top:0; padding:4px 132px 4px 0; + background-color:var(--vscode-editor-background); border-bottom:1px solid rgba(128,128,128,0.5); line-height:32px; text-align:center; @@ -790,6 +799,11 @@ body.tagLabelsRightAligned .gitRef.tag{ user-select:none; } +#controls.sticky{ + position:sticky; + z-index:12; +} + #repoControl, #branchControl, #showRemoteBranchesControl{ display:inline-block; white-space:nowrap; From f7b3d8958e012438069e34075ade156192c216fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 10:14:39 +0000 Subject: [PATCH 2/2] Fix web TypeScript compile with current @types packages Restrict web/tsconfig types to node and include es2016.array.include so tsc no longer pulls in incompatible babel declaration files. Co-authored-by: Philipp Litzenberger --- web/tsconfig.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/tsconfig.json b/web/tsconfig.json index f0981199..5bcda9c8 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "lib": [ "es6", - "dom" + "dom", + "es2016.array.include" ], "module": "none", "moduleResolution": "node", @@ -13,6 +14,9 @@ "outDir": "../media", "removeComments": true, "strict": true, - "target": "es5" + "target": "es5", + "types": [ + "node" + ] } } \ No newline at end of file