diff --git a/client/src/CommandBar/steps/Initial.tsx b/client/src/CommandBar/steps/Initial.tsx
index 8722dbf617..5ae2105694 100644
--- a/client/src/CommandBar/steps/Initial.tsx
+++ b/client/src/CommandBar/steps/Initial.tsx
@@ -93,12 +93,11 @@ const InitialCommandBar = ({}: Props) => {
];
const projectItems: CommandBarItemGeneralType[] = projects
.map(
- (p, i): CommandBarItemGeneralType => ({
+ (p): CommandBarItemGeneralType => ({
label: p.name,
Icon: MagazineIcon,
id: `project-${p.id}`,
key: `project-${p.id}`,
- shortcut: i < 9 ? ['cmd', (i + 1).toString()] : undefined,
onClick: () => switchProject(p.id),
footerHint:
project?.id === p.id
diff --git a/client/src/Project/CurrentTabContent/ChatTab/index.tsx b/client/src/Project/CurrentTabContent/ChatTab/index.tsx
index 3a5eae4aa7..d223c000d1 100644
--- a/client/src/Project/CurrentTabContent/ChatTab/index.tsx
+++ b/client/src/Project/CurrentTabContent/ChatTab/index.tsx
@@ -20,6 +20,7 @@ import { ChatTabType } from '../../../types/general';
import { ProjectContext } from '../../../context/projectContext';
import { CommandBarContext } from '../../../context/commandBarContext';
import { openInSplitViewShortcut } from '../../../consts/commandBar';
+import { UIContext } from '../../../context/uiContext';
import Conversation from './Conversation';
import ActionsDropdown from './ActionsDropdown';
@@ -41,6 +42,7 @@ const ChatTab = ({
const { t } = useTranslation();
const { focusedPanel } = useContext(TabsContext.All);
const { closeTab } = useContext(TabsContext.Handlers);
+ const { isLeftSidebarFocused } = useContext(UIContext.Focus);
const { setFocusedTabItems } = useContext(CommandBarContext.Handlers);
const { project, refreshCurrentProjectConversations } = useContext(
ProjectContext.Current,
@@ -74,7 +76,10 @@ const ChatTab = ({
},
[handleMoveToAnotherSide],
);
- useKeyboardNavigation(handleKeyEvent, focusedPanel !== side);
+ useKeyboardNavigation(
+ handleKeyEvent,
+ focusedPanel !== side || isLeftSidebarFocused,
+ );
useEffect(() => {
if (focusedPanel === side) {
@@ -108,21 +113,23 @@ const ChatTab = ({
/>
{title || t('New chat')}
-
-
-
+
+
+ )}
diff --git a/client/src/Project/CurrentTabContent/FileTab/index.tsx b/client/src/Project/CurrentTabContent/FileTab/index.tsx
index bf7ee4312a..5ab6dd929e 100644
--- a/client/src/Project/CurrentTabContent/FileTab/index.tsx
+++ b/client/src/Project/CurrentTabContent/FileTab/index.tsx
@@ -38,6 +38,7 @@ import { CommandBarContext } from '../../../context/commandBarContext';
import { openInSplitViewShortcut } from '../../../consts/commandBar';
import BreadcrumbsPathContainer from '../../../components/Breadcrumbs/PathContainer';
import { RepositoriesContext } from '../../../context/repositoriesContext';
+import { UIContext } from '../../../context/uiContext';
import ActionsDropdown from './ActionsDropdown';
type Props = {
@@ -76,6 +77,7 @@ const FileTab = ({
const [isPending, startTransition] = useTransition();
const { openNewTab, updateTabProperty } = useContext(TabsContext.Handlers);
const { focusedPanel } = useContext(TabsContext.All);
+ const { isLeftSidebarFocused } = useContext(UIContext.Focus);
const { fileHighlights, hoveredLines } = useContext(
FileHighlightsContext.Values,
);
@@ -166,7 +168,7 @@ const FileTab = ({
);
useKeyboardNavigation(
handleKeyEvent,
- !file?.contents || focusedPanel !== side,
+ !file?.contents || focusedPanel !== side || isLeftSidebarFocused,
);
useEffect(() => {
@@ -227,21 +229,23 @@ const FileTab = ({
nonInteractive
/>
-
-
-
+
+
+ )}
{file?.lang === 'jupyter notebook' ? (
diff --git a/client/src/Project/CurrentTabContent/index.tsx b/client/src/Project/CurrentTabContent/index.tsx
index c118a12910..88ba20755e 100644
--- a/client/src/Project/CurrentTabContent/index.tsx
+++ b/client/src/Project/CurrentTabContent/index.tsx
@@ -4,6 +4,7 @@ import { Trans } from 'react-i18next';
import { TabsContext } from '../../context/tabsContext';
import { DraggableTabItem, TabType, TabTypesEnum } from '../../types/general';
import { SplitViewIcon } from '../../icons';
+import { UIContext } from '../../context/uiContext';
import EmptyTab from './EmptyTab';
import FileTab from './FileTab';
import Header from './Header';
@@ -26,6 +27,7 @@ const CurrentTabContent = ({
TabsContext[side === 'left' ? 'CurrentLeft' : 'CurrentRight'],
);
const { setFocusedPanel } = useContext(TabsContext.Handlers);
+ const { setIsLeftSidebarFocused } = useContext(UIContext.Focus);
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
@@ -44,6 +46,7 @@ const CurrentTabContent = ({
const focusPanel = useCallback(() => {
setFocusedPanel(side);
+ setIsLeftSidebarFocused(false);
}, [side]);
const handleMoveToAnotherSide = useCallback(() => {
diff --git a/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx
index 175a1f9dcd..bf55777d39 100644
--- a/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx
+++ b/client/src/Project/LeftSidebar/NavPanel/Conversations.tsx
@@ -23,11 +23,18 @@ import ConversationEntry from './CoversationEntry';
type Props = {
setExpanded: Dispatch
>;
isExpanded: boolean;
+ focusedIndex: string;
+ index: number;
};
const reactRoot = document.getElementById('root')!;
-const ConversationsNav = ({ isExpanded, setExpanded }: Props) => {
+const ConversationsNav = ({
+ isExpanded,
+ setExpanded,
+ focusedIndex,
+ index,
+}: Props) => {
const { t } = useTranslation();
const { project } = useContext(ProjectContext.Current);
const containerRef = useRef(null);
@@ -46,18 +53,25 @@ const ConversationsNav = ({ isExpanded, setExpanded }: Props) => {
e?.stopPropagation();
}, []);
+ useEffect(() => {
+ if (focusedIndex === index.toString() && containerRef.current) {
+ containerRef.current.scrollIntoView({ block: 'nearest' });
+ }
+ }, [focusedIndex, index]);
+
return (
-
+
{
)}
-
- {project?.conversations.map((c) => (
-
- ))}
-
+ {isExpanded && (
+
+ {project?.conversations.map((c, ci) => (
+
+ ))}
+
+ )}
);
};
diff --git a/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx
index 6056e9b3c3..1586f043bd 100644
--- a/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx
+++ b/client/src/Project/LeftSidebar/NavPanel/CoversationEntry.tsx
@@ -3,9 +3,12 @@ import { ConversationShortType } from '../../../types/api';
import { TabsContext } from '../../../context/tabsContext';
import { TabTypesEnum } from '../../../types/general';
-type Props = ConversationShortType & {};
+type Props = ConversationShortType & {
+ index: string;
+ focusedIndex: string;
+};
-const ConversationEntry = ({ title, id }: Props) => {
+const ConversationEntry = ({ title, id, index, focusedIndex }: Props) => {
const { openNewTab } = useContext(TabsContext.Handlers);
const handleClick = useCallback(() => {
@@ -16,9 +19,14 @@ const ConversationEntry = ({ title, id }: Props) => {
{title}
diff --git a/client/src/Project/LeftSidebar/NavPanel/Repo.tsx b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx
index 6d777fe19a..ce3c61bb95 100644
--- a/client/src/Project/LeftSidebar/NavPanel/Repo.tsx
+++ b/client/src/Project/LeftSidebar/NavPanel/Repo.tsx
@@ -40,6 +40,8 @@ type Props = {
allBranches: { name: string; last_commit_unix_secs: number }[];
indexedBranches: string[];
indexingData?: RepoIndexingStatusType;
+ focusedIndex: string;
+ index: number;
};
const reactRoot = document.getElementById('root')!;
@@ -56,6 +58,8 @@ const RepoNav = ({
lastIndex,
currentPath,
indexingData,
+ focusedIndex,
+ index,
}: Props) => {
const { t } = useTranslation();
const [files, setFiles] = useState([]);
@@ -122,15 +126,25 @@ const RepoNav = ({
].includes(indexingData.status);
}, [indexingData]);
+ useEffect(() => {
+ if (focusedIndex === index.toString() && containerRef.current) {
+ containerRef.current.scrollIntoView({ block: 'nearest' });
+ }
+ }, [focusedIndex, index]);
+
return (
-
+
{isIndexing && indexingData ? (
)}
-
- {files.map((f) => (
-
- ))}
-
+ {isExpanded && (
+
+ {files.map((f, fi) => (
+
+ ))}
+
+ )}
);
};
diff --git a/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx
index ad2bd30915..9b4415e921 100644
--- a/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx
+++ b/client/src/Project/LeftSidebar/NavPanel/RepoEntry.tsx
@@ -16,6 +16,8 @@ import {
TabTypesEnum,
} from '../../../types/general';
import SpinLoaderContainer from '../../../components/Loaders/SpinnerLoader';
+import { UIContext } from '../../../context/uiContext';
+import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation';
type Props = {
name: string;
@@ -30,6 +32,8 @@ type Props = {
currentPath?: string;
branch?: string | null;
indexingData?: RepoIndexingStatusType;
+ focusedIndex: string;
+ index: string;
};
const RepoEntry = ({
@@ -45,8 +49,11 @@ const RepoEntry = ({
lastIndex,
branch,
indexingData,
+ focusedIndex,
+ index,
}: Props) => {
const { openNewTab } = useContext(TabsContext.Handlers);
+ const { isLeftSidebarFocused } = useContext(UIContext.Focus);
const [isOpen, setOpen] = useState(
defaultOpen || (currentPath && currentPath.startsWith(fullPath)),
);
@@ -103,6 +110,22 @@ const RepoEntry = ({
}
}, [isDirectory, fullPath, openNewTab, repoRef, branch]);
+ const handleKeyEvent = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleClick();
+ }
+ },
+ [handleClick],
+ );
+ useKeyboardNavigation(handleKeyEvent, focusedIndex !== index);
+
+ useEffect(() => {
+ if (focusedIndex === index && ref.current) {
+ ref.current.scrollIntoView({ block: 'nearest' });
+ }
+ }, [focusedIndex, index]);
+
return (
{isDirectory ? (
@@ -167,13 +195,13 @@ const RepoEntry = ({
{/*
*/}
{/*)}*/}
- {subItems?.length ? (
+ {isOpen && subItems?.length ? (
- {subItems.map((si) => (
+ {subItems.map((si, sii) => (
))}
diff --git a/client/src/Project/LeftSidebar/NavPanel/index.tsx b/client/src/Project/LeftSidebar/NavPanel/index.tsx
index 69aca529de..e774bfd5d7 100644
--- a/client/src/Project/LeftSidebar/NavPanel/index.tsx
+++ b/client/src/Project/LeftSidebar/NavPanel/index.tsx
@@ -1,8 +1,18 @@
-import { memo, useContext, useEffect, useMemo, useState } from 'react';
+import {
+ memo,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { ProjectContext } from '../../../context/projectContext';
import { TabTypesEnum } from '../../../types/general';
import { TabsContext } from '../../../context/tabsContext';
import { RepositoriesContext } from '../../../context/repositoriesContext';
+import { UIContext } from '../../../context/uiContext';
+import useKeyboardNavigation from '../../../hooks/useKeyboardNavigation';
import RepoNav from './Repo';
import ConversationsNav from './Conversations';
@@ -15,6 +25,10 @@ const NavPanel = ({}: Props) => {
const { tab: leftTab } = useContext(TabsContext.CurrentLeft);
const { tab: rightTab } = useContext(TabsContext.CurrentRight);
const { indexingStatus } = useContext(RepositoriesContext);
+ const { isLeftSidebarFocused } = useContext(UIContext.Focus);
+ const ref = useRef
(null);
+ const [focusedIndex, setFocusedIndex] = useState(-1);
+ const [focusedIndexFull, setFocusedIndexFull] = useState('');
const currentlyFocusedTab = useMemo(() => {
const focusedTab = focusedPanel === 'left' ? leftTab : rightTab;
@@ -41,12 +55,39 @@ const NavPanel = ({}: Props) => {
}
}, [currentlyFocusedTab]);
+ const handleKeyEvent = useCallback((e: KeyboardEvent) => {
+ if (ref.current) {
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ e.stopPropagation();
+ const nodes = ref.current.querySelectorAll('[data-node-index]');
+ setFocusedIndex((prev) => {
+ const newInd =
+ e.key === 'ArrowDown'
+ ? prev < nodes.length - 1
+ ? prev + 1
+ : 0
+ : prev > 0
+ ? prev - 1
+ : nodes.length - 1;
+ setFocusedIndexFull(
+ (nodes[newInd] as HTMLElement)?.dataset?.nodeIndex || '',
+ );
+ return newInd;
+ });
+ }
+ }
+ }, []);
+ useKeyboardNavigation(handleKeyEvent, !isLeftSidebarFocused);
+
return (
-
+
{!!project?.conversations.length && (
)}
{project?.repos.map((r, i) => (
@@ -69,6 +110,15 @@ const NavPanel = ({}: Props) => {
? currentlyFocusedTab?.path
: undefined
}
+ focusedIndex={focusedIndexFull}
+ index={
+ i +
+ (project?.conversations.length
+ ? expanded === 0
+ ? project?.conversations.length + 1
+ : 1
+ : 0)
+ }
/>
))}
diff --git a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx
index 62c9fc41ec..ac6de34ec2 100644
--- a/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx
+++ b/client/src/Project/LeftSidebar/RegexSearchPanel/index.tsx
@@ -26,6 +26,7 @@ import { splitPath } from '../../../utils';
import { ProjectContext } from '../../../context/projectContext';
import { CommandBarContext } from '../../../context/commandBarContext';
import { regexToggleShortcut } from '../../../consts/shortcuts';
+import { UIContext } from '../../../context/uiContext';
import CodeResult from './Results/CodeResult';
import RepoResult from './Results/RepoResult';
import FileResult from './Results/FileResult';
@@ -58,6 +59,7 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => {
const inputRef = useRef
(null);
const { setIsRegexSearchEnabled } = useContext(ProjectContext.RegexSearch);
const { isVisible } = useContext(CommandBarContext.General);
+ const { isLeftSidebarFocused } = useContext(UIContext.Focus);
const [focusedIndex, setFocusedIndex] = useState(-1);
const onChange = useCallback((e: ChangeEvent) => {
@@ -180,7 +182,10 @@ const RegexSearchPanel = ({ projectId, isRegexEnabled }: Props) => {
},
[resultsRaw],
);
- useKeyboardNavigation(handleKeyEvent, isVisible || !isRegexEnabled);
+ useKeyboardNavigation(
+ handleKeyEvent,
+ isVisible || !isRegexEnabled || !isLeftSidebarFocused,
+ );
return !isRegexEnabled ? null : (
diff --git a/client/src/Project/LeftSidebar/index.tsx b/client/src/Project/LeftSidebar/index.tsx
index c1057df6ea..fae480ead0 100644
--- a/client/src/Project/LeftSidebar/index.tsx
+++ b/client/src/Project/LeftSidebar/index.tsx
@@ -1,4 +1,4 @@
-import React, { memo, useContext } from 'react';
+import React, { memo, useCallback, useContext, MouseEvent } from 'react';
import useResizeableWidth from '../../hooks/useResizeableWidth';
import { LEFT_SIDEBAR_WIDTH_KEY } from '../../services/storage';
import ProjectsDropdown from '../../components/Header/ProjectsDropdown';
@@ -6,8 +6,11 @@ import { ChevronDownIcon } from '../../icons';
import Dropdown from '../../components/Dropdown';
import { DeviceContext } from '../../context/deviceContext';
import { ProjectContext } from '../../context/projectContext';
-import NavPanel from './NavPanel';
+import { UIContext } from '../../context/uiContext';
+import { checkEventKeys } from '../../utils/keyboardUtils';
+import useKeyboardNavigation from '../../hooks/useKeyboardNavigation';
import RegexSearchPanel from './RegexSearchPanel';
+import NavPanel from './NavPanel';
type Props = {};
@@ -15,12 +18,29 @@ const LeftSidebar = ({}: Props) => {
const { os } = useContext(DeviceContext);
const { project } = useContext(ProjectContext.Current);
const { isRegexSearchEnabled } = useContext(ProjectContext.RegexSearch);
+ const { setIsLeftSidebarFocused } = useContext(UIContext.Focus);
+
const { panelRef, dividerRef } = useResizeableWidth(
true,
LEFT_SIDEBAR_WIDTH_KEY,
20,
40,
);
+
+ const handleKeyEvent = useCallback((e: KeyboardEvent) => {
+ if (checkEventKeys(e, ['cmd', '0'])) {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsLeftSidebarFocused(true);
+ }
+ }, []);
+ useKeyboardNavigation(handleKeyEvent);
+
+ const handleClick = useCallback((e: MouseEvent) => {
+ e.stopPropagation();
+ setIsLeftSidebarFocused(true);
+ }, []);
+
return (
{
-
- {!isRegexSearchEnabled && }
+
+
+ {!isRegexSearchEnabled && }
+
{
useTranslation();
const { project } = useContext(ProjectContext.Current);
const { rightTabs, leftTabs } = useContext(TabsContext.All);
+ const { setIsLeftSidebarFocused } = useContext(UIContext.Focus);
const {
setActiveRightTab,
setActiveLeftTab,
@@ -69,6 +73,24 @@ const Project = ({}: Props) => {
setFocusedPanel('left');
}, []);
+ const handleKeyEvent = useCallback(
+ (e: KeyboardEvent) => {
+ if (checkEventKeys(e, ['cmd', '1'])) {
+ e.preventDefault();
+ e.stopPropagation();
+ setFocusedPanel('left');
+ setIsLeftSidebarFocused(false);
+ } else if (checkEventKeys(e, ['cmd', '2']) && rightTabs.length) {
+ e.preventDefault();
+ e.stopPropagation();
+ setFocusedPanel('right');
+ setIsLeftSidebarFocused(false);
+ }
+ },
+ [rightTabs.length],
+ );
+ useKeyboardNavigation(handleKeyEvent);
+
return !project?.repos?.length ? (
) : (
diff --git a/client/src/context/providers/UIContextProvider.tsx b/client/src/context/providers/UIContextProvider.tsx
index 3c1260de37..0d5693b6f5 100644
--- a/client/src/context/providers/UIContextProvider.tsx
+++ b/client/src/context/providers/UIContextProvider.tsx
@@ -53,6 +53,7 @@ export const UIContextProvider = memo(
const [theme, setTheme] = useState(
(getPlainFromStorage(THEME) as 'system' | null) || 'system',
);
+ const [isLeftSidebarFocused, setIsLeftSidebarFocused] = useState(false);
const refreshToken = useCallback(async (refToken: string) => {
if (refToken) {
@@ -163,6 +164,14 @@ export const UIContextProvider = memo(
[theme],
);
+ const focusContextValue = useMemo(
+ () => ({
+ isLeftSidebarFocused,
+ setIsLeftSidebarFocused,
+ }),
+ [isLeftSidebarFocused],
+ );
+
return (
@@ -170,7 +179,9 @@ export const UIContextProvider = memo(
- {children}
+
+ {children}
+
diff --git a/client/src/context/uiContext.ts b/client/src/context/uiContext.ts
index 2c3e0f133d..2cd0afb867 100644
--- a/client/src/context/uiContext.ts
+++ b/client/src/context/uiContext.ts
@@ -35,4 +35,8 @@ export const UIContext = {
theme: 'system' as Theme,
setTheme: (t: Theme) => {},
}),
+ Focus: createContext({
+ isLeftSidebarFocused: false,
+ setIsLeftSidebarFocused: (b: boolean) => {},
+ }),
};