feat(svelte): Add Cody chat sidebar (#63638)

This commit adds the React version of the cody chat sidebar to the
Svelte web app. I used @vovakulikov's work in the React app as guidance.

I'm sure we'll have to do follow up work, but it's a start.

This PR also fixes `sg start web-sveltekit-standalone` (because I
originally thought that running it from `sourcegraph.test` is necessary
to make auth work).

## Test plan

Manual testing. 


https://github.com/sourcegraph/sourcegraph/assets/179026/3fa3f2ea-b23e-44ca-a75a-0089bf07fb2b
This commit is contained in:
Felix Kling 2024-07-04 22:50:03 +02:00 committed by GitHub
parent 3e1c8fce97
commit f0c9551f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 264 additions and 18 deletions

View File

@ -64,6 +64,8 @@ BUILD_DEPS = [
"//:node_modules/@reach/menu-button",
"//:node_modules/@types/lodash",
"//:node_modules/@types/node",
"//:node_modules/@types/react",
"//:node_modules/@types/react-dom",
"//:node_modules/classnames",
"//:node_modules/copy-to-clipboard",
"//:node_modules/date-fns",
@ -71,11 +73,15 @@ BUILD_DEPS = [
"//:node_modules/lodash-es",
"//:node_modules/open-color",
"//:node_modules/path-browserify",
"//:node_modules/react",
"//:node_modules/react-dom",
"//:node_modules/react-resizable",
"//:node_modules/rxjs",
"//:node_modules/uuid",
":node_modules/@faker-js/faker",
":node_modules/@floating-ui/dom",
":node_modules/@fontsource-variable/inter",
":node_modules/@fontsource-variable/roboto-mono",
":node_modules/@graphql-codegen/cli",
":node_modules/@graphql-codegen/near-operation-file-preset",
":node_modules/@graphql-codegen/typed-document-node",
@ -92,19 +98,18 @@ BUILD_DEPS = [
":node_modules/@sourcegraph/branded",
":node_modules/@sourcegraph/client-api",
":node_modules/@sourcegraph/common",
":node_modules/@fontsource-variable/inter",
":node_modules/@fontsource-variable/roboto-mono",
":node_modules/@sourcegraph/http-client",
":node_modules/@sourcegraph/shared",
":node_modules/@sourcegraph/telemetry",
":node_modules/@sourcegraph/web",
":node_modules/@sourcegraph/wildcard",
":node_modules/@sourcegraph/telemetry",
":node_modules/@storybook/svelte",
":node_modules/@sveltejs/adapter-static",
":node_modules/@sveltejs/kit",
":node_modules/@sveltejs/vite-plugin-svelte",
":node_modules/@types/prismjs",
":node_modules/@urql/core",
":node_modules/cody-web-experimental",
":node_modules/fzf",
":node_modules/graphql",
":node_modules/hotkeys-js",

View File

@ -92,6 +92,7 @@
"@sourcegraph/wildcard": "workspace:*",
"@storybook/test": "^8.0.5",
"@urql/core": "^4.2.3",
"cody-web-experimental": "^0.1.4",
"copy-to-clipboard": "^3.3.1",
"fzf": "^0.5.2",
"highlight.js": "^10.0.0",

View File

@ -0,0 +1,4 @@
fragment CodySidebar_ResolvedRevision on Repository {
id
name
}

View File

@ -0,0 +1,73 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import Icon from '$lib/Icon.svelte'
import LoadingSpinner from '$lib/LoadingSpinner.svelte'
import { user } from '$lib/stores'
import Tooltip from '$lib/Tooltip.svelte'
import { Alert, Badge, Button } from '$lib/wildcard'
import type { CodySidebar_ResolvedRevision } from './CodySidebar.gql'
export let repository: CodySidebar_ResolvedRevision
export let filePath: string
const dispatch = createEventDispatcher<{ close: void }>()
</script>
<div class="root">
<div class="header">
<h3>
<Icon icon={ISgCody} /> Cody
<Badge variant="warning">Experimental</Badge>
</h3>
<Tooltip tooltip="Close Cody chat">
<Button variant="icon" aria-label="Close Cody" on:click={() => dispatch('close')}>
<Icon icon={ILucideX} inline aria-hidden />
</Button>
</Tooltip>
</div>
{#if $user}
{#await import('./CodySidebarChat.svelte')}
<LoadingSpinner />
{:then module}
<svelte:component this={module.default} {repository} {filePath} />
{/await}
{:else}
<Alert variant="info">
<strong>Cody is only available to signed-in users.</strong>
<a href="/sign-in">Sign in</a> to use Cody.
</Alert>
{/if}
</div>
<style lang="scss">
.root {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.header {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
font-size: var(--font-size-small);
background-color: var(--input-bg);
border-bottom: 1px solid var(--border-color-2);
padding: 0.25rem 1rem;
// Shows the cody icon in color
--icon-color: initial;
h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
margin: 0;
}
}
</style>

View File

@ -0,0 +1,93 @@
<script lang="ts">
import { CodyWebChat, CodyWebChatProvider } from 'cody-web-experimental'
import { createElement } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { onDestroy } from 'svelte'
import type { CodySidebar_ResolvedRevision } from './CodySidebar.gql'
import 'cody-web-experimental/dist/style.css'
import { createLocalWritable } from '$lib/stores'
export let repository: CodySidebar_ResolvedRevision
export let filePath: string
const chatIDs = createLocalWritable<Record<string, string>>('cody.context-to-chat-ids', {})
let container: HTMLDivElement
let root: Root | null
$: if (container) {
render(repository, filePath)
}
onDestroy(() => {
root?.unmount()
root = null
})
function render(repository: CodySidebar_ResolvedRevision, filePath: string) {
if (!root) {
root = createRoot(container)
}
const chat = createElement(CodyWebChat)
const provider = createElement(
CodyWebChatProvider,
{
accessToken: '',
chatID: $chatIDs[`${repository.id}-${filePath}`] ?? null,
initialContext: {
repositories: [repository],
fileURL: filePath ? (!filePath.startsWith('/') ? `/${filePath}` : filePath) : undefined,
},
serverEndpoint: window.location.origin,
onNewChatCreated: (chatID: string) => {
chatIDs.update(ids => {
ids[`${repository.id}-${filePath}`] = chatID
return ids
})
},
},
[chat]
)
root.render(provider)
}
</script>
<div class="chat" bind:this={container} />
<style lang="scss">
.chat {
--vscode-editor-background: var(--body-bg);
--vscode-editor-foreground: var(--body-color);
--vscode-input-background: var(--input-bg);
--vscode-input-foreground: var(--body-color);
--vscode-textLink-foreground: var(--primary);
--vscode-input-border: var(--border-color-2);
--vscode-inputOption-activeBackground: var(--search-input-token-filter);
--vscode-inputOption-activeForeground: var(--body-color);
--vscode-loading-dot-color: var(--body-color);
--mention-color-opacity: 100%;
height: 100%;
:global(h3) {
font-size: inherit;
margin: 0;
}
:global(ul) {
margin: 0;
}
:global(a) {
color: var(--link-color) !important;
}
}
:global([data-floating-ui-portal]) {
--vscode-quickInput-background: var(--secondary-2);
--vscode-widget-border: var(--border-color);
--vscode-list-activeSelectionBackground: var(--primary);
--vscode-foreground: var(--body-color);
--vscode-widget-shadow: rgba(36, 41, 54, 0.2);
}
</style>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import Icon from '$lib/Icon.svelte'
import { rightPanelOpen } from '$lib/repo/stores'
import Tooltip from '$lib/Tooltip.svelte'
function handleClick(): void {
$rightPanelOpen = true
}
</script>
<Tooltip tooltip="Open Cody chat">
<button on:click={handleClick}>
<Icon icon={ISgCody} />
<span data-action-label>Cody</span>
</button>
</Tooltip>
<style lang="scss">
button {
all: unset;
white-space: nowrap;
}
</style>

View File

@ -1,6 +1,7 @@
import { memoize } from 'lodash'
import { writable, type Writable } from 'svelte/store'
import { createLocalWritable } from '$lib/stores'
import { createEmptySingleSelectTreeState, type TreeState } from '$lib/TreeView'
/**
@ -11,3 +12,5 @@ export const getSidebarFileTreeStateForRepo = memoize(
(_repoName: string): Writable<TreeState> => writable<TreeState>(createEmptySingleSelectTreeState()),
repoName => repoName
)
export const rightPanelOpen = createLocalWritable<boolean>('repo.right-panel.open', false)

View File

@ -41,6 +41,7 @@
import { afterNavigate, goto } from '$app/navigation'
import { page } from '$app/stores'
import CodySidebar from '$lib/cody/CodySidebar.svelte'
import { isErrorLike, SourcegraphURL } from '$lib/common'
import { openFuzzyFinder } from '$lib/fuzzyfinder/FuzzyFinderContainer.svelte'
import { filesHotkey } from '$lib/fuzzyfinder/keys'
@ -50,6 +51,7 @@
import { fetchSidebarFileTree } from '$lib/repo/api/tree'
import HistoryPanel from '$lib/repo/HistoryPanel.svelte'
import LastCommit from '$lib/repo/LastCommit.svelte'
import { rightPanelOpen } from '$lib/repo/stores'
import TabPanel from '$lib/TabPanel.svelte'
import Tabs from '$lib/Tabs.svelte'
import Tooltip from '$lib/Tooltip.svelte'
@ -249,8 +251,22 @@
<Panel id="blob-content-panels" order={2}>
<PanelGroup id="content-panels" direction="vertical">
<Panel id="main-content-panel" order={1}>
<slot />
<Panel id="content-panel" order={1}>
<PanelGroup id="content-sidebar-panels">
<Panel order={1} id="main-content-panel">
<slot />
</Panel>
{#if $rightPanelOpen}
<PanelResizeHandle id="right-sidebar-resize-handle" />
<Panel id="right-sidebar-panel" order={2} minSize={20} maxSize={70}>
<CodySidebar
repository={data.resolvedRevision.repo}
filePath={data.filePath}
on:close={() => ($rightPanelOpen = false)}
/>
</Panel>
{/if}
</PanelGroup>
</Panel>
<PanelResizeHandle />
<Panel
@ -345,6 +361,7 @@
isolation: isolate;
}
:global([data-panel-resize-handle-id='right-sidebar-resize-handle']),
:global([data-panel-resize-handle-id='blob-page-panels-separator']) {
&::before {
// Even though side-panel shadow should be rendered over
@ -438,6 +455,11 @@
box-shadow: var(--bottom-panel-shadow);
}
:global([data-panel-id='right-sidebar-panel']) {
z-index: 1;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
}
.bottom-panel {
--align-tabs: flex-start;

View File

@ -53,15 +53,15 @@ fragment FileViewCodeGraphData on CodeGraphData {
fragment FileViewOccurrence on SCIPOccurrence {
symbol
range {
start {
line
character
}
end {
line
character
}
range {
start {
line
character
}
end {
line
character
}
}
roles
}

View File

@ -36,6 +36,7 @@
import { FileViewGitBlob, FileViewHighlightedFile } from './FileView.gql'
import FileViewModeSwitcher from './FileViewModeSwitcher.svelte'
import OpenInCodeHostAction from './OpenInCodeHostAction.svelte'
import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte'
import { CodeViewMode, toCodeViewMode } from './util'
export let data: Extract<PageData, { type: 'FileView' }>
@ -184,6 +185,7 @@
<OpenInCodeHostAction data={blob} lineOrPosition={data.lineOrPosition} />
{/if}
<Permalink {commitID} />
<OpenCodyAction />
</svelte:fragment>
<svelte:fragment slot="actionmenu">
<MenuLink href={rawURL} target="_blank">

View File

@ -64,8 +64,8 @@ query BlobFileViewCodeGraphDataQuery($repoName: String!, $revspec: String!, $pat
query BlobViewCodeGraphDataNextPage($codeGraphDataID: ID!, $after: String!) {
node(id: $codeGraphDataID) {
...on CodeGraphData {
occurrences(first: 10000, after: $after){
... on CodeGraphData {
occurrences(first: 10000, after: $after) {
nodes {
...FileViewOccurrence
}

View File

@ -10,6 +10,7 @@
import Readme from '$lib/repo/Readme.svelte'
import { createPromiseStore } from '$lib/utils'
import { Alert } from '$lib/wildcard'
import OpenCodyAction from '$lib/repo/OpenCodyAction.svelte'
import { getRepositoryPageContext } from '../../../../../context'
@ -37,6 +38,7 @@
<FileHeader type="tree" repoName={data.repoName} revision={data.revision} path={data.filePath}>
<svelte:fragment slot="actions">
<Permalink commitID={data.resolvedRevision.commitID} />
<OpenCodyAction />
</svelte:fragment>
</FileHeader>

View File

@ -34,4 +34,5 @@ fragment ResolvedRepository on Repository {
}
...RepoPage_ResolvedRevision
...BlobPage_ResolvedRevision
...CodySidebar_ResolvedRevision
}

View File

@ -72,18 +72,25 @@ export default defineConfig(({ mode }) => {
},
},
server: {
// When running behind caddy we have to listen to a different host.
host: process.env.SK_HOST || 'localhost',
// Allow setting the port via env variables to make it easier to integrate with
// our existing caddy setup (which proxies requests to a specific port).
port: process.env.SK_PORT ? +process.env.SK_PORT : undefined,
strictPort: !!process.env.SV_PORT,
strictPort: !!process.env.SK_PORT,
proxy: {
// Proxy requests to specific endpoints to a real Sourcegraph
// instance.
'^(/sign-in|/.assets|/-|/.api|/search/stream|/users|/notebooks|/insights|/batch-changes)|/-/(raw|compare|own|code-graph|batch-changes|settings)(/|$)':
'^(/sign-(in|out)|/.assets|/-|/.api|/.auth|/search/stream|/users|/notebooks|/insights|/batch-changes)|/-/(raw|compare|own|code-graph|batch-changes|settings)(/|$)':
{
target: process.env.SOURCEGRAPH_API_URL || 'https://sourcegraph.sourcegraph.com',
changeOrigin: true,
secure: false,
headers: {
// This needs to be set to make the cody sidebar work, which doesn't use the web graphql client work.
// todo(fkling): Figure out how the React app makes this work without this header.
'X-Requested-With': 'Sourcegraph',
},
},
},
},

View File

@ -1593,6 +1593,9 @@ importers:
'@urql/core':
specifier: ^4.2.3
version: 4.2.3(graphql@15.4.0)
cody-web-experimental:
specifier: ^0.1.4
version: 0.1.4
copy-to-clipboard:
specifier: ^3.3.1
version: 3.3.1

View File

@ -510,6 +510,12 @@ commands:
install: |
pnpm install
pnpm generate
env:
SOURCEGRAPH_API_URL: https://sourcegraph.sourcegraph.com
# The SvelteKit app uses this environment variable to determine where
# to store the generated assets. We don't need to store them in a different
# place in standalone mode.
DEPLOY_TYPE: ""
web-sveltekit-prod-watch:
description: Builds the prod version of the SvelteKit web app and rebuilds on changes
@ -1789,6 +1795,7 @@ commandsets:
- caddy
env:
SK_PORT: 3080
SK_HOST: 127.0.0.1
# For testing our OpenTelemetry stack
otel: