From 694236116d7f45ef6b2dd007f26fe0baa277ff56 Mon Sep 17 00:00:00 2001 From: Jose Jordan Date: Tue, 23 Jun 2026 17:10:34 +0800 Subject: [PATCH 1/5] chore: isolate webview TypeScript globals --- web/dropdown.ts | 2 +- web/findWidget.ts | 2 +- web/graph.ts | 2 +- web/main.ts | 2 +- web/tsconfig.json | 6 ++++-- web/utils.ts | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/web/dropdown.ts b/web/dropdown.ts index d69407d0..8f3fd635 100644 --- a/web/dropdown.ts +++ b/web/dropdown.ts @@ -17,7 +17,7 @@ class Dropdown { private lastSelected: number = 0; // Only used when multipleAllowed === false private dropdownVisible: boolean = false; private lastClicked: number = 0; - private doubleClickTimeout: NodeJS.Timer | null = null; + private doubleClickTimeout: number | null = null; private readonly elem: HTMLElement; private readonly currentValueElem: HTMLDivElement; diff --git a/web/findWidget.ts b/web/findWidget.ts index ff0e0974..e2d08856 100644 --- a/web/findWidget.ts +++ b/web/findWidget.ts @@ -38,7 +38,7 @@ class FindWidget { document.body.appendChild(this.widgetElem); this.inputElem = document.getElementById('findInput')!; - let keyupTimeout: NodeJS.Timer | null = null; + let keyupTimeout: number | null = null; this.inputElem.addEventListener('keyup', (e) => { if ((e.keyCode ? e.keyCode === 13 : e.key === 'Enter') && this.text !== '') { if (e.shiftKey) { diff --git a/web/graph.ts b/web/graph.ts index 415dfd2e..9aec0212 100644 --- a/web/graph.ts +++ b/web/graph.ts @@ -358,7 +358,7 @@ class Graph { private tooltipId: number = -1; private tooltipElem: HTMLElement | null = null; - private tooltipTimeout: NodeJS.Timer | null = null; + private tooltipTimeout: number | null = null; private tooltipVertex: HTMLElement | null = null; constructor(id: string, viewElem: HTMLElement, config: GG.GraphConfig, muteConfig: GG.MuteCommitsConfig) { diff --git a/web/main.ts b/web/main.ts index 29c23c82..28ec9b46 100644 --- a/web/main.ts +++ b/web/main.ts @@ -1996,7 +1996,7 @@ class GitGraphView { } private observeViewScroll() { - let active = this.viewElem.scrollTop > 0, timeout: NodeJS.Timer | null = null; + let active = this.viewElem.scrollTop > 0, timeout: number | null = null; this.scrollShadowElem.className = active ? CLASS_ACTIVE : ''; this.viewElem.addEventListener('scroll', () => { const scrollTop = this.viewElem.scrollTop; diff --git a/web/tsconfig.json b/web/tsconfig.json index f0981199..a51d4689 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "lib": [ "es6", + "es2016.array.include", "dom" ], "module": "none", @@ -13,6 +14,7 @@ "outDir": "../media", "removeComments": true, "strict": true, - "target": "es5" + "target": "es5", + "types": [] } -} \ No newline at end of file +} diff --git a/web/utils.ts b/web/utils.ts index a63b64c8..ef2ce412 100644 --- a/web/utils.ts +++ b/web/utils.ts @@ -470,7 +470,7 @@ function observeElemScroll(id: string, initialScrollTop: number, onScroll: (scro const elem = document.getElementById(id); if (elem === null) return; - let timeout: NodeJS.Timer | null = null; + let timeout: number | null = null; elem.scroll(0, initialScrollTop); elem.addEventListener('scroll', () => { const elem = document.getElementById(id); From a310dac578f47c66c58061aa29cb60b07b8b3043 Mon Sep 17 00:00:00 2001 From: Jose Jordan Date: Tue, 23 Jun 2026 17:11:44 +0800 Subject: [PATCH 2/5] docs: add switch repository keyboard design --- ...06-23-switch-repository-keyboard-design.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md diff --git a/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md b/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md new file mode 100644 index 00000000..27ffc805 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md @@ -0,0 +1,93 @@ +# Switch Repository Keyboard Access Design + +## Goal + +Allow users to switch the focused Git Graph repository without using the mouse. + +## Requirements + +- The existing Git Graph repository dropdown remains the primary in-webview repository switcher. +- After opening the repository dropdown and filtering repositories by typing, `ArrowUp` and `ArrowDown` move through matching visible repositories. +- Pressing `Enter` in the repository dropdown activates the currently highlighted matching repository. +- A command palette command named `Git Graph: Switch Repository...` opens a native VS Code QuickPick containing the repositories currently known to Git Graph. +- Selecting a repository from the command palette QuickPick opens or focuses Git Graph and loads that repository. +- Repository names and ordering must match Git Graph's existing repository dropdown behavior. +- The implementation must be small and upstream-friendly, without replacing the existing dropdown UI. +- Users must be able to build a local VSIX and install it if the upstream pull request is not released. + +## Recommended Approach + +Reuse existing repository-switching primitives: + +- Extend the generic webview `Dropdown` component with keyboard navigation for filtered options. +- Add a focused extension command, `git-graph.switchRepository`, contributed as `Git Graph: Switch Repository...`. +- Reuse `RepoManager.getRepos()`, `getSortedRepositoryPaths()`, `getRepoName()`, and `GitGraphView.createOrShow()` in the command handler. + +This keeps the feature close to existing code paths and avoids a repo-specific dropdown fork. + +## Component Design + +### Webview Dropdown + +`web/dropdown.ts` owns dropdown rendering, filtering, and selection. + +Add internal highlighted-option state for visible filtered options. When the filter text changes, initialize the highlight to the first visible option. While the filter input is focused: + +- `ArrowDown` moves the highlight to the next visible option, wrapping at the end. +- `ArrowUp` moves the highlight to the previous visible option, wrapping at the beginning. +- `Enter` activates the highlighted option by reusing the existing option selection path. + +The behavior should apply to the existing generic dropdown component, but must preserve current mouse and multi-select branch dropdown behavior. + +### Command Palette + +`src/commands.ts` owns command registration. + +Add `git-graph.switchRepository` and implement it as a native QuickPick. The QuickPick items should use: + +- `label`: configured repository name or short repository name. +- `description`: full repository path. +- sorted order: `getSortedRepositoryPaths(repos, getConfig().repoDropdownOrder)`. + +When a repository is selected, call `GitGraphView.createOrShow()` with `{ repo: selectedPath }`. + +For zero known repositories, open Git Graph normally so the existing no-repo flow is preserved. For one known repository, open that repository directly without forcing an unnecessary picker. + +### Metadata And Docs + +Update `package.json` command contributions and README command list. If a changelog entry is expected by upstream conventions, add a concise unreleased or top-entry bullet only if the repository already has that pattern. + +## Testing + +Add focused tests for the command palette path in `tests/commands.test.ts`: + +- command is registered and counted in `CommandManager` construction expectations. +- multiple repos produce a QuickPick in existing repository dropdown order. +- selecting a repo calls `GitGraphView.createOrShow()` with the selected repo. +- no selection does not open a specific repo. +- one repo opens directly. +- no repos opens Git Graph with `null`. + +For dropdown keyboard behavior, prefer a focused DOM-capable test if the current test setup supports it. If it does not, rely on TypeScript compile plus manual verification in an Extension Development Host. + +## Verification + +Run the applicable project checks: + +- `npm install` +- `npm run lint` +- `npm test` +- `npm run compile` + +Package and local install path: + +- `npm install -g vsce` if `vsce` is not available. +- Use upstream's documented local flow: `npm run package-and-install`. +- Restart VS Code or Cursor and verify the installed Git Graph version is the local build. + +## Out Of Scope + +- Replacing Git Graph's repository dropdown with VS Code QuickPick. +- Adding custom keybindings beyond command palette access. +- Changing repository discovery or repository sorting semantics. +- Changing branch dropdown filtering semantics except where generic keyboard navigation necessarily shares dropdown code. From 1de0eb4afcb2a95c7607b791e25060f3a095a9c1 Mon Sep 17 00:00:00 2001 From: Jose Jordan Date: Tue, 23 Jun 2026 17:23:10 +0800 Subject: [PATCH 3/5] chore: bump version to 1.30.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d024431..c4afdeae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "git-graph", "displayName": "Git Graph", - "version": "1.30.0", + "version": "1.30.1", "publisher": "mhutchie", "author": { "name": "Michael Hutchison", From 1c667e4e8282285f0f20679203dd7db8b5c7e8e1 Mon Sep 17 00:00:00 2001 From: Jose Jordan Date: Tue, 23 Jun 2026 17:34:02 +0800 Subject: [PATCH 4/5] feat: add keyboard repository switching --- CHANGELOG.md | 6 +- README.md | 3 +- package.json | 5 ++ src/commands.ts | 35 +++++++++++ tests/commands.test.ts | 133 +++++++++++++++++++++++++++++++++++++++- web/dropdown.ts | 82 ++++++++++++++++++++++++- web/styles/dropdown.css | 3 + 7 files changed, 261 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d7015b..6fc20cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 1.30.1 - 2026-06-23 +* Added a new "Git Graph: Switch Repository..." command to switch repositories from the Command Palette. +* Added keyboard navigation to dropdown filters, allowing `Up` / `Down` to choose a filtered option and `Enter` to select it. + ## 1.30.0 - 2021-04-05 * #395 Added a "Force Fetch" option onto the "Fetch into Local Branch" Dialog, allowing any local branch (that's not checked out) to be reset to the remote branch. This dialog is accessed via the Remote Branch Context Menu. * #457 New "View Diff with Working File" action on the File Context Menu in the Commit Details View. @@ -439,4 +443,4 @@ * `git-graph.repository.commits.loadMore` - Specifies the number of commits to load when the "Load More Commits" button is pressed (only shown when more commits are available). * `git-graph.showStatusBarItem` - Show a Status Bar item which opens Git Graph when clicked. * `git-graph.repository.showUncommittedChanges` - Show uncommitted changes (set to false to decrease load time on large repositories). -* Shortcut Button in the Status Bar \ No newline at end of file +* Shortcut Button in the Status Bar diff --git a/README.md b/README.md index 022a5c4f..8d5b0eb7 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ This extension contributes the following commands: * `git-graph.fetch`: Git Graph: Fetch from Remote(s) _(used to open the Git Graph View and immediately run "Fetch from Remote(s)")_ * `git-graph.removeGitRepository`: Git Graph: Remove Git Repository... _(used to remove repositories from Git Graph)_ * `git-graph.resumeWorkspaceCodeReview`: Git Graph: Resume a specific Code Review in Workspace... _(used to open the Git Graph View to a Code Review that is already in progress)_ +* `git-graph.switchRepository`: Git Graph: Switch Repository... _(used to switch the repository open in the Git Graph View)_ * `git-graph.version`: Git Graph: Get Version Information ## Release Notes @@ -171,4 +172,4 @@ Thank you to all of the contributors that help with the development of Git Graph Some of the icons used in Git Graph are from the following sources, please support them for their excellent work! - [GitHub Octicons](https://octicons.github.com/) ([License](https://github.com/primer/octicons/blob/master/LICENSE)) -- [Icons8](https://icons8.com/icon/pack/free-icons/ios11) ([License](https://icons8.com/license)) \ No newline at end of file +- [Icons8](https://icons8.com/icon/pack/free-icons/ios11) ([License](https://icons8.com/license)) diff --git a/package.json b/package.json index c4afdeae..d0272f18 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "command": "git-graph.removeGitRepository", "title": "Remove Git Repository..." }, + { + "category": "Git Graph", + "command": "git-graph.switchRepository", + "title": "Switch Repository..." + }, { "category": "Git Graph", "command": "git-graph.resumeWorkspaceCodeReview", diff --git a/src/commands.ts b/src/commands.ts index a6474a68..725a8880 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -53,6 +53,7 @@ export class CommandManager extends Disposable { this.registerCommand('git-graph.fetch', () => this.fetch()); this.registerCommand('git-graph.endAllWorkspaceCodeReviews', () => this.endAllWorkspaceCodeReviews()); this.registerCommand('git-graph.endSpecificWorkspaceCodeReview', () => this.endSpecificWorkspaceCodeReview()); + this.registerCommand('git-graph.switchRepository', () => this.switchRepository()); this.registerCommand('git-graph.resumeWorkspaceCodeReview', () => this.resumeWorkspaceCodeReview()); this.registerCommand('git-graph.version', () => this.version()); this.registerCommand('git-graph.openFile', (arg) => this.openFile(arg)); @@ -239,6 +240,40 @@ export class CommandManager extends Disposable { } } + /** + * The method run when the `git-graph.switchRepository` command is invoked. + */ + private switchRepository() { + const repos = this.repoManager.getRepos(); + const repoPaths = getSortedRepositoryPaths(repos, getConfig().repoDropdownOrder); + + if (repoPaths.length > 1) { + const items: vscode.QuickPickItem[] = repoPaths.map((path) => ({ + label: repos[path].name || getRepoName(path), + description: path + })); + + vscode.window.showQuickPick(items, { + placeHolder: 'Select the repository you want to open in Git Graph:', + canPickMany: false + }).then((item) => { + if (item && item.description) { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, { + repo: item.description + }); + } + }, () => { + showErrorMessage('An unexpected error occurred while running the command "Switch Repository".'); + }); + } else if (repoPaths.length === 1) { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, { + repo: repoPaths[0] + }); + } else { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, null); + } + } + /** * The method run when the `git-graph.endAllWorkspaceCodeReviews` command is invoked. */ diff --git a/tests/commands.test.ts b/tests/commands.test.ts index 921105ba..8d458e6d 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -73,7 +73,7 @@ describe('CommandManager', () => { it('Should construct a CommandManager, and be disposed', () => { // Assert - expect(commandManager['disposables']).toHaveLength(11); + expect(commandManager['disposables']).toHaveLength(12); expect(commandManager['gitExecutable']).toStrictEqual({ path: '/path/to/git', version: '2.25.0' @@ -805,6 +805,137 @@ describe('CommandManager', () => { }); }); + describe('git-graph.switchRepository', () => { + it('Should display a quick pick to select a repository to open in the Git Graph View', async () => { + // Setup + const repos = { + '/path/to/repo3': mockRepoState({ name: null, workspaceFolderIndex: 2 }), + '/path/to/repo2': mockRepoState({ name: 'Custom Name', workspaceFolderIndex: 1 }), + '/path/to/repo1': mockRepoState({ name: null, workspaceFolderIndex: 0 }) + }; + spyOnGetRepos.mockReturnValueOnce(repos); + vscode.window.showQuickPick.mockResolvedValueOnce({ + label: 'repo3', + description: '/path/to/repo3' + }); + const spyOnGetSortedRepositoryPaths = jest.spyOn(utils, 'getSortedRepositoryPaths'); + + // Run + vscode.commands.executeCommand('git-graph.switchRepository'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.switchRepository'); + expect(spyOnGetSortedRepositoryPaths).toHaveBeenCalledWith(repos, RepoDropdownOrder.WorkspaceFullPath); + expect(vscode.window.showQuickPick).toHaveBeenCalledWith( + [ + { + label: 'repo1', + description: '/path/to/repo1' + }, + { + label: 'Custom Name', + description: '/path/to/repo2' + }, + { + label: 'repo3', + description: '/path/to/repo3' + } + ], + { + placeHolder: 'Select the repository you want to open in Git Graph:', + canPickMany: false + } + ); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo3' }); + }); + }); + + it('Shouldn\'t open the Git Graph View when no item is selected in the quick pick', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState({ name: null, workspaceFolderIndex: 0 }), + '/path/to/repo2': mockRepoState({ name: 'Custom Name', workspaceFolderIndex: 0 }) + }); + vscode.window.showQuickPick.mockResolvedValueOnce(null); + + // Run + vscode.commands.executeCommand('git-graph.switchRepository'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.switchRepository'); + expect(vscode.window.showQuickPick).toHaveBeenCalledWith( + [ + { + label: 'repo1', + description: '/path/to/repo1' + }, + { + label: 'Custom Name', + description: '/path/to/repo2' + } + ], + { + placeHolder: 'Select the repository you want to open in Git Graph:', + canPickMany: false + } + ); + expect(spyOnGitGraphViewCreateOrShow).not.toHaveBeenCalled(); + }); + }); + + it('Should display an error message when showQuickPick rejects', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState({ name: null, workspaceFolderIndex: 0 }), + '/path/to/repo2': mockRepoState({ name: 'Custom Name', workspaceFolderIndex: 0 }) + }); + vscode.window.showQuickPick.mockRejectedValueOnce(null); + vscode.window.showErrorMessage.mockResolvedValueOnce(null); + + // Run + vscode.commands.executeCommand('git-graph.switchRepository'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.switchRepository'); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('An unexpected error occurred while running the command "Switch Repository".'); + expect(spyOnGitGraphViewCreateOrShow).not.toHaveBeenCalled(); + }); + }); + + it('Should open the Git Graph View immediately when there is only one repository', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState({ name: null, workspaceFolderIndex: 0 }) + }); + + // Run + vscode.commands.executeCommand('git-graph.switchRepository'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.switchRepository'); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo1' }); + }); + }); + + it('Should open the Git Graph View immediately when there are no repositories', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({}); + + // Run + vscode.commands.executeCommand('git-graph.switchRepository'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.switchRepository'); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + }); + }); + }); + describe('git-graph.endAllWorkspaceCodeReviews', () => { it('Should end all workspace code reviews', () => { // Setup diff --git a/web/dropdown.ts b/web/dropdown.ts index 8f3fd635..061c2801 100644 --- a/web/dropdown.ts +++ b/web/dropdown.ts @@ -18,6 +18,7 @@ class Dropdown { private dropdownVisible: boolean = false; private lastClicked: number = 0; private doubleClickTimeout: number | null = null; + private keyboardFocus: number | null = null; private readonly elem: HTMLElement; private readonly currentValueElem: HTMLDivElement; @@ -86,7 +87,11 @@ class Dropdown { } }, true); document.addEventListener('contextmenu', () => this.close(), true); - this.filterInput.addEventListener('keyup', () => this.filter()); + this.filterInput.addEventListener('keydown', (e) => this.onFilterInputKeyDown(e)); + this.filterInput.addEventListener('keyup', (e) => { + if (isKeyboardNavigationEvent(e)) return; + this.filter(); + }); } /** @@ -208,6 +213,7 @@ class Dropdown { public close() { this.elem.classList.remove('dropdownOpen'); this.dropdownVisible = false; + this.keyboardFocus = null; this.clearDoubleClickTimeout(); } @@ -247,16 +253,82 @@ class Dropdown { * Filter the options displayed in the dropdown list, based on the filter criteria specified by the user. */ private filter() { - let val = this.filterInput.value.toLowerCase(), match, matches = false; + let val = this.filterInput.value.toLowerCase(), match, matches = false, firstMatch: number | null = null; for (let i = 0; i < this.options.length; i++) { match = this.options[i].name.toLowerCase().indexOf(val) > -1; (this.optionsElem.children[i]).style.display = match ? 'block' : 'none'; - if (match) matches = true; + if (match) { + matches = true; + if (firstMatch === null) firstMatch = i; + } } + this.keyboardFocus = firstMatch; + this.renderKeyboardFocus(); this.filterInput.style.display = 'block'; this.noResultsElem.style.display = matches ? 'none' : 'block'; } + /** + * Handle keyboard navigation in the dropdown filter input. + * @param e The keyboard event. + */ + private onFilterInputKeyDown(e: KeyboardEvent) { + if (e.key === 'ArrowDown' || e.keyCode === 40) { + this.moveKeyboardFocus(1); + handledEvent(e); + } else if (e.key === 'ArrowUp' || e.keyCode === 38) { + this.moveKeyboardFocus(-1); + handledEvent(e); + } else if (e.key === 'Enter' || e.keyCode === 13) { + if (this.keyboardFocus !== null) { + this.onOptionClick(this.keyboardFocus); + handledEvent(e); + } + } + } + + /** + * Move the keyboard focus between filtered dropdown options. + * @param direction The direction to move, either 1 for down or -1 for up. + */ + private moveKeyboardFocus(direction: number) { + const visibleOptions = this.getVisibleOptions(); + if (visibleOptions.length === 0) return; + + const currentVisibleOptionIndex = this.keyboardFocus !== null ? visibleOptions.indexOf(this.keyboardFocus) : -1; + const nextVisibleOptionIndex = currentVisibleOptionIndex === -1 + ? direction > 0 ? 0 : visibleOptions.length - 1 + : (currentVisibleOptionIndex + direction + visibleOptions.length) % visibleOptions.length; + this.keyboardFocus = visibleOptions[nextVisibleOptionIndex]; + this.renderKeyboardFocus(); + } + + /** + * Get the option indexes that are visible after filtering. + * @returns The visible option indexes. + */ + private getVisibleOptions() { + let visibleOptions = []; + for (let i = 0; i < this.optionsElem.children.length; i++) { + if ((this.optionsElem.children[i]).style.display !== 'none') { + visibleOptions.push(i); + } + } + return visibleOptions; + } + + /** + * Render the keyboard focus state on dropdown options. + */ + private renderKeyboardFocus() { + for (let i = 0; i < this.optionsElem.children.length; i++) { + alterClass(this.optionsElem.children[i], CLASS_ACTIVE, this.keyboardFocus === i); + } + if (this.keyboardFocus !== null) { + (this.optionsElem.children[this.keyboardFocus]).scrollIntoView({ block: 'nearest' }); + } + } + /** * Get an array of the selected dropdown options. * @param names TRUE => Return the names of the selected options, FALSE => Return the values of the selected options. @@ -359,3 +431,7 @@ class Dropdown { } } } + +function isKeyboardNavigationEvent(e: KeyboardEvent) { + return e.key === 'ArrowDown' || e.keyCode === 40 || e.key === 'ArrowUp' || e.keyCode === 38 || e.key === 'Enter' || e.keyCode === 13; +} diff --git a/web/styles/dropdown.css b/web/styles/dropdown.css index cabf69ae..980da870 100644 --- a/web/styles/dropdown.css +++ b/web/styles/dropdown.css @@ -79,10 +79,12 @@ .dropdownOption.selected{ background-color:rgba(128,128,128,0.15); } +.dropdownOption.active, .dropdownOption:hover{ background-color:var(--vscode-menu-selectionBackground, var(--vscode-menu-background)); color:var(--vscode-menu-selectionForeground, var(--vscode-menu-foreground)); } +body.vscode-high-contrast .dropdownOption.active, body.vscode-high-contrast .dropdownOption:hover{ outline:1px dotted var(--vscode-menu-selectionBorder, transparent); outline-offset:-2px; @@ -115,6 +117,7 @@ body.vscode-high-contrast .dropdownOption:hover{ opacity:0.7; } +.dropdownOption.active svg, .dropdownOption:hover svg{ fill:var(--vscode-menu-selectionForeground, var(--vscode-menu-foreground)); opacity:0.85; From 91f83885da93cdbb47c1faddce4f9fd748d7e430 Mon Sep 17 00:00:00 2001 From: Jose Jordan Date: Tue, 23 Jun 2026 18:00:58 +0800 Subject: [PATCH 5/5] chore: prepare upstream pull request --- CHANGELOG.md | 6 +- ...06-23-switch-repository-keyboard-design.md | 93 ------------------- package.json | 2 +- 3 files changed, 2 insertions(+), 99 deletions(-) delete mode 100644 docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fc20cd7..96d7015b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Change Log -## 1.30.1 - 2026-06-23 -* Added a new "Git Graph: Switch Repository..." command to switch repositories from the Command Palette. -* Added keyboard navigation to dropdown filters, allowing `Up` / `Down` to choose a filtered option and `Enter` to select it. - ## 1.30.0 - 2021-04-05 * #395 Added a "Force Fetch" option onto the "Fetch into Local Branch" Dialog, allowing any local branch (that's not checked out) to be reset to the remote branch. This dialog is accessed via the Remote Branch Context Menu. * #457 New "View Diff with Working File" action on the File Context Menu in the Commit Details View. @@ -443,4 +439,4 @@ * `git-graph.repository.commits.loadMore` - Specifies the number of commits to load when the "Load More Commits" button is pressed (only shown when more commits are available). * `git-graph.showStatusBarItem` - Show a Status Bar item which opens Git Graph when clicked. * `git-graph.repository.showUncommittedChanges` - Show uncommitted changes (set to false to decrease load time on large repositories). -* Shortcut Button in the Status Bar +* Shortcut Button in the Status Bar \ No newline at end of file diff --git a/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md b/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md deleted file mode 100644 index 27ffc805..00000000 --- a/docs/superpowers/specs/2026-06-23-switch-repository-keyboard-design.md +++ /dev/null @@ -1,93 +0,0 @@ -# Switch Repository Keyboard Access Design - -## Goal - -Allow users to switch the focused Git Graph repository without using the mouse. - -## Requirements - -- The existing Git Graph repository dropdown remains the primary in-webview repository switcher. -- After opening the repository dropdown and filtering repositories by typing, `ArrowUp` and `ArrowDown` move through matching visible repositories. -- Pressing `Enter` in the repository dropdown activates the currently highlighted matching repository. -- A command palette command named `Git Graph: Switch Repository...` opens a native VS Code QuickPick containing the repositories currently known to Git Graph. -- Selecting a repository from the command palette QuickPick opens or focuses Git Graph and loads that repository. -- Repository names and ordering must match Git Graph's existing repository dropdown behavior. -- The implementation must be small and upstream-friendly, without replacing the existing dropdown UI. -- Users must be able to build a local VSIX and install it if the upstream pull request is not released. - -## Recommended Approach - -Reuse existing repository-switching primitives: - -- Extend the generic webview `Dropdown` component with keyboard navigation for filtered options. -- Add a focused extension command, `git-graph.switchRepository`, contributed as `Git Graph: Switch Repository...`. -- Reuse `RepoManager.getRepos()`, `getSortedRepositoryPaths()`, `getRepoName()`, and `GitGraphView.createOrShow()` in the command handler. - -This keeps the feature close to existing code paths and avoids a repo-specific dropdown fork. - -## Component Design - -### Webview Dropdown - -`web/dropdown.ts` owns dropdown rendering, filtering, and selection. - -Add internal highlighted-option state for visible filtered options. When the filter text changes, initialize the highlight to the first visible option. While the filter input is focused: - -- `ArrowDown` moves the highlight to the next visible option, wrapping at the end. -- `ArrowUp` moves the highlight to the previous visible option, wrapping at the beginning. -- `Enter` activates the highlighted option by reusing the existing option selection path. - -The behavior should apply to the existing generic dropdown component, but must preserve current mouse and multi-select branch dropdown behavior. - -### Command Palette - -`src/commands.ts` owns command registration. - -Add `git-graph.switchRepository` and implement it as a native QuickPick. The QuickPick items should use: - -- `label`: configured repository name or short repository name. -- `description`: full repository path. -- sorted order: `getSortedRepositoryPaths(repos, getConfig().repoDropdownOrder)`. - -When a repository is selected, call `GitGraphView.createOrShow()` with `{ repo: selectedPath }`. - -For zero known repositories, open Git Graph normally so the existing no-repo flow is preserved. For one known repository, open that repository directly without forcing an unnecessary picker. - -### Metadata And Docs - -Update `package.json` command contributions and README command list. If a changelog entry is expected by upstream conventions, add a concise unreleased or top-entry bullet only if the repository already has that pattern. - -## Testing - -Add focused tests for the command palette path in `tests/commands.test.ts`: - -- command is registered and counted in `CommandManager` construction expectations. -- multiple repos produce a QuickPick in existing repository dropdown order. -- selecting a repo calls `GitGraphView.createOrShow()` with the selected repo. -- no selection does not open a specific repo. -- one repo opens directly. -- no repos opens Git Graph with `null`. - -For dropdown keyboard behavior, prefer a focused DOM-capable test if the current test setup supports it. If it does not, rely on TypeScript compile plus manual verification in an Extension Development Host. - -## Verification - -Run the applicable project checks: - -- `npm install` -- `npm run lint` -- `npm test` -- `npm run compile` - -Package and local install path: - -- `npm install -g vsce` if `vsce` is not available. -- Use upstream's documented local flow: `npm run package-and-install`. -- Restart VS Code or Cursor and verify the installed Git Graph version is the local build. - -## Out Of Scope - -- Replacing Git Graph's repository dropdown with VS Code QuickPick. -- Adding custom keybindings beyond command palette access. -- Changing repository discovery or repository sorting semantics. -- Changing branch dropdown filtering semantics except where generic keyboard navigation necessarily shares dropdown code. diff --git a/package.json b/package.json index d0272f18..7650db35 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "git-graph", "displayName": "Git Graph", - "version": "1.30.1", + "version": "1.30.0", "publisher": "mhutchie", "author": { "name": "Michael Hutchison",