From f8ead8534b682060d7dcd29709eea99f84fde345 Mon Sep 17 00:00:00 2001 From: Kelli Rockwell Date: Sun, 10 Sep 2023 15:55:47 -0700 Subject: [PATCH] cody web: provide suggestions in context selector (#56039) * Add note to gitignore about cody directory inclusion * Update docstring about user-history usage * Re-index user history entries by user ID * Pass authenticated user ID to useUserHistory hook * Pass `authenticatedUser` through Cody chat UI component props * Refactor scope selector queries to use fragments * Hook up repo selector popover to query 10 most recently visited repos * Change visibility of `UserHistory.loadEntries()` * Pass authenticated user through a couple more components * Refactor suggested repos query to grab 10 by name and first 10 embedded * Construct suggested repos list from user history, transcript history, and defaults * Display suggestions, cleanup popover height/display * Initialize new transcript with previous scope * Type fixups * Some language cleanup * Add docstring for user history hook * Refactor repo suggestions out to a custom hook * Re-index Cody transcript history entries by user ID * Don't show suggestions section at all if there are none * Update for autoload case * Update cody-shared dependency * Fix placeholder spacing * DRY up suggestions rendering, improve spacing * Use abbreviated repo name in header * Bazel configure * Update editor implementations for cody-shared package * Add extra top padding to section headers * Vertically align radio buttons in rows * Update unindexed warning copy, link color * Hide context explanation when repos are selected * Remove "Great" --- .gitignore | 2 + client/web/BUILD.bazel | 1 + client/web/package.json | 2 +- client/web/src/LegacyLayout.tsx | 2 +- client/web/src/cody/chat/CodyChatPage.tsx | 14 +- .../web/src/cody/components/ChatUI/ChatUi.tsx | 17 +- .../src/cody/components/CodeMirrorEditor.ts | 14 + .../src/cody/components/FileContentEditor.ts | 14 + .../components/GettingStarted.module.scss | 14 +- .../src/cody/components/GettingStarted.tsx | 71 ++-- .../cody/components/RepoContainerEditor.ts | 14 + .../RepositoriesSelectorPopover.tsx | 312 +++++++++++------- .../ScopeSelector/ScopeSelector.tsx | 12 +- .../cody/components/ScopeSelector/backend.ts | 82 +++-- .../ScopeSelector/useRepoSuggestions.ts | 179 ++++++++++ client/web/src/cody/sidebar/CodySidebar.tsx | 6 +- client/web/src/cody/sidebar/Provider.tsx | 6 +- client/web/src/cody/useCodyChat.tsx | 67 ++-- client/web/src/components/useUserHistory.ts | 65 +++- client/web/src/repo/RepoContainer.tsx | 5 +- client/web/src/routes.tsx | 2 +- .../src/storm/pages/LayoutPage/LayoutPage.tsx | 2 +- pnpm-lock.yaml | 19 +- 23 files changed, 697 insertions(+), 225 deletions(-) create mode 100644 client/web/src/cody/components/ScopeSelector/useRepoSuggestions.ts diff --git a/.gitignore b/.gitignore index 556822d9ccc..b3056755826 100644 --- a/.gitignore +++ b/.gitignore @@ -205,4 +205,6 @@ result-* # Windows /cmd/sourcegraph/__debug_bin.exe +# For locally building and testing shared packages from the sourcegraph/cody repository on +# the web client. /cody diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index 0b771d38dcb..252860f180f 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -237,6 +237,7 @@ ts_project( "src/cody/components/ScopeSelector/ScopeSelector.tsx", "src/cody/components/ScopeSelector/backend.ts", "src/cody/components/ScopeSelector/index.tsx", + "src/cody/components/ScopeSelector/useRepoSuggestions.ts", "src/cody/isCodyEnabled.tsx", "src/cody/search/CodySearchPage.tsx", "src/cody/search/api.ts", diff --git a/client/web/package.json b/client/web/package.json index eaea092ee68..9570e47c4f5 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -43,7 +43,7 @@ "@sourcegraph/branded": "workspace:*", "@sourcegraph/client-api": "workspace:*", "@sourcegraph/codeintellify": "workspace:*", - "@sourcegraph/cody-shared": "^0.0.5", + "@sourcegraph/cody-shared": "^0.0.6", "@sourcegraph/cody-ui": "^0.0.7", "@sourcegraph/common": "workspace:*", "@sourcegraph/http-client": "workspace:*", diff --git a/client/web/src/LegacyLayout.tsx b/client/web/src/LegacyLayout.tsx index a600427c9fc..e89f4481c75 100644 --- a/client/web/src/LegacyLayout.tsx +++ b/client/web/src/LegacyLayout.tsx @@ -87,7 +87,7 @@ export const LegacyLayout: FC = props => { const isSetupWizardPage = location.pathname.startsWith(PageRoutes.SetupWizard) const [isFuzzyFinderVisible, setFuzzyFinderVisible] = useState(false) - const userHistory = useUserHistory(isRepositoryRelatedPage) + const userHistory = useUserHistory(props.authenticatedUser?.id, isRepositoryRelatedPage) const communitySearchContextPaths = communitySearchContextsRoutes.map(route => route.path) const isCommunitySearchContextPage = communitySearchContextPaths.includes(location.pathname) diff --git a/client/web/src/cody/chat/CodyChatPage.tsx b/client/web/src/cody/chat/CodyChatPage.tsx index 009ad2f8495..d0d0b1402b3 100644 --- a/client/web/src/cody/chat/CodyChatPage.tsx +++ b/client/web/src/cody/chat/CodyChatPage.tsx @@ -99,6 +99,7 @@ export const CodyChatPage: React.FunctionComponent = ({ const navigate = useNavigate() const codyChatStore = useCodyChat({ + userID: authenticatedUser?.id, onTranscriptHistoryLoad, autoLoadTranscriptFromHistory: false, autoLoadScopeWithRepositories: isSourcegraphApp, @@ -377,7 +378,12 @@ export const CodyChatPage: React.FunctionComponent = ({ New chat - + {showMobileHistory && ( @@ -457,7 +463,11 @@ export const CodyChatPage: React.FunctionComponent = ({ deleteHistoryItem={deleteHistoryItem} /> ) : ( - + )} )} diff --git a/client/web/src/cody/components/ChatUI/ChatUi.tsx b/client/web/src/cody/components/ChatUI/ChatUi.tsx index 62b3939b64c..11d158285ef 100644 --- a/client/web/src/cody/components/ChatUI/ChatUi.tsx +++ b/client/web/src/cody/components/ChatUI/ChatUi.tsx @@ -22,6 +22,7 @@ import { type FeedbackButtonsProps, } from '@sourcegraph/cody-ui/dist/Chat' import type { FileLinkProps } from '@sourcegraph/cody-ui/dist/chat/ContextFiles' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { Button, Icon, TextArea, Link, Tooltip, Alert, Text, H2 } from '@sourcegraph/wildcard' import { eventLogger } from '../../../tracking/eventLogger' @@ -44,9 +45,15 @@ interface IChatUIProps { codyChatStore: CodyChatStore isSourcegraphApp?: boolean isCodyChatPage?: boolean + authenticatedUser: AuthenticatedUser | null } -export const ChatUI: React.FC = ({ codyChatStore, isSourcegraphApp, isCodyChatPage }): JSX.Element => { +export const ChatUI: React.FC = ({ + codyChatStore, + isSourcegraphApp, + isCodyChatPage, + authenticatedUser, +}): JSX.Element => { const { submitMessage, editMessage, @@ -90,7 +97,9 @@ export const ChatUI: React.FC = ({ codyChatStore, isSourcegraphApp fetchRepositoryNames, isSourcegraphApp, logTranscriptEvent, + transcriptHistory, className: 'mt-2', + authenticatedUser, }), [ scope, @@ -100,12 +109,14 @@ export const ChatUI: React.FC = ({ codyChatStore, isSourcegraphApp fetchRepositoryNames, isSourcegraphApp, logTranscriptEvent, + transcriptHistory, + authenticatedUser, ] ) const gettingStartedComponentProps = useMemo( - () => ({ ...scopeSelectorProps, logTranscriptEvent, isCodyChatPage }), - [scopeSelectorProps, logTranscriptEvent, isCodyChatPage] + () => ({ ...scopeSelectorProps, logTranscriptEvent, isCodyChatPage, authenticatedUser }), + [scopeSelectorProps, isCodyChatPage, logTranscriptEvent, authenticatedUser] ) if (!loaded) { diff --git a/client/web/src/cody/components/CodeMirrorEditor.ts b/client/web/src/cody/components/CodeMirrorEditor.ts index e3990690f3c..4485c4c39d8 100644 --- a/client/web/src/cody/components/CodeMirrorEditor.ts +++ b/client/web/src/cody/components/CodeMirrorEditor.ts @@ -99,6 +99,10 @@ export class CodeMirrorEditor implements Editor { return null } + public getActiveTextEditorSelectionOrVisibleContent(): ActiveTextEditorSelection | null { + return this.getActiveTextEditorSelectionOrEntireFile() + } + public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null { const editor = this.editor if (editor) { @@ -153,4 +157,14 @@ export class CodeMirrorEditor implements Editor { // Not implemented. return Promise.resolve(undefined) } + + public getActiveInlineChatTextEditor(): ActiveTextEditor | null { + // Not implemented. + return null + } + + public getActiveInlineChatSelection(): ActiveTextEditorSelection | null { + // Not implemented. + return null + } } diff --git a/client/web/src/cody/components/FileContentEditor.ts b/client/web/src/cody/components/FileContentEditor.ts index c9fcbe92e20..9a70d1c2b51 100644 --- a/client/web/src/cody/components/FileContentEditor.ts +++ b/client/web/src/cody/components/FileContentEditor.ts @@ -42,6 +42,10 @@ export class FileContentEditor implements Editor { } } + public getActiveTextEditorSelectionOrVisibleContent(): ActiveTextEditorSelection | null { + return this.getActiveTextEditorSelectionOrEntireFile() + } + public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null { return { ...this.editor, @@ -85,4 +89,14 @@ export class FileContentEditor implements Editor { // Not implemented. return Promise.resolve(undefined) } + + public getActiveInlineChatTextEditor(): ActiveTextEditor | null { + // Not implemented. + return null + } + + public getActiveInlineChatSelection(): ActiveTextEditorSelection | null { + // Not implemented. + return null + } } diff --git a/client/web/src/cody/components/GettingStarted.module.scss b/client/web/src/cody/components/GettingStarted.module.scss index 1d72d51b0eb..61b1c2c9c66 100644 --- a/client/web/src/cody/components/GettingStarted.module.scss +++ b/client/web/src/cody/components/GettingStarted.module.scss @@ -62,10 +62,20 @@ padding-left: var(--form-check-input-gutter); } - &-warning { - margin-top: 0.75rem; + &-warning, + &-warning-link { + margin-top: 0.5rem; + margin-bottom: 0; color: var(--warning-3); } + + &-warning-link { + text-decoration: underline; + } + + &-warning-link:hover { + color: var(--warning); + } } .divider { diff --git a/client/web/src/cody/components/GettingStarted.tsx b/client/web/src/cody/components/GettingStarted.tsx index 94912060d6c..e3e9e4d279c 100644 --- a/client/web/src/cody/components/GettingStarted.tsx +++ b/client/web/src/cody/components/GettingStarted.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { H4, H5, RadioButton, Text, Button, Grid, Icon, Link } from '@sourcegraph/wildcard' import { CodyColorIcon, CodySpeechBubbleIcon } from '../chat/CodyPageIcon' @@ -22,6 +23,7 @@ export const GettingStarted: React.FC< CodyChatStore, | 'scope' | 'logTranscriptEvent' + | 'transcriptHistory' | 'setScope' | 'toggleIncludeInferredRepository' | 'toggleIncludeInferredFile' @@ -30,8 +32,9 @@ export const GettingStarted: React.FC< isSourcegraphApp?: boolean isCodyChatPage?: boolean submitInput: (input: string, submitType: 'user' | 'suggestion' | 'example') => void + authenticatedUser: AuthenticatedUser | null } -> = ({ isCodyChatPage, submitInput, ...scopeSelectorProps }) => { +> = ({ isCodyChatPage, submitInput, authenticatedUser, ...scopeSelectorProps }) => { const [conversationScope, setConversationScope] = useState( !isCodyChatPage || scopeSelectorProps.scope.repositories.length > 0 ? 'repo' : 'general' ) @@ -77,9 +80,9 @@ export const GettingStarted: React.FC< const content: { title: string; examples: { label?: string; text: string }[] } = useMemo(() => { if (conversationScope === 'repo') { return { - title: `Great examples to start with${ + title: `Examples to start with${ scopeSelectorProps.scope.repositories.length === 1 - ? ` for ${scopeSelectorProps.scope.repositories[0]}` + ? ` for ${scopeSelectorProps.scope.repositories[0].split('/').slice(-2).join('/')}` : '' }`, examples: [ @@ -101,10 +104,10 @@ export const GettingStarted: React.FC< examples: [ { label: 'Algorithms', - text: 'How the QuickSort algorithm works with an implementation in Python?', + text: 'Can you explain how the QuickSort algorithm works in Python?', }, { - label: 'Good Practice', + label: 'Best practices', text: "I'm working on a large-scale web application using React. What are some best practices or design patterns I should be aware of to maintain code readability and performance?", }, { @@ -121,11 +124,22 @@ export const GettingStarted: React.FC< return null } + const unindexedCount = repos.filter(repo => !isRepoIndexed(repo)).length + const warningText = + repos.length === 1 + ? 'The selected repository is not indexed for Cody and is missing embeddings.' + : `${unindexedCount} of ${repos.length} selected repositories are not indexed for Cody and are missing embeddings.` + return ( - {repos.length === 1 ? 'This repo is' : 'Some repos are'} not indexed for Cody. This may affect the - quality of the answers. Learn more about this{' '} - in the docs. + {warningText} This may affect the quality of the answers. To enable indexing, see the{' '} + + embeddings documentation + + . ) }, @@ -165,11 +179,11 @@ export const GettingStarted: React.FC< name="general" label={ - General Coding Knowledge + General knowledge } value="general" - wrapperClassName="d-flex align-items-baseline" + wrapperClassName="d-flex align-items-center" checked={conversationScope === 'general'} onChange={event => setConversationScope(event.target.value as ConversationScope)} /> @@ -180,11 +194,11 @@ export const GettingStarted: React.FC< name="repo" label={ - My Repos: + Specific repositories: } value="repo" - wrapperClassName="d-flex align-items-baseline mb-1" + wrapperClassName="d-flex align-items-center mb-1" checked={conversationScope === 'repo'} onChange={event => setConversationScope(event.target.value as ConversationScope)} /> @@ -193,24 +207,29 @@ export const GettingStarted: React.FC< {...scopeSelectorProps} renderHint={renderRepoIndexingWarning} encourageOverlap={true} + authenticatedUser={authenticatedUser} /> -
- - Why context is important? - - - Without providing a specific repo as context, Cody won’t be able to answer with - relevant knowledge about your project. - + {scopeSelectorProps.scope.repositories.length === 0 && ( + <> +
+ + Why is context important? + + + Without providing relevant repo(s) for context, Cody won't be able to answer + questions specific to your project. + - - - Tip: - {' '} - The context selector is always available at the bottom of the screen - + + + Tip: + {' '} + The context selector is always available at the bottom of the screen + + + )} diff --git a/client/web/src/cody/components/RepoContainerEditor.ts b/client/web/src/cody/components/RepoContainerEditor.ts index 8c728107886..801b2d52faf 100644 --- a/client/web/src/cody/components/RepoContainerEditor.ts +++ b/client/web/src/cody/components/RepoContainerEditor.ts @@ -29,6 +29,10 @@ export class RepoContainerEditor implements Editor { return null } + public getActiveTextEditorSelectionOrVisibleContent(): ActiveTextEditorSelection | null { + return null + } + public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null { return null } @@ -69,4 +73,14 @@ export class RepoContainerEditor implements Editor { // Not implemented. return Promise.resolve(undefined) } + + public getActiveInlineChatTextEditor(): ActiveTextEditor | null { + // Not implemented. + return null + } + + public getActiveInlineChatSelection(): ActiveTextEditorSelection | null { + // Not implemented. + return null + } } diff --git a/client/web/src/cody/components/ScopeSelector/RepositoriesSelectorPopover.tsx b/client/web/src/cody/components/ScopeSelector/RepositoriesSelectorPopover.tsx index 90854514f65..fb721502a3c 100644 --- a/client/web/src/cody/components/ScopeSelector/RepositoriesSelectorPopover.tsx +++ b/client/web/src/cody/components/ScopeSelector/RepositoriesSelectorPopover.tsx @@ -14,7 +14,9 @@ import { } from '@mdi/js' import classNames from 'classnames' +import type { TranscriptJSON } from '@sourcegraph/cody-shared/dist/chat/transcript' import { useLazyQuery } from '@sourcegraph/http-client' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary' import { Icon, @@ -38,6 +40,7 @@ import { ExternalRepositoryIcon } from '../../../site-admin/components/ExternalR import { ReposSelectorSearchQuery } from './backend' import { Callout } from './Callout' +import { useRepoSuggestions } from './useRepoSuggestions' import styles from './ScopeSelector.module.scss' @@ -73,6 +76,8 @@ export const RepositoriesSelectorPopover: React.FC<{ // Whether to encourage the popover to overlap its trigger if necessary, rather than // collapsing or flipping position. encourageOverlap?: boolean + transcriptHistory: TranscriptJSON[] + authenticatedUser: AuthenticatedUser | null }> = React.memo(function RepositoriesSelectorPopoverContent({ inferredRepository, inferredFilePath, @@ -85,6 +90,8 @@ export const RepositoriesSelectorPopover: React.FC<{ toggleIncludeInferredRepository, toggleIncludeInferredFile, encourageOverlap = false, + transcriptHistory, + authenticatedUser, }) { const [isPopoverOpen, setIsPopoverOpen] = useState(false) const [searchText, setSearchText] = useState('') @@ -95,6 +102,10 @@ export const RepositoriesSelectorPopover: React.FC<{ ReposSelectorSearchVariables >(ReposSelectorSearchQuery, {}) + const suggestions = useRepoSuggestions(transcriptHistory, authenticatedUser, { + omitSuggestions: additionalRepositories, + }) + const searchResults = useMemo(() => searchResultsData?.repositories.nodes || [], [searchResultsData]) const onSearch = useCallback( @@ -113,12 +124,12 @@ export const RepositoriesSelectorPopover: React.FC<{ if (searchTextDebounced) { /* eslint-disable no-console */ searchRepositories({ - variables: { query: searchTextDebounced, includeJobs: !!window.context.currentUser?.siteAdmin }, + variables: { query: searchTextDebounced, includeJobs: !!authenticatedUser?.siteAdmin }, pollInterval: 5000, }).catch(console.error) /* eslint-enable no-console */ } - }, [searchTextDebounced, searchRepositories]) + }, [searchTextDebounced, searchRepositories, authenticatedUser?.siteAdmin]) const [isCalloutDismissed = true, setIsCalloutDismissed] = useTemporarySetting( 'cody.contextCallout.dismissed', @@ -207,42 +218,76 @@ export const RepositoriesSelectorPopover: React.FC<{ styles.repositorySelectorContent )} > - {!searchText && ( - <> -
- Chat Context - {resetScope && scopeChanged && ( - - )} -
-
- {inferredRepository && ( -
- {inferredFilePath && ( + <> + {!searchText && ( + <> +
+ Chat Context + {resetScope && scopeChanged && ( + + )} +
+
+ {inferredRepository && ( +
+ {inferredFilePath && ( + + )} - )} - -
- )} - {!!additionalRepositories.length && ( -
- - - {inferredRepository ? 'Additional repositories' : 'Repositories'} - - {additionalRepositories.length}/10 + + {additionalRepositories.length}/{MAX_ADDITIONAL_REPOSITORIES} + + + {additionalRepositories.map(repository => ( + + ))} +
+ )} + + {!inferredRepository && !inferredFilePath && !additionalRepositories.length && ( + + Add up to {MAX_ADDITIONAL_REPOSITORIES} repositories for Cody to + reference when providing answers. - {additionalRepositories.map(repository => ( - + + Suggestions + +
+ {suggestions.map(repository => ( + + ))} +
+
+ )} +
+ + )} + {searchText && ( + <> +
+ + {additionalRepositoriesLeft + ? `Add up to ${additionalRepositoriesLeft} additional repositories` + : 'Maximum additional repositories added'} + +
+
+ {searchResults.length ? ( + searchResults.map(repository => ( + - ))} -
- )} - - {!inferredRepository && !inferredFilePath && !additionalRepositories.length && ( -
- - Add up to 10 repositories for Cody to reference when providing answers. - -
- )} -
- - )} - {searchText && ( - <> -
- - {additionalRepositoriesLeft - ? `Add up to ${additionalRepositoriesLeft} additional repositories` - : 'Maximum additional repositories added'} - -
-
- {searchResults.length ? ( - searchResults.map(repository => ( - - )) - ) : !loadingSearchResults ? ( -
- - No matching repositories found - -
- ) : null} -
- - )} + )) + ) : !loadingSearchResults ? ( +
+ + No matching repositories found + +
+ ) : null} + + + )} +
void -}> = React.memo(function RepositoryListItemContent({ repository, removeRepository }) { + authenticatedUser: AuthenticatedUser | null +}> = React.memo(function RepositoryListItemContent({ repository, removeRepository, authenticatedUser }) { const onClick = useCallback(() => { removeRepository(repository.name) }, [repository, removeRepository]) @@ -407,7 +460,7 @@ const AdditionalRepositoriesListItem: React.FC<{
- + ) }) @@ -432,12 +485,14 @@ const SearchResultsListItem: React.FC<{ searchText: string addRepository: (repoName: string) => void removeRepository: (repoName: string) => void + authenticatedUser: AuthenticatedUser | null }> = React.memo(function RepositoryListItemContent({ additionalRepositories, repository, searchText, addRepository, removeRepository, + authenticatedUser, }) { const selected = useMemo( () => !!additionalRepositories.find(({ name }) => name === repository.name), @@ -458,7 +513,7 @@ const SearchResultsListItem: React.FC<{ ) }) @@ -594,12 +649,13 @@ export const isRepoIndexed = (repo: IRepo): boolean => getEmbeddingStatus(repo). const EmbeddingExistsIcon: React.FC<{ repo: IRepo -}> = React.memo(function EmbeddingExistsIconContent({ repo }) { + authenticatedUser: AuthenticatedUser | null +}> = React.memo(function EmbeddingExistsIconContent({ repo, authenticatedUser }) { const { tooltip, icon, className } = getEmbeddingStatus(repo) return ( - {window.context.currentUser?.siteAdmin ? ( + {authenticatedUser?.siteAdmin ? ( event.stopPropagation()}> diff --git a/client/web/src/cody/components/ScopeSelector/ScopeSelector.tsx b/client/web/src/cody/components/ScopeSelector/ScopeSelector.tsx index a7691d9769e..e13150b12df 100644 --- a/client/web/src/cody/components/ScopeSelector/ScopeSelector.tsx +++ b/client/web/src/cody/components/ScopeSelector/ScopeSelector.tsx @@ -2,8 +2,10 @@ import React, { useEffect, useMemo, useCallback } from 'react' import classNames from 'classnames' +import type { TranscriptJSON } from '@sourcegraph/cody-shared/dist/chat/transcript' import type { CodyClientScope } from '@sourcegraph/cody-shared/dist/chat/useClient' import { useLazyQuery } from '@sourcegraph/http-client' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { Text } from '@sourcegraph/wildcard' import type { ReposStatusResult, ReposStatusVariables } from '../../../graphql-operations' @@ -22,11 +24,13 @@ export interface ScopeSelectorProps { fetchRepositoryNames: (count: number) => Promise isSourcegraphApp?: boolean logTranscriptEvent: (eventLabel: string, eventProperties?: { [key: string]: any }) => void + transcriptHistory: TranscriptJSON[] className?: string renderHint?: (repos: IRepo[]) => React.ReactNode // Whether to encourage the selector popover to overlap its trigger if necessary, // rather than collapsing or flipping position. encourageOverlap?: boolean + authenticatedUser: AuthenticatedUser | null } export const ScopeSelector: React.FC = React.memo(function ScopeSelectorComponent({ @@ -37,9 +41,11 @@ export const ScopeSelector: React.FC = React.memo(function S fetchRepositoryNames, isSourcegraphApp, logTranscriptEvent, + transcriptHistory, className, renderHint, encourageOverlap, + authenticatedUser, }) { const [loadReposStatus, { data: newReposStatusData, previousData: previousReposStatusData }] = useLazyQuery< ReposStatusResult, @@ -62,10 +68,10 @@ export const ScopeSelector: React.FC = React.memo(function S } loadReposStatus({ - variables: { repoNames, first: repoNames.length, includeJobs: !!window.context.currentUser?.siteAdmin }, + variables: { repoNames, first: repoNames.length, includeJobs: !!authenticatedUser?.siteAdmin }, pollInterval: 2000, }).catch(() => null) - }, [activeEditor, scope.repositories, loadReposStatus]) + }, [activeEditor, scope.repositories, loadReposStatus, authenticatedUser?.siteAdmin]) const allRepositories = useMemo(() => reposStatusData?.repositories.nodes || [], [reposStatusData]) @@ -134,6 +140,8 @@ export const ScopeSelector: React.FC = React.memo(function S toggleIncludeInferredRepository={toggleIncludeInferredRepository} toggleIncludeInferredFile={toggleIncludeInferredFile} encourageOverlap={encourageOverlap} + transcriptHistory={transcriptHistory} + authenticatedUser={authenticatedUser} /> {scope.includeInferredFile && activeEditor?.filePath && ( diff --git a/client/web/src/cody/components/ScopeSelector/backend.ts b/client/web/src/cody/components/ScopeSelector/backend.ts index c500b296475..2c416bd7251 100644 --- a/client/web/src/cody/components/ScopeSelector/backend.ts +++ b/client/web/src/cody/components/ScopeSelector/backend.ts @@ -1,47 +1,87 @@ import { gql } from '@sourcegraph/http-client' +const REPO_FIELDS = gql` + fragment ContextSelectorRepoFields on Repository { + id + name + embeddingExists + externalRepository { + id + serviceType + } + } +` + +const EMBEDDING_JOB_FIELDS = gql` + fragment ContextSelectorEmbeddingJobFields on RepoEmbeddingJob { + id + state + failureMessage + } +` + export const ReposSelectorSearchQuery = gql` query ReposSelectorSearch($query: String!, $includeJobs: Boolean!) { - repositories(query: $query, first: 10) { + repositories(query: $query, first: 15) { nodes { - id - name - embeddingExists - externalRepository { - id - serviceType - } + ...ContextSelectorRepoFields embeddingJobs(first: 1) @include(if: $includeJobs) { nodes { - id - state - failureMessage + ...ContextSelectorEmbeddingJobFields } } } } } + + ${REPO_FIELDS} + ${EMBEDDING_JOB_FIELDS} +` + +export const SuggestedReposQuery = gql` + query SuggestedRepos($names: [String!]!, $numResults: Int!, $includeJobs: Boolean!) { + byName: repositories(names: $names, first: $numResults) { + nodes { + ...ContextSelectorRepoFields + embeddingJobs(first: 1) @include(if: $includeJobs) { + nodes { + ...ContextSelectorEmbeddingJobFields + } + } + } + } + # We also grab the first $numResults embedded repos available on the site + # to show in suggestions as a backup. + firstN: repositories(first: $numResults, embedded: true) { + nodes { + ...ContextSelectorRepoFields + embeddingJobs(first: 1) @include(if: $includeJobs) { + nodes { + ...ContextSelectorEmbeddingJobFields + } + } + } + } + } + + ${REPO_FIELDS} + ${EMBEDDING_JOB_FIELDS} ` export const ReposStatusQuery = gql` query ReposStatus($repoNames: [String!]!, $first: Int!, $includeJobs: Boolean!) { repositories(names: $repoNames, first: $first) { nodes { - id - name - embeddingExists - externalRepository { - id - serviceType - } + ...ContextSelectorRepoFields embeddingJobs(first: 1) @include(if: $includeJobs) { nodes { - id - state - failureMessage + ...ContextSelectorEmbeddingJobFields } } } } } + + ${REPO_FIELDS} + ${EMBEDDING_JOB_FIELDS} ` diff --git a/client/web/src/cody/components/ScopeSelector/useRepoSuggestions.ts b/client/web/src/cody/components/ScopeSelector/useRepoSuggestions.ts new file mode 100644 index 00000000000..7c34e33a1f8 --- /dev/null +++ b/client/web/src/cody/components/ScopeSelector/useRepoSuggestions.ts @@ -0,0 +1,179 @@ +import { useMemo } from 'react' + +import type { TranscriptJSON } from '@sourcegraph/cody-shared/dist/chat/transcript' +import { useQuery } from '@sourcegraph/http-client' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' + +import { useUserHistory } from '../../../components/useUserHistory' +import type { + ContextSelectorRepoFields, + SuggestedReposResult, + SuggestedReposVariables, +} from '../../../graphql-operations' + +import { SuggestedReposQuery } from './backend' + +interface UseRepoSuggestionsOpts { + numSuggestions: number + fallbackSuggestions: string[] + omitSuggestions: { name: string }[] +} + +const DEFAULT_OPTS: UseRepoSuggestionsOpts = { + numSuggestions: 10, + fallbackSuggestions: [ + // This is mostly relevant for dotcom but could fill in for any instance which + // indexes GitHub OSS repositories. If the repositories are not indexed on the + // instance, they will not return any results from the search API and thus will + // not be included in the final list of suggestions. + // + // NOTE: The actual 10 most popular OSS repositories on GitHub are typically just + // plaintext resource collections like awesome lists or how-to-program tutorials, which + // are not likely to be as interesting to Cody users. Instead, we hardcode a list of + // repositories that are among the top 100 that contain actual source code. + 'github.com/sourcegraph/sourcegraph', + 'github.com/freeCodeCamp/freeCodeCamp', + 'github.com/facebook/react', + 'github.com/tensorflow/tensorflow', + 'github.com/torvalds/linux', + 'github.com/microsoft/vscode', + 'github.com/flutter/flutter', + 'github.com/golang/go', + 'github.com/d3/d3', + 'github.com/kubernetes/kubernetes', + ], + omitSuggestions: [], +} + +/** + * useRepoSuggestions is a custom hook that generates repository suggestions for the + * context scope selector. + * + * The suggestions are based on the current user's chat transcript history, search + * browsing history, embedded repositories available on their instance, and any fallbacks + * configured in the options. + * + * The number of suggestions can be configured with `opts.numSuggestions`. The default is 10. + * + * Fallback suggestions can be configured with `opts.fallbackSuggestions`. The default is + * a list of 10 of the most popular OSS repositories on GitHub. + * + * Repositories can be omitted from the suggestions (for example, repositories that are + * already added to the context scope) by passing them as `opts.omitSuggestions.` + * + * @param transcriptHistory the current user's chat transcript history from the store + * @param authenticatedUser the current authenticated user + * @param opts any options for further configuring the suggestions + * @returns a list of repository suggestions as `ContextSelectorRepoFields` objects + */ +export const useRepoSuggestions = ( + transcriptHistory: TranscriptJSON[], + authenticatedUser: AuthenticatedUser | null = null, + opts?: Partial +): ContextSelectorRepoFields[] => { + const { numSuggestions, fallbackSuggestions, omitSuggestions } = { ...DEFAULT_OPTS, ...opts } + + const userHistory = useUserHistory(authenticatedUser?.id, false) + const suggestedRepoNames: string[] = useMemo(() => { + const flattenedTranscriptHistoryEntries = transcriptHistory + .map(item => { + const { scope, lastInteractionTimestamp } = item + return ( + // Return a new item for each repository in the scope. + scope?.repositories.map(name => ({ + // Parse a date from the last interaction timestamp. + lastAccessed: new Date(lastInteractionTimestamp), + name, + })) || [] + ) + }) + .flat() + // Remove duplicates. + .filter(removeDupes) + // We only need up to the first numSuggestions. + .slice(0, numSuggestions) + + const userHistoryEntries = + userHistory + .loadEntries() + .map(item => ({ + name: item.repoName, + // Parse a date from the last acessed timestamp. + lastAccessed: new Date(item.lastAccessed), + })) + // We only need up to the first numSuggestions. + .slice(0, numSuggestions) || [] + + // We also can take a list of up to numSuggestions fallback repos to + // fill in if we have fewer than numSuggestions to actually suggest. + const fallbackRepos = fallbackSuggestions + .map(name => ({ + name, + // We order by most recently accessed; these should always be ranked last. + lastAccessed: new Date(0), + })) + .slice(0, numSuggestions) + + // Merge the lists. + const merged = [...flattenedTranscriptHistoryEntries, ...userHistoryEntries, ...fallbackRepos] + // Sort by most recently accessed. + .sort((a, b) => b.lastAccessed.getTime() - a.lastAccessed.getTime()) + // Remove duplicates. + .filter(removeDupes) + // Take the most recent numSuggestions. + .slice(0, numSuggestions) + + // Return just the names. + return merged.map(({ name }) => name) + }, [transcriptHistory, userHistory, numSuggestions, fallbackSuggestions]) + + // Query for the suggested repositories. + const { data: suggestedReposData } = useQuery(SuggestedReposQuery, { + variables: { + names: suggestedRepoNames, + numResults: numSuggestions, + includeJobs: !!authenticatedUser?.siteAdmin, + }, + fetchPolicy: 'cache-first', + }) + + // Filter out and reorder the suggested repository results. + const suggestions: ContextSelectorRepoFields[] = useMemo(() => { + if (!suggestedReposData) { + return [] + } + + const nodes = [...suggestedReposData.byName.nodes] + // The order of the by-name repos returned by the search API will not match the + // order of suggestions we intend to display (the ordering of suggestedRepoNames), + // since the default ordering of the search API is alphabetical. Thus, we reorder + // them to match the initial ordering of suggestedRepoNames. + const sortedByNameNodes = nodes.sort( + (a, b) => suggestedRepoNames.indexOf(a.name) - suggestedRepoNames.indexOf(b.name) + ) + // Make sure we have a full numSuggestions to display in the + // suggestions. We'll prioritize the repositories we looked up by name, and then + // fill in the rest from the first 10 embedded repositories returned by the search + // API. + return ( + [...sortedByNameNodes, ...suggestedReposData.firstN.nodes] + // Remove any duplicates. + .filter(removeDupes) + // Take the first numSuggestions. + .slice(0, numSuggestions) + // Finally, filter out repositories that are should be omitted. + .filter(suggestion => !omitSuggestions.find(toOmit => toOmit.name === suggestion.name)) + ) + }, [suggestedReposData, suggestedRepoNames, omitSuggestions, numSuggestions]) + + return suggestions +} + +/** + * removeDupes is an `Array.filter` predicate function which removes duplicate entries + * from an array of objects based on the `name` property. It filters out any entries which + * are not the first occurrence with a given `name`, which means it will preserve the + * earliest occurrence of each. + */ +const removeDupes = (first: { name: string }, index: number, self: { name: string }[]): boolean => + index === self.findIndex(entry => entry.name === first.name) diff --git a/client/web/src/cody/sidebar/CodySidebar.tsx b/client/web/src/cody/sidebar/CodySidebar.tsx index 4184134f82b..0f2ce54130a 100644 --- a/client/web/src/cody/sidebar/CodySidebar.tsx +++ b/client/web/src/cody/sidebar/CodySidebar.tsx @@ -4,6 +4,7 @@ import { mdiClose, mdiHistory, mdiPlus, mdiDelete } from '@mdi/js' import classNames from 'classnames' import { CodyLogo } from '@sourcegraph/cody-ui/dist/icons/CodyLogo' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { Button, Icon, Tooltip, Badge } from '@sourcegraph/wildcard' import { ChatUI, ScrollDownButton } from '../components/ChatUI' @@ -17,9 +18,10 @@ export const SCROLL_THRESHOLD = 100 interface CodySidebarProps { onClose?: () => void + authenticatedUser: AuthenticatedUser | null } -export const CodySidebar: React.FC = ({ onClose }) => { +export const CodySidebar: React.FC = ({ onClose, authenticatedUser }) => { const codySidebarStore = useCodySidebar() const { initializeNewChat, @@ -143,7 +145,7 @@ export const CodySidebar: React.FC = ({ onClose }) => { deleteHistoryItem={deleteHistoryItem} /> ) : ( - + )} {showScrollDownButton && scrollToBottom('smooth')} />} diff --git a/client/web/src/cody/sidebar/Provider.tsx b/client/web/src/cody/sidebar/Provider.tsx index 57683ba0f88..a973ce5472e 100644 --- a/client/web/src/cody/sidebar/Provider.tsx +++ b/client/web/src/cody/sidebar/Provider.tsx @@ -1,5 +1,6 @@ import React, { useContext, useState, useCallback, useMemo } from 'react' +import type { AuthenticatedUser } from '@sourcegraph/shared/src/auth' import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary' import { useCodyChat, type CodyChatStore, codyChatStoreMock } from '../useCodyChat' @@ -25,9 +26,10 @@ const CodySidebarContext = React.createContext({ interface ICodySidebarStoreProviderProps { children?: React.ReactNode + authenticatedUser: AuthenticatedUser | null } -export const CodySidebarStoreProvider: React.FC = ({ children }) => { +export const CodySidebarStoreProvider: React.FC = ({ authenticatedUser, children }) => { const [isSidebarOpen, setIsSidebarOpenState] = useTemporarySetting('cody.showSidebar', false) const [inputNeedsFocus, setInputNeedsFocus] = useState(false) const { setSidebarSize } = useSidebarSize() @@ -46,7 +48,7 @@ export const CodySidebarStoreProvider: React.FC const onEvent = useCallback(() => setIsSidebarOpen(true), [setIsSidebarOpen]) - const codyChatStore = useCodyChat({ onEvent }) + const codyChatStore = useCodyChat({ userID: authenticatedUser?.id, onEvent }) const state = useMemo( () => ({ diff --git a/client/web/src/cody/useCodyChat.tsx b/client/web/src/cody/useCodyChat.tsx index 5c56724b8de..f943cdfde4b 100644 --- a/client/web/src/cody/useCodyChat.tsx +++ b/client/web/src/cody/useCodyChat.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' +import { noop } from 'lodash' + import { Transcript, type TranscriptJSON, @@ -13,6 +15,7 @@ import { type CodyClientEvent, } from '@sourcegraph/cody-shared/dist/chat/useClient' import { NoopEditor } from '@sourcegraph/cody-shared/dist/editor' +import type { Scalars } from '@sourcegraph/shared/src/graphql-operations' import { useLocalStorage } from '@sourcegraph/wildcard' import { eventLogger } from '../tracking/eventLogger' @@ -31,7 +34,6 @@ export interface CodyChatStore | 'messageInProgress' | 'submitMessage' | 'editMessage' - | 'initializeNewChat' | 'executeRecipe' | 'scope' | 'setScope' @@ -47,6 +49,7 @@ export interface CodyChatStore deleteHistoryItem: (id: string) => void loadTranscriptFromHistory: (id: string) => Promise logTranscriptEvent: (eventLabel: string, eventProperties?: { [key: string]: any }) => void + initializeNewChat: () => void } export const codyChatStoreMock: CodyChatStore = { @@ -79,6 +82,7 @@ export const codyChatStoreMock: CodyChatStore = { } interface CodyChatProps { + userID?: Scalars['ID'] scope?: CodyClientScope config?: CodyClientConfig onEvent?: (event: CodyClientEvent) => void @@ -95,6 +99,7 @@ const CODY_TRANSCRIPT_HISTORY_KEY = 'cody.chat.history' const SAVE_MAX_TRANSCRIPT_HISTORY = 20 export const useCodyChat = ({ + userID = 'anonymous', scope: initialScope, config: initialConfig, onEvent, @@ -103,8 +108,13 @@ export const useCodyChat = ({ autoLoadScopeWithRepositories = false, }: CodyChatProps): CodyChatStore => { const [loadedTranscriptFromHistory, setLoadedTranscriptFromHistory] = useState(false) + // Read old transcript history from local storage, if any exists. We will use this to + // preserve the history as we migrate to a new key that is differentiated by user. + const oldJSON = window.localStorage.getItem(CODY_TRANSCRIPT_HISTORY_KEY) + // eslint-disable-next-line no-restricted-syntax const [transcriptHistoryInternal, setTranscriptHistoryState] = useLocalStorage( - CODY_TRANSCRIPT_HISTORY_KEY, + // Users have distinct transcript histories, so we use the user ID as a key. + `${CODY_TRANSCRIPT_HISTORY_KEY}:${userID}`, [] ) const transcriptHistory = useMemo(() => transcriptHistoryInternal || [], [transcriptHistoryInternal]) @@ -135,6 +145,7 @@ export const useCodyChat = ({ customHeaders: window.context.xhrHeaders, debugEnable: false, needsEmailVerification: isEmailVerificationNeededForCody(), + experimentalLocalSymbols: false, }, scope: initialScope, onEvent, @@ -330,25 +341,30 @@ export const useCodyChat = ({ return null } - const newTranscript = initializeNewChatInternal() + const newTranscript = initializeNewChatInternal(scope) - if (newTranscript) { - pushTranscriptToHistory(newTranscript).catch(() => null) + if (!newTranscript) { + return null + } - if (autoLoadScopeWithRepositories) { - fetchRepositoryNames(10) - .then(repositories => { - const updatedScope = { - includeInferredRepository: true, - includeInferredFile: true, - repositories, - editor: scope.editor, - } - setScopeInternal(updatedScope) - updateTranscriptInHistory(newTranscript, updatedScope).catch(() => null) - }) - .catch(() => null) - } + pushTranscriptToHistory(newTranscript).catch(noop) + + // If we couldn't populate the scope with repositories from the last chat + // conversation and `autoLoadScopeWithRepositories` is enabled, then we fetch 10 + // to set in the scope. + if (scope.repositories.length === 0 && autoLoadScopeWithRepositories) { + fetchRepositoryNames(10) + .then(repositories => { + const updatedScope = { + includeInferredRepository: true, + includeInferredFile: true, + repositories, + editor: scope.editor, + } + setScopeInternal(updatedScope) + updateTranscriptInHistory(newTranscript, updatedScope).catch(noop) + }) + .catch(noop) } logTranscriptEvent(EventName.CODY_CHAT_INITIALIZED) @@ -385,6 +401,18 @@ export const useCodyChat = ({ // Autoload the latest transcript from history once it is loaded. Initially the transcript is null. useEffect(() => { if (!loadedTranscriptFromHistory && transcript === null) { + // User transcript history entries were previously stored in a single array in + // local storage under the generic key `cody.chat.history`, which is not + // differentiated by user. The first time we run this effect, we take any old + // entries and write them to the new user-differentiated key, and then delete + // the old key. When the effect runs again in the future, it will thus only + // run the code that comes after this block. + if (oldJSON) { + setTranscriptHistoryState(JSON.parse(oldJSON)) + window.localStorage.removeItem(CODY_TRANSCRIPT_HISTORY_KEY) + return + } + const history = sortSliceTranscriptHistory([...transcriptHistory]) if (autoLoadTranscriptFromHistory) { @@ -411,6 +439,7 @@ export const useCodyChat = ({ } }, [ transcriptHistory, + oldJSON, loadedTranscriptFromHistory, transcript, autoLoadTranscriptFromHistory, diff --git a/client/web/src/components/useUserHistory.ts b/client/web/src/components/useUserHistory.ts index 2b242db3fc1..d1368db6a69 100644 --- a/client/web/src/components/useUserHistory.ts +++ b/client/web/src/components/useUserHistory.ts @@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'react' import type * as H from 'history' import { useLocation } from 'react-router-dom' +import type { Scalars } from '../graphql-operations' import { parseBrowserRepoURL } from '../util/url' export interface UserHistoryEntry { @@ -17,32 +18,53 @@ const LOCAL_STORAGE_KEY = 'user-history' const MAX_LOCAL_STORAGE_COUNT = 100 /** - * Collects all browser history events and stores which repos/files are visited - * in local storage. In the future, we should consider storing this history - * remotely in temporary settings (or similar). The history is used to - * personalize ranking in the fuzzy finder, but could theorically power other - * features like improve ranking in the search bar suggestions. + * Collects all browser history events and stores which repos/files are visited in local + * storage. The history is used to personalize ranking in the fuzzy finder and populate + * suggestions in the Cody context selector. + * + * In the future, we should consider storing this history remotely in temporary settings + * and use them to power other features like improving ranking in the search bar + * suggestions. */ export class UserHistory { + private userID: Scalars['ID'] = 'anonymous' private repos: Map> = new Map() private storage = window.localStorage - constructor() { + constructor(userID: Scalars['ID'] = 'anonymous') { + this.userID = userID + this.migrateOldEntries() for (const entry of this.loadEntries()) { this.onEntry(entry) } } + private storageKey(userID: Scalars['ID']): string { + return `${LOCAL_STORAGE_KEY}:${userID}` + } + // User history entries were previously stored in a single array in local storage + // under the generic key `user-history`, which is not differentiated by user. The + // first time we reinitialize UserHistory and this method runs, we take any old + // entries and write them to the new user-differentiated key, and then delete the old + // key. When the method runs again in the future, it will thus be a no-op. + private migrateOldEntries(): void { + const oldJSON = this.storage.getItem(LOCAL_STORAGE_KEY) + if (!oldJSON) { + return + } + this.storage.setItem(this.storageKey(this.userID), oldJSON) + this.storage.removeItem(LOCAL_STORAGE_KEY) + } private saveEntries(entries: UserHistoryEntry[]): void { entries.sort((a, b) => b.lastAccessed - a.lastAccessed) const truncated = entries.slice(0, MAX_LOCAL_STORAGE_COUNT) - this.storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(truncated)) + this.storage.setItem(this.storageKey(this.userID), JSON.stringify(truncated)) for (let index = MAX_LOCAL_STORAGE_COUNT; index < entries.length; index++) { // Synchronize persisted entries with in-memory entries so that // reloading the page doesn't change which entries are available. this.deleteEntry(entries[index]) } } - private loadEntries(): UserHistoryEntry[] { - return JSON.parse(this.storage.getItem(LOCAL_STORAGE_KEY) ?? '[]') + public loadEntries(): UserHistoryEntry[] { + return JSON.parse(this.storage.getItem(this.storageKey(this.userID)) ?? '[]') } private deleteEntry(entry: UserHistoryEntry): void { if (!entry.filePath) { @@ -104,9 +126,30 @@ export class UserHistory { } } -export function useUserHistory(isRepositoryRelatedPage: boolean): UserHistory { +/** + * useUserHistory is a custom hook that collects browser history events for the current + * user and stores visited repos and files in local storage. + * + * It takes in the user ID of the current user and whether the current page is + * repository-related. On repository pages, it parses the location to extract the repo + * name and file path. It then updates the history entry for that repo/file with the + * current timestamp. + * + * The returned `UserHistory` instance provides methods to get the list of visited repos, + * and lookup the last accessed timestamp for a repo or file. + * + * The repo history is persisted to local storage and can be used to personalize and + * improve the search experience for the user. + * + * @param userID the ID of the currently-authenticated user, or undefined if the user is + * anonymous + * @param isRepositoryRelatedPage whether the component rendering this hook is on a page + * that is related to a repository (e.g. a code view page) and should be tracked + * @returns a `UserHistory` instance + */ +export function useUserHistory(userID: Scalars['ID'] | undefined, isRepositoryRelatedPage: boolean): UserHistory { const location = useLocation() - const userHistory = useMemo(() => new UserHistory(), []) + const userHistory = useMemo(() => new UserHistory(userID), [userID]) useEffect(() => { if (isRepositoryRelatedPage) { userHistory.onLocation(location) diff --git a/client/web/src/repo/RepoContainer.tsx b/client/web/src/repo/RepoContainer.tsx index 6a07446c7c2..7f8d0dab840 100644 --- a/client/web/src/repo/RepoContainer.tsx +++ b/client/web/src/repo/RepoContainer.tsx @@ -510,7 +510,10 @@ export const RepoContainer: FC = props => { storageKey="size-cache-cody-sidebar" onResize={setCodySidebarSize} > - setIsCodySidebarOpen(false)} /> + setIsCodySidebarOpen(false)} + authenticatedUser={props.authenticatedUser} + /> )} diff --git a/client/web/src/routes.tsx b/client/web/src/routes.tsx index b7fa0576469..b0b3bb62308 100644 --- a/client/web/src/routes.tsx +++ b/client/web/src/routes.tsx @@ -155,7 +155,7 @@ export const routes: RouteObject[] = [ element: ( ( - + )} diff --git a/client/web/src/storm/pages/LayoutPage/LayoutPage.tsx b/client/web/src/storm/pages/LayoutPage/LayoutPage.tsx index 3cc5877371a..d6f50b8b252 100644 --- a/client/web/src/storm/pages/LayoutPage/LayoutPage.tsx +++ b/client/web/src/storm/pages/LayoutPage/LayoutPage.tsx @@ -86,7 +86,7 @@ export const Layout: React.FC = props => { const isSetupWizardPage = location.pathname.startsWith(PageRoutes.SetupWizard) const [isFuzzyFinderVisible, setFuzzyFinderVisible] = useState(false) - const userHistory = useUserHistory(isRepositoryRelatedPage) + const userHistory = useUserHistory(props.authenticatedUser?.id, isRepositoryRelatedPage) const communitySearchContextPaths = communitySearchContextsRoutes.map(route => route.path) const isCommunitySearchContextPage = communitySearchContextPaths.includes(location.pathname) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4772656786..3e412ea4d3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1581,8 +1581,8 @@ importers: specifier: workspace:* version: link:../codeintellify '@sourcegraph/cody-shared': - specifier: ^0.0.5 - version: 0.0.5 + specifier: ^0.0.6 + version: 0.0.6 '@sourcegraph/cody-ui': specifier: ^0.0.7 version: 0.0.7 @@ -9358,6 +9358,21 @@ packages: - encoding dev: false + /@sourcegraph/cody-shared@0.0.6: + resolution: {integrity: sha512-Lnc/E8ooH3O2FuvvqLM2T+Vy3a0fPrFh1o4LsTYvMBdekcTz7g2ENyRcEEMwqcbn83TlnO5OqYIPqq/2DJFKLg==} + dependencies: + '@microsoft/fetch-event-source': 2.0.1 + dompurify: 3.0.4 + highlight.js: 10.7.3 + isomorphic-fetch: 3.0.0 + lodash: 4.17.21 + marked: 4.0.16 + vscode-uri: 3.0.7 + x2js: 3.4.4 + transitivePeerDependencies: + - encoding + dev: false + /@sourcegraph/cody-ui@0.0.7: resolution: {integrity: sha512-JhPcZppe01jMAWSUqYeCfGoy0Eq6W0+wgeAHjxBADDXmeNlswODgyTtNA4OBD5/8Rw6bYvsxEjnV2u9iWRCsZw==} dependencies: