mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
Cody Agent: reuse TypeScript logic between JetBrains and VS Code (#53370)
Previously, the JetBrains plugin had to manually translate all the prompt logic from TypeScript from Java. This commit fixes the problem by introducing a Cody "agent" that runs as a JSON-RPC server and encapsulates all the prompt logic. The JetBrains plugin launches the Cody agent server during startup and communicates via the agent protocol to accomplish tasks like listing recipes and executing recipes. This commit only updates the JetBrains plugin to use the "chat-question" recipe. Remaining functionality like other recipes and completions will need to migrate to the agent in follow-up PRs. Based on https://github.com/sourcegraph/sourcegraph/pull/52800 by @SuperAuguste More details in the RFC https://docs.google.com/document/d/1KEJEd_Xx8STbJeGyOo86dkh3QRHQMs4CPw4XRAm7o7U/edit#heading=h.trqab8y0kufp ## Test plan - Green CI. - See new client/cody-agent/src/index.test.ts - Manual testing with `./gradlew :runIde -PdisableAgent=false`. First, validate that chat works. Secondly, validate that `build/sourcegraph/cody-agent-trace.json` is non-empty. <!-- All pull requests REQUIRE a test plan: https://docs.sourcegraph.com/dev/background-information/testing_principles -->
This commit is contained in:
parent
65e9fa5488
commit
c320b41764
@ -11,6 +11,7 @@ client/cody-cli/node_modules
|
||||
client/cody-icons-font/node_modules
|
||||
client/cody-shared/node_modules
|
||||
client/cody-slack/node_modules
|
||||
client/cody-agent/node_modules
|
||||
client/cody-ui/node_modules
|
||||
client/cody-web/node_modules
|
||||
client/common/node_modules
|
||||
|
||||
2
.github/workflows/jetbrains-tests.yml
vendored
2
.github/workflows/jetbrains-tests.yml
vendored
@ -30,6 +30,8 @@ jobs:
|
||||
- name: Gradle Wrapper Validation
|
||||
uses: gradle/wrapper-validation-action@v1.0.4
|
||||
|
||||
- run: yarn global add pnpm
|
||||
|
||||
# Setup Java 11 environment for the next steps
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v2
|
||||
|
||||
2
client/cody-agent/.eslintignore
Normal file
2
client/cody-agent/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
/dist/
|
||||
/out/
|
||||
21
client/cody-agent/.eslintrc.js
Normal file
21
client/cody-agent/.eslintrc.js
Normal file
@ -0,0 +1,21 @@
|
||||
// @ts-check
|
||||
|
||||
const baseConfig = require('../../.eslintrc')
|
||||
module.exports = {
|
||||
extends: '../../.eslintrc.js',
|
||||
parserOptions: {
|
||||
...baseConfig.parserOptions,
|
||||
project: [__dirname + '/tsconfig.json'],
|
||||
},
|
||||
overrides: baseConfig.overrides,
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'id-length': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: ['!highlight.js'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
1
client/cody-agent/.gitignore
vendored
Normal file
1
client/cody-agent/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/dist
|
||||
49
client/cody-agent/BUILD.bazel
generated
Normal file
49
client/cody-agent/BUILD.bazel
generated
Normal file
@ -0,0 +1,49 @@
|
||||
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
|
||||
load("@npm//:defs.bzl", "npm_link_all_packages")
|
||||
load("//dev:defs.bzl", "ts_project")
|
||||
load("//dev:eslint.bzl", "eslint_config_and_lint_root")
|
||||
|
||||
npm_link_all_packages(name = "node_modules")
|
||||
|
||||
eslint_config_and_lint_root()
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
visibility = ["//client:__subpackages__"],
|
||||
deps = [
|
||||
"//:tsconfig",
|
||||
"//client/cody-shared:tsconfig",
|
||||
"//client/common:tsconfig",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "cody-agent",
|
||||
srcs = [
|
||||
"src/agent.ts",
|
||||
"src/editor.ts",
|
||||
"src/index.ts",
|
||||
"src/jsonrpc.ts",
|
||||
"src/offsets.ts",
|
||||
"src/protocol.ts",
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
":node_modules/@sourcegraph/cody-shared",
|
||||
"//:node_modules/@types/node",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "cody-agent_tests",
|
||||
testonly = True,
|
||||
srcs = [
|
||||
"src/index.test.ts",
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
":cody-agent",
|
||||
"//:node_modules/@types/node",
|
||||
],
|
||||
)
|
||||
3
client/cody-agent/CODENOTIFY
Normal file
3
client/cody-agent/CODENOTIFY
Normal file
@ -0,0 +1,3 @@
|
||||
# See https://github.com/sourcegraph/codenotify for documentation.
|
||||
|
||||
src/protocol.ts @olafurpg
|
||||
54
client/cody-agent/README.md
Normal file
54
client/cody-agent/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Cody agent
|
||||
|
||||
The `@sourcegraph/cody-agent` package implements a JSON-RPC server to interact
|
||||
with Cody via stdout/stdin. This package is intended to be used by
|
||||
non-ECMAScript clients such as the JetBrains and NeoVim plugins.
|
||||
|
||||
## Protocol
|
||||
|
||||
The protocol is defined in the file [`src/protocol.ts`](src/protocol.ts). The
|
||||
TypeScript code is the single source of truth of what JSON-RPC methods are
|
||||
supported in the protocol.
|
||||
|
||||
## Updating the protocol
|
||||
|
||||
Directly edit the TypeScript source code to add new JSON-RPC methods or add
|
||||
properties to existing data structures.
|
||||
|
||||
The agent is a new project that is being actively worked on at the time of this
|
||||
writing. The protocol is subject to breaking changes without notice. Please
|
||||
let us know if you are implementing an agent client.
|
||||
|
||||
## Client bindings
|
||||
|
||||
There's no tool to automatically generate bindings for the Cody agent protocol.
|
||||
Currently, clients have to manually write bindings for the JSON-RPC methods.
|
||||
|
||||
## Useful commands
|
||||
|
||||
- The command `pnpm run build-agent-binaries` builds standalone binaries for
|
||||
macOS, Linux, and Windows. By default, the binaries get written to the `dist/`
|
||||
directory. The destination directory can be configured with the environment
|
||||
variable `AGENT_EXECUTABLE_TARGET_DIRECTORY`.
|
||||
- The command `pnpm run test` runs the agent against a minimized testing client.
|
||||
The tests are disabled in CI because they run against uses an actual Sourcegraph
|
||||
instance. Set the environment variables `SRC_ENDPOINT` and `SRC_ACCESS_TOKEN`
|
||||
to run the tests against an actual Sourcegraph instance.
|
||||
See the file [`src/index.test.ts`](src/index.test.ts) for a detailed but minimized example
|
||||
interaction between an agent client and agent server.
|
||||
|
||||
## Client implementations
|
||||
|
||||
- The Sourcegraph JetBrains plugin is defined in the sibling directory
|
||||
[`client/jetbrains`](../jetbrains/README.md). The file
|
||||
[`CodyAgentClient.java`](../jetbrains/src/main/java/com/sourcegraph/agent/CodyAgentClient.java)
|
||||
implements the client-side of the protocol.
|
||||
|
||||
## Miscellaneous notes
|
||||
|
||||
- By the nature of using JSON-RPC via stdin/stdout, both the agent server and
|
||||
client run on the same computer and there can only be one client per server.
|
||||
It's normal for both the client and server to be stateful processes. For
|
||||
example, the `connectionConfiguration/didChange` notification is sent from the
|
||||
client to the server to notify that subsequent requests should use the new
|
||||
connection configuration.
|
||||
11
client/cody-agent/jest.config.js
Normal file
11
client/cody-agent/jest.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
const config = require('../../jest.config.base')
|
||||
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
...config,
|
||||
displayName: 'cody-agent',
|
||||
rootDir: __dirname,
|
||||
}
|
||||
29
client/cody-agent/package.json
Normal file
29
client/cody-agent/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@sourcegraph/cody-agent",
|
||||
"version": "0.0.1",
|
||||
"description": "Cody JSON-RPC agent for consistent cross-editor support",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sourcegraph/sourcegraph.git",
|
||||
"directory": "client/cody-agent"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "esbuild ./src/index.ts --bundle --outfile=dist/agent.js --format=cjs --platform=node",
|
||||
"build-ts": "tsc -b",
|
||||
"build-agent-binaries": "pnpm run build && pkg -t node16-linux-arm64,node16-linux-x64,node16-macos-arm64,node16-macos-x64,node16-win-x64 dist/agent.js --out-path ${AGENT_EXECUTABLE_TARGET_DIRECTORY:-dist}",
|
||||
"lint": "pnpm run lint:js",
|
||||
"lint:js": "eslint --cache '**/*.[tj]s?(x)'",
|
||||
"test": "pnpm run build && jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sourcegraph/cody-shared": "workspace:*",
|
||||
"@sourcegraph/common": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pkg": "^5.8.1"
|
||||
}
|
||||
}
|
||||
100
client/cody-agent/src/agent.ts
Normal file
100
client/cody-agent/src/agent.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Client, createClient } from '@sourcegraph/cody-shared/src/chat/client'
|
||||
import { registeredRecipes } from '@sourcegraph/cody-shared/src/chat/recipes/agent-recipes'
|
||||
import { SourcegraphNodeCompletionsClient } from '@sourcegraph/cody-shared/src/sourcegraph-api/completions/nodeClient'
|
||||
|
||||
import { AgentEditor } from './editor'
|
||||
import { MessageHandler } from './jsonrpc'
|
||||
import { ConnectionConfiguration, TextDocument } from './protocol'
|
||||
|
||||
export class Agent extends MessageHandler {
|
||||
private client?: Promise<Client>
|
||||
public workspaceRootPath: string | null = null
|
||||
public activeDocumentFilePath: string | null = null
|
||||
public documents: Map<string, TextDocument> = new Map()
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.setClient({
|
||||
customHeaders: {},
|
||||
accessToken: process.env.SRC_ACCESS_TOKEN || '',
|
||||
serverEndpoint: process.env.SRC_ENDPOINT || 'https://sourcegraph.com',
|
||||
})
|
||||
|
||||
this.registerRequest('initialize', client => {
|
||||
process.stderr.write(
|
||||
`Cody Agent: handshake with client '${client.name}' (version '${client.version}') at workspace root path '${client.workspaceRootPath}'\n`
|
||||
)
|
||||
this.workspaceRootPath = client.workspaceRootPath
|
||||
if (client.connectionConfiguration) {
|
||||
this.setClient(client.connectionConfiguration)
|
||||
}
|
||||
return Promise.resolve({
|
||||
name: 'cody-agent',
|
||||
})
|
||||
})
|
||||
this.registerNotification('initialized', () => {})
|
||||
|
||||
this.registerRequest('shutdown', () => Promise.resolve(null))
|
||||
|
||||
this.registerNotification('exit', () => {
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
this.registerNotification('textDocument/didFocus', document => {
|
||||
this.activeDocumentFilePath = document.filePath
|
||||
})
|
||||
this.registerNotification('textDocument/didOpen', document => {
|
||||
this.documents.set(document.filePath, document)
|
||||
this.activeDocumentFilePath = document.filePath
|
||||
})
|
||||
this.registerNotification('textDocument/didChange', document => {
|
||||
if (document.content === undefined) {
|
||||
document.content = this.documents.get(document.filePath)?.content
|
||||
}
|
||||
this.documents.set(document.filePath, document)
|
||||
this.activeDocumentFilePath = document.filePath
|
||||
})
|
||||
this.registerNotification('textDocument/didClose', document => {
|
||||
this.documents.delete(document.filePath)
|
||||
})
|
||||
|
||||
this.registerNotification('connectionConfiguration/didChange', config => {
|
||||
this.setClient(config)
|
||||
})
|
||||
|
||||
this.registerRequest('recipes/list', () =>
|
||||
Promise.resolve(
|
||||
Object.values(registeredRecipes).map(({ id }) => ({
|
||||
id,
|
||||
title: id, // TODO: will be added in a follow PR
|
||||
}))
|
||||
)
|
||||
)
|
||||
|
||||
this.registerRequest('recipes/execute', async data => {
|
||||
const client = await this.client
|
||||
if (!client) {
|
||||
return null
|
||||
}
|
||||
await client.executeRecipe(data.id, {
|
||||
humanChatInput: data.humanChatInput,
|
||||
})
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
private setClient(config: ConnectionConfiguration): void {
|
||||
this.client = createClient({
|
||||
editor: new AgentEditor(this),
|
||||
config: { ...config, useContext: 'none' },
|
||||
setMessageInProgress: messageInProgress => {
|
||||
this.notify('chat/updateMessageInProgress', messageInProgress)
|
||||
},
|
||||
setTranscript: () => {
|
||||
// Not supported yet by agent.
|
||||
},
|
||||
createCompletionsClient: config => new SourcegraphNodeCompletionsClient(config),
|
||||
})
|
||||
}
|
||||
}
|
||||
99
client/cody-agent/src/editor.ts
Normal file
99
client/cody-agent/src/editor.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import {
|
||||
ActiveTextEditor,
|
||||
ActiveTextEditorSelection,
|
||||
ActiveTextEditorViewControllers,
|
||||
ActiveTextEditorVisibleContent,
|
||||
Editor,
|
||||
} from '@sourcegraph/cody-shared/src/editor'
|
||||
|
||||
import { Agent } from './agent'
|
||||
import { DocumentOffsets } from './offsets'
|
||||
import { TextDocument } from './protocol'
|
||||
|
||||
export class AgentEditor implements Editor {
|
||||
public controllers?: ActiveTextEditorViewControllers | undefined
|
||||
|
||||
constructor(private agent: Agent) {}
|
||||
|
||||
public didReceiveFixupText(): Promise<void> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
public getWorkspaceRootPath(): string | null {
|
||||
return this.agent.workspaceRootPath
|
||||
}
|
||||
|
||||
private activeDocument(): TextDocument | undefined {
|
||||
if (this.agent.activeDocumentFilePath === null) {
|
||||
return undefined
|
||||
}
|
||||
return this.agent.documents.get(this.agent.activeDocumentFilePath)
|
||||
}
|
||||
|
||||
public getActiveTextEditor(): ActiveTextEditor | null {
|
||||
const document = this.activeDocument()
|
||||
if (document === undefined) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
filePath: document.filePath,
|
||||
content: document.content || '',
|
||||
}
|
||||
}
|
||||
|
||||
public getActiveTextEditorSelection(): ActiveTextEditorSelection | null {
|
||||
const document = this.activeDocument()
|
||||
if (document === undefined || document.content === undefined || document.selection === undefined) {
|
||||
return null
|
||||
}
|
||||
const offsets = new DocumentOffsets(document)
|
||||
const from = offsets.offset(document.selection.start)
|
||||
const to = offsets.offset(document.selection.end)
|
||||
return {
|
||||
fileName: document.filePath || '',
|
||||
precedingText: document.content.slice(0, from),
|
||||
selectedText: document.content.slice(from, to),
|
||||
followingText: document.content.slice(to, document.content.length),
|
||||
}
|
||||
}
|
||||
|
||||
public getActiveTextEditorSelectionOrEntireFile(): ActiveTextEditorSelection | null {
|
||||
const document = this.activeDocument()
|
||||
if (document !== undefined && document.selection === undefined) {
|
||||
return {
|
||||
fileName: document.filePath || '',
|
||||
precedingText: '',
|
||||
selectedText: document.content || '',
|
||||
followingText: '',
|
||||
}
|
||||
}
|
||||
return this.getActiveTextEditorSelection()
|
||||
}
|
||||
|
||||
public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null {
|
||||
const document = this.activeDocument()
|
||||
if (document === undefined) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
content: document.content || '',
|
||||
fileName: document.filePath,
|
||||
}
|
||||
}
|
||||
|
||||
public replaceSelection(): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
public showQuickPick(): Promise<string | undefined> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
public showWarningMessage(): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
public showInputBox(): Promise<string | undefined> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
80
client/cody-agent/src/index.test.ts
Normal file
80
client/cody-agent/src/index.test.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import assert from 'assert'
|
||||
import { spawn } from 'child_process'
|
||||
import path from 'path'
|
||||
|
||||
import { RecipeID } from '@sourcegraph/cody-shared/src/chat/recipes/recipe'
|
||||
|
||||
import { MessageHandler } from './jsonrpc'
|
||||
|
||||
export class TestClient extends MessageHandler {
|
||||
public async handshake() {
|
||||
const info = await this.request('initialize', {
|
||||
name: 'test-client',
|
||||
version: 'v1',
|
||||
workspaceRootPath: '/path/to/foo',
|
||||
})
|
||||
this.notify('initialized', null)
|
||||
return info
|
||||
}
|
||||
|
||||
public listRecipes() {
|
||||
return this.request('recipes/list', null)
|
||||
}
|
||||
|
||||
public async executeRecipe(id: RecipeID, humanChatInput: string) {
|
||||
return this.request('recipes/execute', {
|
||||
id,
|
||||
humanChatInput,
|
||||
})
|
||||
}
|
||||
|
||||
public async shutdownAndExit() {
|
||||
await this.request('shutdown', null)
|
||||
this.notify('exit', null)
|
||||
}
|
||||
}
|
||||
|
||||
describe('StandardAgent', () => {
|
||||
if (process.env.SRC_ACCESS_TOKEN === undefined || process.env.SRC_ENDPOINT === undefined) {
|
||||
it('no-op test because SRC_ACCESS_TOKEN is not set. To actually run the Cody Agent tests, set the environment variables SRC_ENDPOINT and SRC_ACCESS_TOKEN', () => {})
|
||||
return
|
||||
}
|
||||
const client = new TestClient()
|
||||
const agentProcess = spawn('node', [path.join(__dirname, '../dist/agent.js')], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
agentProcess.stdout.pipe(client.messageDecoder)
|
||||
client.messageEncoder.pipe(agentProcess.stdin)
|
||||
agentProcess.stderr.on('data', msg => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
console.log(msg.toString())
|
||||
})
|
||||
|
||||
it('initializes properly', async () => {
|
||||
assert.deepStrictEqual(await client.handshake(), { name: 'cody-agent' }, 'Agent should be cody-agent')
|
||||
})
|
||||
|
||||
it('lists recipes correctly', async () => {
|
||||
const recipes = await client.listRecipes()
|
||||
assert(recipes.length === 8)
|
||||
})
|
||||
|
||||
const streamingChatMessages = new Promise<void>(resolve => {
|
||||
client.registerNotification('chat/updateMessageInProgress', msg => {
|
||||
if (msg === null) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('allows us to execute recipes properly', async () => {
|
||||
await client.executeRecipe('chat-question', "What's 2+2?")
|
||||
})
|
||||
|
||||
it('sends back transcript updates and makes sense', () => streamingChatMessages, 20_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await client.shutdownAndExit()
|
||||
})
|
||||
})
|
||||
10
client/cody-agent/src/index.ts
Normal file
10
client/cody-agent/src/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Agent } from './agent'
|
||||
|
||||
process.stderr.write('Starting Cody Agent...\n')
|
||||
|
||||
const agent = new Agent()
|
||||
|
||||
console.log = console.error
|
||||
|
||||
process.stdin.pipe(agent.messageDecoder)
|
||||
agent.messageEncoder.pipe(process.stdout)
|
||||
285
client/cody-agent/src/jsonrpc.ts
Normal file
285
client/cody-agent/src/jsonrpc.ts
Normal file
@ -0,0 +1,285 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import assert from 'assert'
|
||||
import { Readable, Writable } from 'stream'
|
||||
|
||||
import { Notifications, Requests } from './protocol'
|
||||
|
||||
// This file is a standalone implementation of JSON-RPC for Node.js
|
||||
// ReadStream/WriteStream, which conventionally map to stdin/stdout.
|
||||
// The code assumes familiarity with the JSON-RPC specification as documented
|
||||
// here https://www.jsonrpc.org/specification
|
||||
// To learn more about how JSON-RPC protocols work, the LSP specification is
|
||||
// also a good read
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
|
||||
|
||||
// String literal types for the names of the Cody Agent protocol methods.
|
||||
type RequestMethodName = keyof Requests
|
||||
type NotificationMethodName = keyof Notifications
|
||||
type MethodName = RequestMethodName | NotificationMethodName
|
||||
|
||||
// Parameter type of a request or notification. Note: JSON-RPC methods can only
|
||||
// accept one parameter. Multiple parameters must be encoded as an array or an
|
||||
// object.
|
||||
type ParamsOf<K extends MethodName> = (Requests & Notifications)[K][0]
|
||||
// Request result types. Note: notifications don't return values.
|
||||
type ResultOf<K extends RequestMethodName> = Requests[K][1]
|
||||
|
||||
type Id = string | number
|
||||
|
||||
// Error codes as defined by the JSON-RPC spec.
|
||||
enum ErrorCode {
|
||||
ParseError = -32700,
|
||||
InvalidRequest = -32600,
|
||||
MethodNotFound = -32601,
|
||||
InvalidParams = -32602,
|
||||
InternalError = -32603,
|
||||
}
|
||||
|
||||
// Result of an erroneous request, which populates the `error` property instead
|
||||
// of `result` for successful results.
|
||||
interface ErrorInfo<T> {
|
||||
code: ErrorCode
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// The three different kinds of toplevel JSON objects that get written to the
|
||||
// wire: requests, request responses, and notifications.
|
||||
interface RequestMessage<M extends RequestMethodName> {
|
||||
jsonrpc: '2.0'
|
||||
id: Id
|
||||
method: M
|
||||
params?: ParamsOf<M>
|
||||
}
|
||||
interface ResponseMessage<M extends RequestMethodName> {
|
||||
jsonrpc: '2.0'
|
||||
id: Id
|
||||
result?: ResultOf<M>
|
||||
error?: ErrorInfo<any>
|
||||
}
|
||||
interface NotificationMessage<M extends NotificationMethodName> {
|
||||
jsonrpc: '2.0'
|
||||
method: M
|
||||
params?: ParamsOf<M>
|
||||
}
|
||||
type Message = RequestMessage<any> & ResponseMessage<any> & NotificationMessage<any>
|
||||
|
||||
type MessageHandlerCallback = (err: Error | null, msg: Message | null) => void
|
||||
|
||||
class MessageDecoder extends Writable {
|
||||
private buffer: Buffer = Buffer.alloc(0)
|
||||
private contentLengthRemaining: number | null = null
|
||||
private contentBuffer: Buffer = Buffer.alloc(0)
|
||||
|
||||
constructor(public callback: MessageHandlerCallback) {
|
||||
super()
|
||||
}
|
||||
|
||||
public _write(chunk: Buffer, encoding: string, callback: (error?: Error | null) => void): void {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk])
|
||||
|
||||
// We loop through as we could have a double message that requires processing twice
|
||||
read: while (true) {
|
||||
if (this.contentLengthRemaining === null) {
|
||||
const headerString = this.buffer.toString()
|
||||
|
||||
let startIndex = 0
|
||||
let endIndex
|
||||
|
||||
// We create this as we might get partial messages
|
||||
// so we only want to set the content length
|
||||
// once we get the whole thing
|
||||
let newContentLength = 0
|
||||
|
||||
const LINE_TERMINATOR = '\r\n'
|
||||
|
||||
while ((endIndex = headerString.indexOf(LINE_TERMINATOR, startIndex)) !== -1) {
|
||||
const entry = headerString.slice(startIndex, endIndex)
|
||||
const [headerName, headerValue] = entry.split(':').map(_ => _.trim())
|
||||
|
||||
if (headerValue === undefined) {
|
||||
this.buffer = this.buffer.slice(endIndex + LINE_TERMINATOR.length)
|
||||
|
||||
// Asserts we actually have a valid header with a Content-Length
|
||||
// This state is irrecoverable because the stream is polluted
|
||||
// Also what is the client doing 😭
|
||||
this.contentLengthRemaining = newContentLength
|
||||
assert(
|
||||
isFinite(this.contentLengthRemaining),
|
||||
`parsed Content-Length ${this.contentLengthRemaining} is not a finite number`
|
||||
)
|
||||
continue read
|
||||
}
|
||||
|
||||
switch (headerName) {
|
||||
case 'Content-Length':
|
||||
newContentLength = parseInt(headerValue, 10)
|
||||
break
|
||||
|
||||
default:
|
||||
console.error(`Unknown header '${headerName}': ignoring!`)
|
||||
break
|
||||
}
|
||||
|
||||
startIndex = endIndex + LINE_TERMINATOR.length
|
||||
}
|
||||
|
||||
break
|
||||
} else {
|
||||
if (this.contentLengthRemaining === 0) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = JSON.parse(this.contentBuffer.toString())
|
||||
this.contentBuffer = Buffer.alloc(0)
|
||||
this.contentLengthRemaining = null
|
||||
this.callback(null, data)
|
||||
} catch (error) {
|
||||
this.callback(error, null)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const data = this.buffer.slice(0, this.contentLengthRemaining)
|
||||
this.contentBuffer = Buffer.concat([this.contentBuffer, data])
|
||||
this.buffer = this.buffer.slice(this.contentLengthRemaining)
|
||||
|
||||
this.contentLengthRemaining -= data.byteLength
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
class MessageEncoder extends Readable {
|
||||
private buffer: Buffer = Buffer.alloc(0)
|
||||
|
||||
public send(data: any): void {
|
||||
this.pause()
|
||||
|
||||
const content = Buffer.from(JSON.stringify(data), 'utf-8')
|
||||
const header = Buffer.from(`Content-Length: ${content.byteLength}\r\n\r\n`, 'utf-8')
|
||||
this.buffer = Buffer.concat([this.buffer, header, content])
|
||||
|
||||
this.resume()
|
||||
}
|
||||
|
||||
public _read(size: number): void {
|
||||
this.push(this.buffer.slice(0, size))
|
||||
this.buffer = this.buffer.slice(size)
|
||||
}
|
||||
}
|
||||
|
||||
type RequestCallback<M extends RequestMethodName> = (params: ParamsOf<M>) => Promise<ResultOf<M>>
|
||||
type NotificationCallback<M extends NotificationMethodName> = (params: ParamsOf<M>) => void
|
||||
|
||||
/**
|
||||
* Only exported API in this file. MessageHandler exposes a public `messageDecoder` property
|
||||
* that can be piped with ReadStream/WriteStream.
|
||||
*/
|
||||
export class MessageHandler {
|
||||
private id = 0
|
||||
private requestHandlers: Map<RequestMethodName, RequestCallback<any>> = new Map()
|
||||
private notificationHandlers: Map<NotificationMethodName, NotificationCallback<any>> = new Map()
|
||||
private responseHandlers: Map<Id, (params: any) => void> = new Map()
|
||||
|
||||
// TODO: RPC error handling
|
||||
public messageDecoder: MessageDecoder = new MessageDecoder((err: Error | null, msg: Message | null) => {
|
||||
if (err) {
|
||||
console.error(`Error: ${err}`)
|
||||
}
|
||||
if (!msg) {
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.id !== undefined && msg.method) {
|
||||
if (typeof msg.id === 'number' && msg.id > this.id) {
|
||||
this.id = msg.id + 1
|
||||
}
|
||||
|
||||
// Requests have ids and methods
|
||||
const handler = this.requestHandlers.get(msg.method)
|
||||
if (handler) {
|
||||
handler(msg.params).then(
|
||||
result => {
|
||||
const data: ResponseMessage<any> = {
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
result,
|
||||
}
|
||||
this.messageEncoder.send(data)
|
||||
},
|
||||
error => {
|
||||
const message = error instanceof Error ? error.message : `${error}`
|
||||
const data: ResponseMessage<any> = {
|
||||
jsonrpc: '2.0',
|
||||
id: msg.id,
|
||||
error: {
|
||||
code: ErrorCode.InternalError,
|
||||
message,
|
||||
data: JSON.stringify(error),
|
||||
},
|
||||
}
|
||||
this.messageEncoder.send(data)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
console.error(`No handler for request with method ${msg.method}`)
|
||||
}
|
||||
} else if (msg.id !== undefined) {
|
||||
// Responses have ids
|
||||
const handler = this.responseHandlers.get(msg.id)
|
||||
if (handler) {
|
||||
handler(msg.result)
|
||||
this.responseHandlers.delete(msg.id)
|
||||
} else {
|
||||
console.error(`No handler for response with id ${msg.id}`)
|
||||
}
|
||||
} else if (msg.method) {
|
||||
// Notifications have methods
|
||||
const notificationHandler = this.notificationHandlers.get(msg.method)
|
||||
if (notificationHandler) {
|
||||
notificationHandler(msg.params)
|
||||
} else {
|
||||
console.error(`No handler for notification with method ${msg.method}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
public messageEncoder: MessageEncoder = new MessageEncoder()
|
||||
|
||||
public registerRequest<M extends RequestMethodName>(method: M, callback: RequestCallback<M>): void {
|
||||
this.requestHandlers.set(method, callback)
|
||||
}
|
||||
|
||||
public registerNotification<M extends NotificationMethodName>(method: M, callback: NotificationCallback<M>): void {
|
||||
this.notificationHandlers.set(method, callback)
|
||||
}
|
||||
|
||||
public request<M extends RequestMethodName>(method: M, params: ParamsOf<M>): Promise<ResultOf<M>> {
|
||||
const id = this.id++
|
||||
|
||||
const data: RequestMessage<M> = {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
}
|
||||
this.messageEncoder.send(data)
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.responseHandlers.set(id, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
public notify<M extends NotificationMethodName>(method: M, params: ParamsOf<M>): void {
|
||||
const data: NotificationMessage<M> = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
params,
|
||||
}
|
||||
this.messageEncoder.send(data)
|
||||
}
|
||||
}
|
||||
27
client/cody-agent/src/offsets.ts
Normal file
27
client/cody-agent/src/offsets.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Position, TextDocument } from './protocol'
|
||||
|
||||
/**
|
||||
* Utility class to convert line/character positions into offsets.
|
||||
*/
|
||||
export class DocumentOffsets {
|
||||
private lines: number[] = []
|
||||
constructor(public readonly document: TextDocument) {
|
||||
if (document.content) {
|
||||
this.lines.push(0)
|
||||
let index = 1
|
||||
while (index < document.content.length) {
|
||||
if (document.content[index] === '\n') {
|
||||
this.lines.push(index + 1)
|
||||
}
|
||||
index++
|
||||
}
|
||||
if (document.content.length !== this.lines[this.lines.length - 1]) {
|
||||
this.lines.push(document.content.length) // sentinel value
|
||||
}
|
||||
}
|
||||
}
|
||||
public offset(position: Position): number {
|
||||
const lineStartOffset = this.lines[position.line]
|
||||
return lineStartOffset + position.character
|
||||
}
|
||||
}
|
||||
134
client/cody-agent/src/protocol.ts
Normal file
134
client/cody-agent/src/protocol.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
import { RecipeID } from '@sourcegraph/cody-shared/src/chat/recipes/recipe'
|
||||
import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
|
||||
|
||||
// This file documents the Cody Agent JSON-RPC protocol. Consult the JSON-RPC
|
||||
// specification to learn about how JSON-RPC works https://www.jsonrpc.org/specification
|
||||
// The Cody Agent server only supports transport via stdout/stdin.
|
||||
|
||||
// The JSON-RPC requests of the Cody Agent protocol. Requests are async
|
||||
// functions that return some (possibly null) value.
|
||||
export type Requests = {
|
||||
// ================
|
||||
// Client -> Server
|
||||
// ================
|
||||
|
||||
// The 'initialize' request must be sent at the start of the connection
|
||||
// before any other request/notification is sent.
|
||||
initialize: [ClientInfo, ServerInfo]
|
||||
// The 'shutdown' request must be sent before terminating the agent process.
|
||||
shutdown: [null, null]
|
||||
|
||||
// Client requests the agent server to lists all recipes that are supported
|
||||
// by the agent.
|
||||
'recipes/list': [null, RecipeInfo[]]
|
||||
// Client requests the agent server to execute an individual recipe.
|
||||
// The response is null because the AI/Assistant messages are streamed through
|
||||
// the chat/updateMessageInProgress notification. The flow to trigger a recipe
|
||||
// is like this:
|
||||
// client --- recipes/execute --> server
|
||||
// client <-- chat/updateMessageInProgress --- server
|
||||
// ....
|
||||
// client <-- chat/updateMessageInProgress --- server
|
||||
'recipes/execute': [ExecuteRecipeParams, null]
|
||||
|
||||
// ================
|
||||
// Server -> Client
|
||||
// ================
|
||||
}
|
||||
|
||||
// The JSON-RPC notifications of the Cody Agent protocol. Notifications are
|
||||
// synchronous fire-and-forget messages that have no return value. Notifications are
|
||||
// conventionally used to represent streams of values.
|
||||
export type Notifications = {
|
||||
// ================
|
||||
// Client -> Server
|
||||
// ================
|
||||
|
||||
// The 'initalized' notification must be sent after receiving the 'initialize' response.
|
||||
initialized: [null]
|
||||
// The 'exit' notification must be sent after the client receives the 'shutdown' response.
|
||||
exit: [null]
|
||||
|
||||
// The server should use the provided connection configuration for all
|
||||
// subsequent requests/notications. The previous connection configuration
|
||||
// should no longer be used.
|
||||
'connectionConfiguration/didChange': [ConnectionConfiguration]
|
||||
|
||||
// Lifecycle notifications for the client to notify the server about text
|
||||
// contents of documents and to notify which document is currently focused.
|
||||
'textDocument/didOpen': [TextDocument]
|
||||
// The 'textDocument/didChange' notification should be sent on almost every
|
||||
// keystroke, whether the text contents changed or the cursor/selection
|
||||
// changed. Leave the `content` property undefined when the document's
|
||||
// content is unchanged.
|
||||
'textDocument/didChange': [TextDocument]
|
||||
// The user focused on a document without changing the document's content.
|
||||
// Only the 'uri' property is required, other properties are ignored.
|
||||
'textDocument/didFocus': [TextDocument]
|
||||
// The user closed the editor tab for the given document.
|
||||
// Only the 'uri' property is required, other properties are ignored.
|
||||
'textDocument/didClose': [TextDocument]
|
||||
|
||||
// ================
|
||||
// Server -> Client
|
||||
// ================
|
||||
// The server received new messages for the ongoing 'chat/executeRecipe'
|
||||
// request. The server should never send this notification outside of a
|
||||
// 'chat/executeRecipe' request.
|
||||
'chat/updateMessageInProgress': [ChatMessage | null]
|
||||
}
|
||||
|
||||
export interface ClientInfo {
|
||||
name: string
|
||||
version: string
|
||||
workspaceRootPath: string
|
||||
connectionConfiguration?: ConnectionConfiguration
|
||||
capabilities?: ClientCapabilities
|
||||
}
|
||||
|
||||
export interface ClientCapabilities {
|
||||
completions?: 'none'
|
||||
// When 'streaming', handles 'chat/updateMessageInProgress' streaming notifications.
|
||||
chat?: 'none' | 'streaming'
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
name: string
|
||||
capabilities?: ServerCapabilities
|
||||
}
|
||||
export interface ServerCapabilities {}
|
||||
|
||||
export interface ConnectionConfiguration {
|
||||
serverEndpoint: string
|
||||
accessToken: string
|
||||
customHeaders: Record<string, string>
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
// 0-indexed
|
||||
line: number
|
||||
// 0-indexed
|
||||
character: number
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
start: Position
|
||||
end: Position
|
||||
}
|
||||
|
||||
export interface TextDocument {
|
||||
filePath: string
|
||||
content?: string
|
||||
selection?: Range
|
||||
}
|
||||
|
||||
export interface RecipeInfo {
|
||||
id: RecipeID
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface ExecuteRecipeParams {
|
||||
id: RecipeID
|
||||
humanChatInput: string
|
||||
}
|
||||
12
client/cody-agent/tsconfig.json
Normal file
12
client/cody-agent/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"rootDir": ".",
|
||||
"sourceRoot": "src",
|
||||
"outDir": "out",
|
||||
},
|
||||
"include": ["**/*", ".*", "package.json"],
|
||||
"exclude": ["out"],
|
||||
"references": [{ "path": "../common" }, { "path": "../cody-shared" }],
|
||||
}
|
||||
1
client/cody-shared/BUILD.bazel
generated
1
client/cody-shared/BUILD.bazel
generated
@ -29,6 +29,7 @@ ts_project(
|
||||
"src/chat/context.ts",
|
||||
"src/chat/markdown.ts",
|
||||
"src/chat/preamble.ts",
|
||||
"src/chat/recipes/agent-recipes.ts",
|
||||
"src/chat/recipes/browser-recipes.ts",
|
||||
"src/chat/recipes/chat-question.ts",
|
||||
"src/chat/recipes/context-search.ts",
|
||||
|
||||
@ -5,6 +5,7 @@ import { PrefilledOptions, withPreselectedOptions } from '../editor/withPreselec
|
||||
import { SourcegraphEmbeddingsSearchClient } from '../embeddings/client'
|
||||
import { SourcegraphIntentDetectorClient } from '../intent-detector/client'
|
||||
import { SourcegraphBrowserCompletionsClient } from '../sourcegraph-api/completions/browserClient'
|
||||
import { CompletionsClientConfig, SourcegraphCompletionsClient } from '../sourcegraph-api/completions/client'
|
||||
import { SourcegraphGraphQLAPIClient } from '../sourcegraph-api/graphql'
|
||||
import { isError } from '../utils'
|
||||
|
||||
@ -20,15 +21,18 @@ import { reformatBotMessage } from './viewHelpers'
|
||||
export type { TranscriptJSON }
|
||||
export { Transcript }
|
||||
|
||||
export type ClientInitConfig = Pick<
|
||||
ConfigurationWithAccessToken,
|
||||
'serverEndpoint' | 'codebase' | 'useContext' | 'accessToken' | 'customHeaders'
|
||||
>
|
||||
|
||||
export interface ClientInit {
|
||||
config: Pick<
|
||||
ConfigurationWithAccessToken,
|
||||
'serverEndpoint' | 'codebase' | 'useContext' | 'accessToken' | 'customHeaders'
|
||||
>
|
||||
config: ClientInitConfig
|
||||
setMessageInProgress: (messageInProgress: ChatMessage | null) => void
|
||||
setTranscript: (transcript: Transcript) => void
|
||||
editor: Editor
|
||||
initialTranscript?: Transcript
|
||||
createCompletionsClient?: (config: CompletionsClientConfig) => SourcegraphCompletionsClient
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
@ -52,10 +56,11 @@ export async function createClient({
|
||||
setTranscript,
|
||||
editor,
|
||||
initialTranscript,
|
||||
createCompletionsClient = config => new SourcegraphBrowserCompletionsClient(config),
|
||||
}: ClientInit): Promise<Client> {
|
||||
const fullConfig = { debugEnable: false, ...config }
|
||||
|
||||
const completionsClient = new SourcegraphBrowserCompletionsClient(fullConfig)
|
||||
const completionsClient = createCompletionsClient(fullConfig)
|
||||
const chatClient = new ChatClient(completionsClient)
|
||||
|
||||
const graphqlClient = new SourcegraphGraphQLAPIClient(fullConfig)
|
||||
|
||||
50
client/cody-shared/src/chat/recipes/agent-recipes.ts
Normal file
50
client/cody-shared/src/chat/recipes/agent-recipes.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { ChatQuestion } from './chat-question'
|
||||
import { ExplainCodeDetailed } from './explain-code-detailed'
|
||||
import { ExplainCodeHighLevel } from './explain-code-high-level'
|
||||
import { FindCodeSmells } from './find-code-smells'
|
||||
import { GenerateDocstring } from './generate-docstring'
|
||||
import { GenerateTest } from './generate-test'
|
||||
import { ImproveVariableNames } from './improve-variable-names'
|
||||
import { Recipe, RecipeID } from './recipe'
|
||||
import { TranslateToLanguage } from './translate'
|
||||
|
||||
function nullLog(filterLabel: string, text: string, ...args: unknown[]): void {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
export const registeredRecipes: { [id in RecipeID]?: Recipe } = {}
|
||||
|
||||
export function getRecipe(id: RecipeID): Recipe | undefined {
|
||||
return registeredRecipes[id]
|
||||
}
|
||||
|
||||
function registerRecipe(id: RecipeID, recipe: Recipe): void {
|
||||
registeredRecipes[id] = recipe
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
if (Object.keys(registeredRecipes).length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const recipes: Recipe[] = [
|
||||
new ChatQuestion(nullLog),
|
||||
new ExplainCodeDetailed(),
|
||||
new ExplainCodeHighLevel(),
|
||||
new GenerateDocstring(),
|
||||
new GenerateTest(),
|
||||
new ImproveVariableNames(),
|
||||
new TranslateToLanguage(),
|
||||
new FindCodeSmells(),
|
||||
]
|
||||
|
||||
for (const recipe of recipes) {
|
||||
const existingRecipe = getRecipe(recipe.id)
|
||||
if (existingRecipe) {
|
||||
throw new Error(`Duplicate recipe with ID ${recipe.id}`)
|
||||
}
|
||||
registerRecipe(recipe.id, recipe)
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
@ -12,7 +12,7 @@ export interface CompletionLogger {
|
||||
}
|
||||
}
|
||||
|
||||
export type Config = Pick<
|
||||
export type CompletionsClientConfig = Pick<
|
||||
ConfigurationWithAccessToken,
|
||||
'serverEndpoint' | 'accessToken' | 'debugEnable' | 'customHeaders'
|
||||
>
|
||||
@ -20,9 +20,9 @@ export type Config = Pick<
|
||||
export abstract class SourcegraphCompletionsClient {
|
||||
private errorEncountered = false
|
||||
|
||||
constructor(protected config: Config, protected logger?: CompletionLogger) {}
|
||||
constructor(protected config: CompletionsClientConfig, protected logger?: CompletionLogger) {}
|
||||
|
||||
public onConfigurationChange(newConfig: Config): void {
|
||||
public onConfigurationChange(newConfig: CompletionsClientConfig): void {
|
||||
this.config = newConfig
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,15 @@ New issues and feature requests can be filed through our [issue tracker](https:/
|
||||
- If you are using an M1 MacBook and get a JCEF-related error using the "Find with Sourcegraph" command, try
|
||||
running `./gradlew -PplatformVersion=221.5080.210 :runIde` instead.
|
||||
See https://youtrack.jetbrains.com/issue/IDEA-291946 for more details.
|
||||
- To debug communication between the IntelliJ plugin and Cody agent, it's useful to keep an open terminal tab that's
|
||||
running the command `fail -f build/sourcegraph/cody-agent-trace.json`.
|
||||
- The Cody agent is a JSON-RPC server that implements the promt logic for Cody. The JetBrains plugin needs access to the
|
||||
agent binary to function propertly. This agent binary is automatically built from source if it does not exist. To
|
||||
speed up edit/test/debug feedback loops, the agent binary does not get rebuilt unless you provdide the
|
||||
`-PforceAgentBuild=true` flag when running Gradle. For example, `./gradlew :runIde -PforceAgentBuild=true`.
|
||||
- Use the `-PenableAgent=true` property to enable the Cody agent. For example, `./gradlew :runIde -PenableAgent=true`.
|
||||
When the agent is disabled, the plugin falls back to the non-agent based implementation. Once we have more
|
||||
confidence with the agent, we will turn this on by default.
|
||||
|
||||
## Publishing a new version
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import org.jetbrains.changelog.markdownToHTML
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
fun properties(key: String) = project.findProperty(key).toString()
|
||||
|
||||
@ -33,6 +35,7 @@ intellij {
|
||||
dependencies {
|
||||
implementation("org.commonmark:commonmark:0.21.0")
|
||||
implementation("org.commonmark:commonmark-ext-gfm-tables:0.21.0")
|
||||
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.21.0")
|
||||
testImplementation(platform("org.junit:junit-bom:5.7.2"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
}
|
||||
@ -76,15 +79,15 @@ tasks {
|
||||
|
||||
// Extract the <!-- Plugin description --> section from README.md and provide for the plugin's manifest
|
||||
pluginDescription.set(
|
||||
projectDir.resolve("README.md").readText().lines().run {
|
||||
val start = "<!-- Plugin description -->"
|
||||
val end = "<!-- Plugin description end -->"
|
||||
projectDir.resolve("README.md").readText().lines().run {
|
||||
val start = "<!-- Plugin description -->"
|
||||
val end = "<!-- Plugin description end -->"
|
||||
|
||||
if (!containsAll(listOf(start, end))) {
|
||||
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
|
||||
}
|
||||
subList(indexOf(start) + 1, indexOf(end))
|
||||
}.joinToString("\n").run { markdownToHTML(this) }
|
||||
if (!containsAll(listOf(start, end))) {
|
||||
throw GradleException("Plugin description section not found in README.md:\n$start ... $end")
|
||||
}
|
||||
subList(indexOf(start) + 1, indexOf(end))
|
||||
}.joinToString("\n").run { markdownToHTML(this) }
|
||||
)
|
||||
|
||||
// Get the latest available change notes from the changelog file
|
||||
@ -95,8 +98,56 @@ tasks {
|
||||
})
|
||||
}
|
||||
|
||||
val agentSourceDirectory = Paths.get("..", "cody-agent").normalize()
|
||||
val agentTargetDirectory =
|
||||
buildDir.resolve("sourcegraph").resolve("agent").toPath()
|
||||
|
||||
fun cleanAgentTargetDirectory() {
|
||||
if (Files.isDirectory(agentTargetDirectory)) {
|
||||
agentTargetDirectory.toFile().deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
fun copyAgentBinariesToPluginPath(targetPath: String) {
|
||||
val shouldBuildBinaries =
|
||||
findProperty("forceAgentBuild") == "true" ||
|
||||
!Files.isDirectory(agentTargetDirectory) ||
|
||||
agentTargetDirectory.toFile().list().isEmpty()
|
||||
if (shouldBuildBinaries) {
|
||||
exec {
|
||||
commandLine("pnpm", "install")
|
||||
workingDir(agentSourceDirectory.toString())
|
||||
}
|
||||
exec {
|
||||
commandLine("pnpm", "run", "build-agent-binaries")
|
||||
workingDir(agentSourceDirectory.toString())
|
||||
environment("AGENT_EXECUTABLE_TARGET_DIRECTORY", targetPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
register("copyAgentBinariesToPluginPath") {
|
||||
doLast {
|
||||
copyAgentBinariesToPluginPath(agentTargetDirectory.toString())
|
||||
}
|
||||
}
|
||||
|
||||
buildPlugin {
|
||||
dependsOn("copyAgentBinariesToPluginPath")
|
||||
// Copy agent binaries into the zip file that `buildPlugin` produces.
|
||||
from(fileTree(agentTargetDirectory.parent.toString()) {
|
||||
include("agent/*")
|
||||
}) {
|
||||
into("agent")
|
||||
}
|
||||
}
|
||||
|
||||
runIde {
|
||||
dependsOn("copyAgentBinariesToPluginPath")
|
||||
jvmArgs("-Djdk.module.illegalAccess.silent=true")
|
||||
systemProperty("cody-agent.trace-path", "$buildDir/sourcegraph/cody-agent-trace.json")
|
||||
systemProperty("cody-agent.directory", agentTargetDirectory.parent.toString())
|
||||
val isAgentEnabled = findProperty("disableAgent") == "true"
|
||||
systemProperty("cody-agent.enabled", (!isAgentEnabled).toString())
|
||||
}
|
||||
|
||||
// Configure UI tests plugin
|
||||
@ -114,6 +165,7 @@ tasks {
|
||||
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
|
||||
}
|
||||
|
||||
|
||||
publishPlugin {
|
||||
dependsOn("patchChangelog")
|
||||
token.set(System.getenv("PUBLISH_TOKEN"))
|
||||
|
||||
@ -15,7 +15,7 @@ popd > /dev/null || exit
|
||||
pnpm build
|
||||
|
||||
# Build the release candidate and publish it onto the registry
|
||||
./gradlew publishPlugin
|
||||
./gradlew -PforceAgentBuild=true publishPlugin
|
||||
|
||||
# The release script automatically changes the README and moves the unreleased
|
||||
# section into a version numbered one. We don't care about this while we are
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package com.sourcegraph.cody;
|
||||
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.ex.FocusChangeListener;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import com.sourcegraph.cody.agent.CodyAgentServer;
|
||||
import com.sourcegraph.cody.agent.protocol.TextDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class CodyAgentFocusListener implements FocusChangeListener {
|
||||
@Override
|
||||
public void focusGained(@NotNull Editor editor) {
|
||||
if (editor.getProject() == null) {
|
||||
return;
|
||||
}
|
||||
VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
CodyAgentServer server = CodyAgent.getServer(editor.getProject());
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
server.textDocumentDidFocus(new TextDocument().setFilePath(file.getPath()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.sourcegraph.cody;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectManagerListener;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class CodyAgentProjectListener implements ProjectManagerListener {
|
||||
@Override
|
||||
public void projectOpened(@NotNull Project project) {
|
||||
CodyAgent service = project.getService(CodyAgent.class);
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
service.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void projectClosed(@NotNull Project project) {
|
||||
CodyAgent service = project.getService(CodyAgent.class);
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
service.shutdown();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.sourcegraph.cody;
|
||||
|
||||
public class CodyComponent {
|
||||
public void initComponent() {}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.sourcegraph.cody;
|
||||
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import com.sourcegraph.cody.agent.CodyAgentServer;
|
||||
import com.sourcegraph.cody.agent.protocol.TextDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class CodyFileEditorListener implements FileEditorManagerListener {
|
||||
@Override
|
||||
public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
|
||||
Document document = FileDocumentManager.getInstance().getDocument(file);
|
||||
if (document == null) {
|
||||
return;
|
||||
}
|
||||
CodyAgentServer server = CodyAgent.getServer(source.getProject());
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
server.textDocumentDidOpen(
|
||||
new TextDocument().setFilePath(file.getPath()).setContent(document.getText()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
|
||||
CodyAgentServer server = CodyAgent.getServer(source.getProject());
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
server.textDocumentDidClose(new TextDocument().setFilePath(file.getPath()));
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI;
|
||||
import com.intellij.ide.ui.laf.darcula.ui.DarculaTextAreaUI;
|
||||
import com.intellij.openapi.actionSystem.*;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.DumbAwareAction;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.VerticalFlowLayout;
|
||||
@ -39,6 +40,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class CodyToolWindowContent implements UpdatableChat {
|
||||
public static Logger logger = Logger.getInstance(CodyToolWindowContent.class);
|
||||
private static final int CHAT_TAB_INDEX = 0;
|
||||
private static final int RECIPES_TAB_INDEX = 1;
|
||||
private final @NotNull JBTabbedPane tabbedPane = new JBTabbedPane();
|
||||
@ -382,7 +384,6 @@ class CodyToolWindowContent implements UpdatableChat {
|
||||
isEnterprise
|
||||
? ConfigUtil.getEnterpriseAccessToken(project)
|
||||
: ConfigUtil.getDotComAccessToken(project);
|
||||
System.out.println("isEnterprise: " + isEnterprise);
|
||||
|
||||
var chat = new Chat("", instanceUrl, accessToken != null ? accessToken : "");
|
||||
ArrayList<String> contextFiles =
|
||||
@ -397,7 +398,11 @@ class CodyToolWindowContent implements UpdatableChat {
|
||||
// in the main thread and then waited, we wouldn't see the messages streamed back to us.
|
||||
new Thread(
|
||||
() -> {
|
||||
chat.sendMessage(humanMessage, responsePrefix, this); // TODO: Use prefix
|
||||
try {
|
||||
chat.sendMessage(project, humanMessage, responsePrefix, this);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error sending message '" + humanMessage + "' to chat", e);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
@ -0,0 +1,233 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import com.intellij.ide.plugins.IdeaPluginDescriptor;
|
||||
import com.intellij.ide.plugins.PluginManagerCore;
|
||||
import com.intellij.openapi.Disposable;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.editor.EditorFactory;
|
||||
import com.intellij.openapi.editor.event.EditorEventMulticaster;
|
||||
import com.intellij.openapi.editor.ex.EditorEventMulticasterEx;
|
||||
import com.intellij.openapi.extensions.PluginId;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.util.SystemInfoRt;
|
||||
import com.intellij.util.system.CpuArch;
|
||||
import com.sourcegraph.cody.CodyAgentFocusListener;
|
||||
import com.sourcegraph.cody.agent.protocol.ClientInfo;
|
||||
import com.sourcegraph.cody.agent.protocol.ServerInfo;
|
||||
import com.sourcegraph.config.ConfigUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import org.eclipse.lsp4j.jsonrpc.Launcher;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Orchestrator for the Cody agent, which is a Node.js program that implements the prompt logic for
|
||||
* Cody. The agent communicates via a JSON-RPC protocol that is documented in the file
|
||||
* "client/cody-agent/src/protocol.ts".
|
||||
*
|
||||
* <p>The class {{{@link com.sourcegraph.cody.CodyAgentProjectListener}}} is responsible for
|
||||
* initializing and shutting down the agent.
|
||||
*/
|
||||
public class CodyAgent implements Disposable {
|
||||
public static Logger logger = Logger.getInstance(CodyAgent.class);
|
||||
private static final @NotNull PluginId PLUGIN_ID = PluginId.getId("com.sourcegraph.jetbrains");
|
||||
public static final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
|
||||
Disposable disposable = Disposer.newDisposable("CodyAgent");
|
||||
private final @NotNull Project project;
|
||||
private final CodyAgentClient client = new CodyAgentClient();
|
||||
private String initializationErrorMessage = "";
|
||||
private final CompletableFuture<CodyAgentServer> initialized = new CompletableFuture<>();
|
||||
private Future<Void> listeningToJsonRpc;
|
||||
|
||||
public CodyAgent(@NotNull Project project) {
|
||||
this.project = project;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static CodyAgentClient getClient(@NotNull Project project) {
|
||||
return project.getService(CodyAgent.class).client;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static CompletableFuture<CodyAgentServer> getInitializedServer(@NotNull Project project) {
|
||||
return project.getService(CodyAgent.class).initialized;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public static boolean isConnected(@NotNull Project project) {
|
||||
CodyAgent agent = project.getService(CodyAgent.class);
|
||||
return agent != null
|
||||
&& agent.initializationErrorMessage.isEmpty()
|
||||
&& agent.listeningToJsonRpc != null
|
||||
&& !agent.listeningToJsonRpc.isDone()
|
||||
&& !agent.listeningToJsonRpc.isCancelled()
|
||||
&& agent.client.server != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static CodyAgentServer getServer(@NotNull Project project) {
|
||||
if (!isConnected(project)) {
|
||||
return null;
|
||||
}
|
||||
return getClient(project).server;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
System.out.println("AGENT ENABLED " + System.getProperty("cody-agent.enabled", "false"));
|
||||
if (!"true".equals(System.getProperty("cody-agent.enabled", "false"))) {
|
||||
logger.info("Cody agent is disabled due to system property '-Dcody-agent.enabled=false'");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
startListeningToAgent();
|
||||
executorService.submit(
|
||||
() -> {
|
||||
try {
|
||||
final CodyAgentServer server = Objects.requireNonNull(client.server);
|
||||
ServerInfo info =
|
||||
server
|
||||
.initialize(
|
||||
new ClientInfo()
|
||||
.setName("JetBrains")
|
||||
.setVersion(ConfigUtil.getPluginVersion())
|
||||
.setWorkspaceRootPath(ConfigUtil.getWorkspaceRoot(project))
|
||||
.setConnectionConfiguration(
|
||||
ConfigUtil.getAgentConfiguration(this.project)))
|
||||
.get();
|
||||
logger.info("connected to Cody agent " + info.name);
|
||||
server.initialized();
|
||||
this.subscribeToFocusEvents();
|
||||
this.initialized.complete(server);
|
||||
} catch (Exception e) {
|
||||
initializationErrorMessage =
|
||||
"failed to send 'initialize' JSON-RPC request Cody agent";
|
||||
logger.error(initializationErrorMessage, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
initializationErrorMessage = "unable to start Cody agent";
|
||||
logger.error(initializationErrorMessage, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void subscribeToFocusEvents() {
|
||||
// Code example taken from
|
||||
// https://intellij-support.jetbrains.com/hc/en-us/community/posts/4578776718354/comments/4594838404882
|
||||
// This listener is registered programmatically because it was not working via plugin.xml
|
||||
// listeners.
|
||||
EditorEventMulticaster multicaster = EditorFactory.getInstance().getEventMulticaster();
|
||||
if (multicaster instanceof EditorEventMulticasterEx) {
|
||||
EditorEventMulticasterEx ex = (EditorEventMulticasterEx) multicaster;
|
||||
ex.addFocusChangeListener(new CodyAgentFocusListener(), this.disposable);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
final CodyAgentServer server = CodyAgent.getServer(project);
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
executorService.submit(() -> server.shutdown().thenAccept((Void) -> server.exit()));
|
||||
}
|
||||
|
||||
private static String binarySuffix() {
|
||||
return SystemInfoRt.isWindows ? ".exe" : "";
|
||||
}
|
||||
|
||||
private static String agentBinaryName() {
|
||||
String os = SystemInfoRt.isMac ? "macos" : SystemInfoRt.isWindows ? "windows" : "linux";
|
||||
@SuppressWarnings("MissingRecentApi")
|
||||
String arch = CpuArch.isArm64() ? "arm64" : "x64";
|
||||
return "agent-" + os + "-" + arch + binarySuffix();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Path agentDirectory() {
|
||||
String fromProperty = System.getProperty("cody-agent.directory", "");
|
||||
if (!fromProperty.isEmpty()) {
|
||||
return Paths.get(fromProperty);
|
||||
}
|
||||
IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PLUGIN_ID);
|
||||
if (plugin == null) {
|
||||
return null;
|
||||
}
|
||||
return plugin.getPluginPath();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static File agentBinary() throws CodyAgentException {
|
||||
Path pluginPath = agentDirectory();
|
||||
if (pluginPath == null) {
|
||||
throw new CodyAgentException("Sourcegraph plugin path not found");
|
||||
}
|
||||
Path binarySource = pluginPath.resolve("agent").resolve(agentBinaryName());
|
||||
if (!Files.isRegularFile(binarySource)) {
|
||||
throw new CodyAgentException(
|
||||
"Cody agent binary not found at path " + binarySource.toAbsolutePath());
|
||||
}
|
||||
try {
|
||||
Path binaryTarget = Files.createTempFile("cody-agent", binarySuffix());
|
||||
logger.info("extracting Cody agent binary to " + binaryTarget.toAbsolutePath());
|
||||
Files.copy(binarySource, binaryTarget, StandardCopyOption.REPLACE_EXISTING);
|
||||
File binary = binaryTarget.toFile();
|
||||
if (binary.setExecutable(true)) {
|
||||
binary.deleteOnExit();
|
||||
return binary;
|
||||
} else {
|
||||
throw new CodyAgentException("failed to make executable " + binary.getAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new CodyAgentException("failed to create agent binary", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PrintWriter traceWriter() {
|
||||
String tracePath = System.getProperty("cody-agent.trace-path", "");
|
||||
if (!tracePath.isEmpty()) {
|
||||
Path trace = Paths.get(tracePath);
|
||||
try {
|
||||
Files.createDirectories(trace.getParent());
|
||||
return new PrintWriter(
|
||||
Files.newOutputStream(
|
||||
trace, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
|
||||
} catch (IOException e) {
|
||||
logger.error("unable to trace JSON-RPC debugging information to path " + tracePath, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void startListeningToAgent() throws IOException, CodyAgentException {
|
||||
File binary = agentBinary();
|
||||
logger.info("starting Cody agent " + binary.getAbsolutePath());
|
||||
Process process =
|
||||
new ProcessBuilder(binary.getAbsolutePath())
|
||||
.redirectError(ProcessBuilder.Redirect.INHERIT)
|
||||
.start();
|
||||
Launcher<CodyAgentServer> launcher =
|
||||
new Launcher.Builder<CodyAgentServer>()
|
||||
.setRemoteInterface(CodyAgentServer.class)
|
||||
.traceMessages(traceWriter())
|
||||
.setExecutorService(executorService)
|
||||
.setInput(process.getInputStream())
|
||||
.setOutput(process.getOutputStream())
|
||||
.setLocalService(client)
|
||||
.create();
|
||||
client.server = launcher.getRemoteProxy();
|
||||
client.documents = new CodyAgentDocuments(client.server);
|
||||
this.listeningToJsonRpc = launcher.startListening();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.sourcegraph.cody.agent.protocol.ChatMessage;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/** Implementation of the client part of the Cody agent protocol. */
|
||||
@SuppressWarnings("unused")
|
||||
public class CodyAgentClient {
|
||||
|
||||
@Nullable public CodyAgentServer server;
|
||||
@Nullable public CodyAgentDocuments documents;
|
||||
// Callback that is invoked when the agent sends a "chat/updateMessageInProgress" notification.
|
||||
@Nullable public Consumer<ChatMessage> onChatUpdateMessageInProgress;
|
||||
@Nullable public Editor editor;
|
||||
|
||||
/**
|
||||
* Helper to run client request/notification handlers on the IntelliJ event thread. Use this
|
||||
* helper for handlers that require access to the IntelliJ editor, for example to read the text
|
||||
* contents of the open editor.
|
||||
*/
|
||||
private <T> CompletableFuture<T> onEventThread(Supplier<T> handler) {
|
||||
CompletableFuture<T> result = new CompletableFuture<>();
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
result.complete(handler.get());
|
||||
} catch (Exception e) {
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============
|
||||
// Notifications
|
||||
// =============
|
||||
|
||||
@JsonNotification("chat/updateMessageInProgress")
|
||||
public void chatUpdateMessageInProgress(ChatMessage params) {
|
||||
if (onChatUpdateMessageInProgress != null && params != null) {
|
||||
ApplicationManager.getApplication()
|
||||
.invokeLater(() -> onChatUpdateMessageInProgress.accept(params));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import com.sourcegraph.cody.agent.protocol.TextDocument;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// Work-in-progress implementation of a helper class to optimize the notification traffic for
|
||||
// textDocument/* methods. For example, we don't need to include the content of the document
|
||||
// when we move the cursor around, or we don't need to send repeated didFocus events for the
|
||||
// same file path. Currently, we send duplicate didFocus events when the user focuses on
|
||||
// another application than IntelliJ, and then focuses back on the original document.
|
||||
public class CodyAgentDocuments {
|
||||
private final CodyAgentServer underlying;
|
||||
private String focusedPath = null;
|
||||
private Map<String, TextDocument> documents = new HashMap<>();
|
||||
|
||||
public CodyAgentDocuments(CodyAgentServer underlying) {
|
||||
this.underlying = underlying;
|
||||
}
|
||||
|
||||
private void handleDocument(TextDocument document) {
|
||||
TextDocument old = this.documents.get(document.filePath);
|
||||
if (old == null) {
|
||||
this.documents.put(document.filePath, document);
|
||||
return;
|
||||
}
|
||||
if (document.content == null) {
|
||||
document.content = old.content;
|
||||
}
|
||||
if (document.selection == null) {
|
||||
document.selection = old.selection;
|
||||
}
|
||||
this.documents.put(document.filePath, document);
|
||||
}
|
||||
|
||||
public void didOpen(TextDocument document) {
|
||||
this.documents.put(document.filePath, document);
|
||||
underlying.textDocumentDidOpen(document);
|
||||
}
|
||||
|
||||
public void didFocus(TextDocument document) {
|
||||
this.documents.put(document.filePath, document);
|
||||
}
|
||||
|
||||
public void didChange(TextDocument document) {}
|
||||
|
||||
public void didClose(TextDocument document) {}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CodyAgentException extends Exception {
|
||||
public CodyAgentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CodyAgentException(String message, IOException e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable fillInStackTrace() {
|
||||
// don't fill in stack trace
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import com.sourcegraph.cody.agent.protocol.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification;
|
||||
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
|
||||
|
||||
/**
|
||||
* Interface for the server-part of the Cody agent protocol. The implementation of this interface is
|
||||
* written in TypeScript in the file "client/cody-agent/src/agent.ts". The Eclipse LSP4J bindings
|
||||
* create a Java implementation of this interface by using a JVM-reflection feature called "Proxy",
|
||||
* which works similar to JavaScript Proxy.
|
||||
*/
|
||||
public interface CodyAgentServer {
|
||||
|
||||
// Requests
|
||||
@JsonRequest("initialize")
|
||||
CompletableFuture<ServerInfo> initialize(ClientInfo clientInfo);
|
||||
|
||||
@JsonRequest("shutdown")
|
||||
CompletableFuture<Void> shutdown();
|
||||
|
||||
@JsonRequest("recipes/list")
|
||||
CompletableFuture<List<RecipeInfo>> recipesList();
|
||||
|
||||
@JsonRequest("recipes/execute")
|
||||
CompletableFuture<Void> recipesExecute(ExecuteRecipeParams params);
|
||||
|
||||
// Notifications
|
||||
@JsonNotification("initialized")
|
||||
void initialized();
|
||||
|
||||
@JsonNotification("exit")
|
||||
void exit();
|
||||
|
||||
@JsonNotification("connectionConfiguration/didChange")
|
||||
void configurationDidChange(ConnectionConfiguration document);
|
||||
|
||||
@JsonNotification("textDocument/didFocus")
|
||||
void textDocumentDidFocus(TextDocument document);
|
||||
|
||||
@JsonNotification("textDocument/didOpen")
|
||||
void textDocumentDidOpen(TextDocument document);
|
||||
|
||||
@JsonNotification("textDocument/didChange")
|
||||
void textDocumentDidChange(TextDocument document);
|
||||
|
||||
@JsonNotification("textDocument/didClose")
|
||||
void textDocumentDidClose(TextDocument document);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.sourcegraph.cody.agent;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ConnectionConfiguration {
|
||||
|
||||
public String serverEndpoint;
|
||||
public String accessToken;
|
||||
public Map<String, String> customHeaders;
|
||||
|
||||
public ConnectionConfiguration setServerEndpoint(String serverEndpoint) {
|
||||
this.serverEndpoint = serverEndpoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConnectionConfiguration setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConnectionConfiguration setCustomHeaders(Map<String, String> customHeaders) {
|
||||
this.customHeaders = customHeaders;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// TODO: consolidate with the other ChatMessage. This duplication exists because the other
|
||||
// ChatMessage uses an enum for Message.speaker that doesn't decode nicely with Gson.
|
||||
public class ChatMessage extends Message {
|
||||
public String displayText;
|
||||
public List<ContextFile> contextFiles;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChatMessage{"
|
||||
+ "displayText='"
|
||||
+ displayText
|
||||
+ '\''
|
||||
+ ", contextFiles="
|
||||
+ contextFiles
|
||||
+ ", speaker='"
|
||||
+ speaker
|
||||
+ '\''
|
||||
+ ", text='"
|
||||
+ text
|
||||
+ '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
import com.sourcegraph.cody.agent.ConnectionConfiguration;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ClientInfo {
|
||||
|
||||
public String name;
|
||||
public String version;
|
||||
public String workspaceRootPath;
|
||||
@Nullable public ConnectionConfiguration connectionConfiguration;
|
||||
|
||||
public ClientInfo setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientInfo setVersion(String version) {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientInfo setWorkspaceRootPath(String workspaceRootPath) {
|
||||
this.workspaceRootPath = workspaceRootPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientInfo setConnectionConfiguration(ConnectionConfiguration connectionConfiguration) {
|
||||
this.connectionConfiguration = connectionConfiguration;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class ContextFile {
|
||||
public String fileName;
|
||||
public String repoName;
|
||||
public String revision;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class ContextMessage {
|
||||
public ContextFile file;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class ExecuteRecipeParams {
|
||||
|
||||
public String id;
|
||||
public String humanChatInput;
|
||||
|
||||
public ExecuteRecipeParams setId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExecuteRecipeParams setHumanChatInput(String humanChatInput) {
|
||||
this.humanChatInput = humanChatInput;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class Message {
|
||||
public String speaker;
|
||||
public String text;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Message{" + "speaker='" + speaker + '\'' + ", text='" + text + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class Position {
|
||||
public int line;
|
||||
public int character;
|
||||
|
||||
public Position setLine(int line) {
|
||||
this.line = line;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Position setCharacter(int character) {
|
||||
this.character = character;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class Range {
|
||||
public Position start;
|
||||
public Position end;
|
||||
|
||||
public Range setStart(Position start) {
|
||||
this.start = start;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Range setEnd(Position end) {
|
||||
this.end = end;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class RecipeInfo {
|
||||
public String id;
|
||||
public String title;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RecipeInfo{" + "id='" + id + '\'' + ", title='" + title + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
public class ServerInfo {
|
||||
public final String name;
|
||||
|
||||
public ServerInfo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class TextDocument {
|
||||
|
||||
public String filePath;
|
||||
@Nullable public String content;
|
||||
@Nullable public Range selection;
|
||||
|
||||
public TextDocument setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextDocument setContent(String content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TextDocument setSelection(Range selection) {
|
||||
this.selection = selection;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TextDocument{"
|
||||
+ "filePath='"
|
||||
+ filePath
|
||||
+ '\''
|
||||
+ ", content='"
|
||||
+ content
|
||||
+ '\''
|
||||
+ ", selection="
|
||||
+ selection
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Handwritten classes for the agent JSON-RPC protocol.
|
||||
*
|
||||
* <p>In the future, these classes should ideally be replaced with automatically generated classes.
|
||||
*/
|
||||
package com.sourcegraph.cody.agent.protocol;
|
||||
@ -1,11 +1,17 @@
|
||||
package com.sourcegraph.cody.chat;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.sourcegraph.cody.UpdatableChat;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import com.sourcegraph.cody.agent.protocol.ExecuteRecipeParams;
|
||||
import com.sourcegraph.cody.api.*;
|
||||
import com.sourcegraph.cody.prompts.Preamble;
|
||||
import com.sourcegraph.cody.vscode.CancellationToken;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -19,10 +25,22 @@ public class Chat {
|
||||
}
|
||||
|
||||
public void sendMessage(
|
||||
@NotNull ChatMessage humanMessage, @Nullable String prefix, @NotNull UpdatableChat chat) {
|
||||
List<Message> preamble = Preamble.getPreamble(codebase);
|
||||
@NotNull Project project,
|
||||
@NotNull ChatMessage humanMessage,
|
||||
@Nullable String prefix,
|
||||
@NotNull UpdatableChat chat)
|
||||
throws ExecutionException, InterruptedException {
|
||||
if (CodyAgent.isConnected(project)) {
|
||||
sendMessageViaAgent(project, humanMessage, chat);
|
||||
} else {
|
||||
sendMessageWithoutAgent(humanMessage, prefix, chat);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use the context getting logic from VS Code
|
||||
private void sendMessageWithoutAgent(
|
||||
@NotNull ChatMessage humanMessage, @Nullable String prefix, @NotNull UpdatableChat chat) {
|
||||
// TODO: Usethe context getting logic from VS Code
|
||||
List<Message> preamble = Preamble.getPreamble(codebase);
|
||||
var codeContext = "";
|
||||
if (humanMessage.getContextFiles().size() == 0) {
|
||||
codeContext = "I have no file open in the editor right now.";
|
||||
@ -41,11 +59,46 @@ public class Chat {
|
||||
|
||||
// ConfigUtil.getAccessToken(project) TODO: Get the access token from the plugin config
|
||||
// TODO: Don't create this each time
|
||||
|
||||
completionsService.streamCompletion(
|
||||
input,
|
||||
new ChatUpdaterCallbacks(chat, prefix),
|
||||
CompletionsService.Endpoint.Stream,
|
||||
new CancellationToken());
|
||||
}
|
||||
|
||||
private void sendMessageViaAgent(
|
||||
@NotNull Project project, @NotNull ChatMessage humanMessage, @NotNull UpdatableChat chat)
|
||||
throws ExecutionException, InterruptedException {
|
||||
final AtomicBoolean isFirstMessage = new AtomicBoolean(false);
|
||||
CodyAgent.getClient(project).onChatUpdateMessageInProgress =
|
||||
(agentChatMessage) -> {
|
||||
if (agentChatMessage.text == null) {
|
||||
return;
|
||||
}
|
||||
ChatMessage chatMessage =
|
||||
new ChatMessage(
|
||||
Speaker.ASSISTANT,
|
||||
agentChatMessage.text,
|
||||
agentChatMessage.displayText,
|
||||
agentChatMessage.contextFiles.stream()
|
||||
.map(file -> file.fileName)
|
||||
.collect(Collectors.toList()));
|
||||
if (isFirstMessage.compareAndSet(false, true)) {
|
||||
chat.addMessageToChat(chatMessage);
|
||||
} else {
|
||||
chat.updateLastMessage(chatMessage);
|
||||
}
|
||||
};
|
||||
|
||||
CodyAgent.getInitializedServer(project)
|
||||
.thenAcceptAsync(
|
||||
server ->
|
||||
server.recipesExecute(
|
||||
new ExecuteRecipeParams()
|
||||
.setId("chat-question")
|
||||
.setHumanChatInput(humanMessage.getText())),
|
||||
CodyAgent.executorService)
|
||||
.get();
|
||||
chat.finishMessageProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ public class ChatMessage extends Message {
|
||||
private final @NotNull String displayText;
|
||||
private final @NotNull List<String> contextFiles;
|
||||
|
||||
private ChatMessage(
|
||||
ChatMessage(
|
||||
@NotNull Speaker speaker,
|
||||
@NotNull String text,
|
||||
@NotNull String displayText,
|
||||
|
||||
@ -3,17 +3,29 @@ package com.sourcegraph.cody.completions;
|
||||
import com.intellij.openapi.Disposable;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.command.CommandProcessor;
|
||||
import com.intellij.openapi.editor.Caret;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.SelectionModel;
|
||||
import com.intellij.openapi.editor.VisualPosition;
|
||||
import com.intellij.openapi.editor.event.*;
|
||||
import com.intellij.openapi.editor.ex.util.EditorUtil;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.fileEditor.FileEditor;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileEditor.TextEditor;
|
||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Disposer;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import com.sourcegraph.cody.agent.CodyAgentServer;
|
||||
import com.sourcegraph.cody.agent.protocol.Position;
|
||||
import com.sourcegraph.cody.agent.protocol.Range;
|
||||
import com.sourcegraph.cody.agent.protocol.TextDocument;
|
||||
import com.sourcegraph.cody.vscode.InlineCompletionTriggerKind;
|
||||
import java.util.List;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Determines when to trigger completions and when to clear completions.
|
||||
@ -29,6 +41,7 @@ public class CodyEditorFactoryListener implements EditorFactoryListener {
|
||||
@Override
|
||||
public void editorCreated(@NotNull EditorFactoryEvent event) {
|
||||
Editor editor = event.getEditor();
|
||||
informAgentAboutEditorChange(editor);
|
||||
Project project = editor.getProject();
|
||||
if (project == null || project.isDisposed()) {
|
||||
return;
|
||||
@ -44,6 +57,7 @@ public class CodyEditorFactoryListener implements EditorFactoryListener {
|
||||
|
||||
@Override
|
||||
public void caretPositionChanged(@NotNull CaretEvent e) {
|
||||
informAgentAboutEditorChange(e.getEditor());
|
||||
CodyCompletionsManager suggestions = CodyCompletionsManager.getInstance();
|
||||
if (suggestions.isEnabledForEditor(e.getEditor())
|
||||
&& CodyEditorFactoryListener.isSelectedEditor(e.getEditor())) {
|
||||
@ -58,6 +72,7 @@ public class CodyEditorFactoryListener implements EditorFactoryListener {
|
||||
public void selectionChanged(@NotNull SelectionEvent e) {
|
||||
if (CodyCompletionsManager.getInstance().isEnabledForEditor(e.getEditor())
|
||||
&& CodyEditorFactoryListener.isSelectedEditor(e.getEditor())) {
|
||||
informAgentAboutEditorChange(e.getEditor());
|
||||
ApplicationManager.getApplication()
|
||||
.getService(CodyCompletionsManager.class)
|
||||
.clearCompletions(e.getEditor());
|
||||
@ -80,6 +95,7 @@ public class CodyEditorFactoryListener implements EditorFactoryListener {
|
||||
completions.clearCompletions(this.editor);
|
||||
if (completions.isEnabledForEditor(this.editor)
|
||||
&& !CommandProcessor.getInstance().isUndoTransparentActionInProgress()) {
|
||||
informAgentAboutEditorChange(this.editor);
|
||||
int changeOffset = event.getOffset() + event.getNewLength();
|
||||
if (this.editor.getCaretModel().getOffset() == changeOffset) {
|
||||
InlineCompletionTriggerKind requestType =
|
||||
@ -115,4 +131,57 @@ public class CodyEditorFactoryListener implements EditorFactoryListener {
|
||||
FileEditor current = editorManager.getSelectedEditor();
|
||||
return current instanceof TextEditor && editor.equals(((TextEditor) current).getEditor());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Range getSelection(Editor editor) {
|
||||
SelectionModel selectionModel = editor.getSelectionModel();
|
||||
VisualPosition selectionStartPosition = selectionModel.getSelectionStartPosition();
|
||||
VisualPosition selectionEndPosition = selectionModel.getSelectionEndPosition();
|
||||
if (selectionStartPosition != null && selectionEndPosition != null) {
|
||||
return new Range()
|
||||
.setStart(
|
||||
new Position()
|
||||
.setLine(selectionStartPosition.line)
|
||||
.setCharacter(selectionStartPosition.column))
|
||||
.setEnd(
|
||||
new Position()
|
||||
.setLine(selectionEndPosition.line)
|
||||
.setCharacter(selectionEndPosition.column));
|
||||
}
|
||||
List<Caret> carets = editor.getCaretModel().getAllCarets();
|
||||
if (!carets.isEmpty()) {
|
||||
Caret caret = carets.get(0);
|
||||
Position position =
|
||||
new Position()
|
||||
.setLine(caret.getLogicalPosition().line)
|
||||
.setCharacter(caret.getLogicalPosition().column);
|
||||
// A single-offset caret is a selection where end == start.
|
||||
return new Range().setStart(position).setEnd(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sends a textDocument/didChange notification to the agent server.
|
||||
public static void informAgentAboutEditorChange(@Nullable Editor editor) {
|
||||
if (editor == null) {
|
||||
return;
|
||||
}
|
||||
if (editor.getProject() == null) {
|
||||
return;
|
||||
}
|
||||
CodyAgentServer server = CodyAgent.getServer(editor.getProject());
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
TextDocument document =
|
||||
new TextDocument()
|
||||
.setFilePath(file.getPath())
|
||||
.setContent(editor.getDocument().getText())
|
||||
.setSelection(getSelection(editor));
|
||||
server.textDocumentDidChange(document);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@ import com.intellij.ide.plugins.IdeaPluginDescriptor;
|
||||
import com.intellij.ide.plugins.PluginManagerCore;
|
||||
import com.intellij.openapi.extensions.PluginId;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.sourcegraph.cody.agent.ConnectionConfiguration;
|
||||
import com.sourcegraph.find.Search;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -14,15 +17,19 @@ import org.jetbrains.annotations.Nullable;
|
||||
public class ConfigUtil {
|
||||
public static final String DOTCOM_URL = "https://sourcegraph.com/";
|
||||
|
||||
@NotNull
|
||||
public static ConnectionConfiguration getAgentConfiguration(@NotNull Project project) {
|
||||
return new ConnectionConfiguration()
|
||||
.setServerEndpoint(getSourcegraphUrl(project))
|
||||
.setAccessToken(getProjectAccessToken(project))
|
||||
.setCustomHeaders(getCustomRequestHeadersAsMap(project));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static JsonObject getConfigAsJson(@NotNull Project project) {
|
||||
JsonObject configAsJson = new JsonObject();
|
||||
configAsJson.addProperty("instanceURL", ConfigUtil.getSourcegraphUrl(project));
|
||||
configAsJson.addProperty(
|
||||
"accessToken",
|
||||
ConfigUtil.getInstanceType(project) == SettingsComponent.InstanceType.ENTERPRISE
|
||||
? ConfigUtil.getDotComAccessToken(project)
|
||||
: ConfigUtil.getEnterpriseAccessToken(project));
|
||||
configAsJson.addProperty("accessToken", ConfigUtil.getProjectAccessToken(project));
|
||||
configAsJson.addProperty(
|
||||
"customRequestHeadersAsString", ConfigUtil.getCustomRequestHeaders(project));
|
||||
configAsJson.addProperty("pluginVersion", ConfigUtil.getPluginVersion());
|
||||
@ -84,6 +91,15 @@ public class ConfigUtil {
|
||||
return !userLevelUrl.equals("") ? addSlashIfNeeded(userLevelUrl) : "";
|
||||
}
|
||||
|
||||
public static Map<String, String> getCustomRequestHeadersAsMap(@NotNull Project project) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
String[] pairs = getCustomRequestHeaders(project).split(",");
|
||||
for (int i = 0; i + 1 < pairs.length; i = i + 2) {
|
||||
result.put(pairs[i], pairs[i + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getCustomRequestHeaders(@NotNull Project project) {
|
||||
// Project level
|
||||
@ -242,6 +258,23 @@ public class ConfigUtil {
|
||||
return Objects.requireNonNull(SourcegraphService.getInstance(project));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getWorkspaceRoot(@NotNull Project project) {
|
||||
if (project.getBasePath() != null) {
|
||||
return project.getBasePath();
|
||||
}
|
||||
// The base path should only be null for the default project. The agent server assumes that the
|
||||
// workspace root is not null, so we have to provide some default value. Feel free to change to
|
||||
// something else than the home directory if this is causing problems.
|
||||
return System.getProperty("user.home");
|
||||
}
|
||||
|
||||
public static String getProjectAccessToken(Project project) {
|
||||
return ConfigUtil.getInstanceType(project) == SettingsComponent.InstanceType.ENTERPRISE
|
||||
? ConfigUtil.getEnterpriseAccessToken(project)
|
||||
: ConfigUtil.getProjectAccessToken(project);
|
||||
}
|
||||
|
||||
public static String getEnterpriseAccessToken(Project project) {
|
||||
// Project level → application level
|
||||
String projectLevelAccessToken = getProjectLevelConfig(project).getEnterpriseAccessToken();
|
||||
|
||||
@ -15,6 +15,8 @@ import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.project.ProjectManager;
|
||||
import com.intellij.util.messages.MessageBus;
|
||||
import com.intellij.util.messages.MessageBusConnection;
|
||||
import com.sourcegraph.cody.agent.CodyAgent;
|
||||
import com.sourcegraph.cody.agent.CodyAgentServer;
|
||||
import com.sourcegraph.cody.completions.CodyCompletionsManager;
|
||||
import com.sourcegraph.find.browser.JavaToJSBridge;
|
||||
import com.sourcegraph.telemetry.GraphQlLogger;
|
||||
@ -55,6 +57,12 @@ public class SettingsChangeListener implements Disposable {
|
||||
javaToJSBridge.callJS("pluginSettingsChanged", ConfigUtil.getConfigAsJson(project));
|
||||
}
|
||||
|
||||
// Notify Cody Agent about config changes.
|
||||
CodyAgentServer agentServer = CodyAgent.getServer(project);
|
||||
if (agentServer != null) {
|
||||
agentServer.configurationDidChange(ConfigUtil.getAgentConfiguration(project));
|
||||
}
|
||||
|
||||
// Log install events
|
||||
if (!Objects.equals(context.oldUrl, context.newUrl)) {
|
||||
GraphQlLogger.logInstallEvent(project, ConfigUtil::setInstallEventLogged);
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
secondary="false"
|
||||
factoryClass="com.sourcegraph.cody.CodyToolWindowFactory"/>
|
||||
<notificationGroup id="Cody Sourcegraph access" displayType="BALLOON"/>
|
||||
<projectService serviceImplementation="com.sourcegraph.cody.agent.CodyAgent"/>
|
||||
|
||||
|
||||
<!-- Code completions -->
|
||||
@ -56,6 +57,14 @@
|
||||
|
||||
</extensions>
|
||||
|
||||
<applicationListeners>
|
||||
<listener class="com.sourcegraph.cody.CodyAgentProjectListener" topic="com.intellij.openapi.project.ProjectManagerListener"/>
|
||||
<listener class="com.sourcegraph.cody.CodyFileEditorListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
|
||||
<!-- CodyAgentFocusListener is commented out since it doesn't seem possible to register a listener via plugin.xml.
|
||||
We programmatically register a listener from CodyAgent instead. -->
|
||||
<!-- <listener class="com.sourcegraph.cody.CodyAgentFocusListener" topic="com.intellij.openapi.editor.ex.FocusChangeListener"/> -->
|
||||
</applicationListeners>
|
||||
|
||||
<actions>
|
||||
<action
|
||||
id="sourcegraph.openFile"
|
||||
|
||||
149
pnpm-lock.yaml
149
pnpm-lock.yaml
@ -1,5 +1,9 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
browserify-zlib: 0.2.0
|
||||
'@types/webpack': '5'
|
||||
@ -1435,6 +1439,19 @@ importers:
|
||||
specifier: ^0.16.0
|
||||
version: 0.16.0(vite@4.1.4)
|
||||
|
||||
client/cody-agent:
|
||||
dependencies:
|
||||
'@sourcegraph/cody-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../cody-shared
|
||||
'@sourcegraph/common':
|
||||
specifier: workspace:*
|
||||
version: link:../common
|
||||
devDependencies:
|
||||
pkg:
|
||||
specifier: ^5.8.1
|
||||
version: 5.8.1
|
||||
|
||||
client/cody-cli:
|
||||
dependencies:
|
||||
'@sourcegraph/cody-shared':
|
||||
@ -2123,6 +2140,15 @@ packages:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/@babel/generator@7.18.2:
|
||||
resolution: {integrity: sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.5
|
||||
'@jridgewell/gen-mapping': 0.3.3
|
||||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/generator@7.22.5:
|
||||
resolution: {integrity: sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -2482,6 +2508,14 @@ packages:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.18.4:
|
||||
resolution: {integrity: sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.22.5:
|
||||
resolution: {integrity: sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -4987,6 +5021,15 @@ packages:
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/types@7.19.0:
|
||||
resolution: {integrity: sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.22.5
|
||||
'@babel/helper-validator-identifier': 7.22.5
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/types@7.22.5:
|
||||
resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -10187,7 +10230,7 @@ packages:
|
||||
file-system-cache: 1.0.5
|
||||
find-up: 5.0.0
|
||||
fork-ts-checker-webpack-plugin: 6.5.2(eslint@8.34.0)(typescript@5.0.2)(webpack@5.75.0)
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
glob: 7.2.3
|
||||
handlebars: 4.7.7
|
||||
interpret: 2.2.0
|
||||
@ -10263,7 +10306,7 @@ packages:
|
||||
cpy: 8.1.2
|
||||
detect-port: 1.3.0
|
||||
express: 4.18.2
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
global: 4.4.0
|
||||
globby: 11.1.0
|
||||
ip: 2.0.0
|
||||
@ -10358,7 +10401,7 @@ packages:
|
||||
'@storybook/csf': 0.0.2--canary.4566f4d.1
|
||||
'@storybook/mdx1-csf': 0.0.1(@babel/core@7.22.1)
|
||||
core-js: 3.22.8
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
global: 4.4.0
|
||||
regenerator-runtime: 0.13.11
|
||||
ts-dedent: 2.0.0
|
||||
@ -10416,7 +10459,7 @@ packages:
|
||||
express: 4.18.2
|
||||
file-loader: 6.2.0(webpack@5.75.0)
|
||||
find-up: 5.0.0
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
html-webpack-plugin: 4.5.2(esbuild@0.17.14)(webpack-cli@5.0.1)(webpack@5.75.0)
|
||||
node-fetch: 2.6.11
|
||||
pnp-webpack-plugin: 1.6.4(typescript@5.0.2)
|
||||
@ -10474,7 +10517,7 @@ packages:
|
||||
css-loader: 5.2.6(webpack@5.75.0)
|
||||
express: 4.18.2
|
||||
find-up: 5.0.0
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
html-webpack-plugin: 5.5.0(webpack@5.75.0)
|
||||
node-fetch: 2.6.11
|
||||
process: 0.11.10
|
||||
@ -10655,7 +10698,7 @@ packages:
|
||||
babel-plugin-react-docgen: 4.2.1
|
||||
core-js: 3.22.8
|
||||
escodegen: 2.0.0
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
global: 4.4.0
|
||||
html-tags: 3.1.0
|
||||
lodash: 4.17.21
|
||||
@ -10767,7 +10810,7 @@ packages:
|
||||
core-js: 3.22.8
|
||||
detect-package-manager: 2.0.1
|
||||
fetch-retry: 5.0.2
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
global: 4.4.0
|
||||
isomorphic-unfetch: 3.1.0
|
||||
nanoid: 3.3.6
|
||||
@ -19094,7 +19137,7 @@ packages:
|
||||
cosmiconfig: 6.0.0
|
||||
deepmerge: 4.2.2
|
||||
eslint: 8.34.0
|
||||
fs-extra: 9.0.1
|
||||
fs-extra: 9.1.0
|
||||
glob: 7.2.3
|
||||
memfs: 3.4.12
|
||||
minimatch: 3.1.2
|
||||
@ -19172,6 +19215,13 @@ packages:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
/from2@2.3.0:
|
||||
resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
readable-stream: 2.3.8
|
||||
dev: true
|
||||
|
||||
/fromentries@1.3.2:
|
||||
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
|
||||
dev: true
|
||||
@ -19212,14 +19262,14 @@ packages:
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
|
||||
/fs-extra@9.0.1:
|
||||
resolution: {integrity: sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==}
|
||||
/fs-extra@9.1.0:
|
||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
at-least-node: 1.0.0
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.0.1
|
||||
universalify: 1.0.0
|
||||
universalify: 2.0.0
|
||||
|
||||
/fs-minipass@2.1.0:
|
||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||
@ -20934,6 +20984,14 @@ packages:
|
||||
intl-messageformat-parser: 1.8.1
|
||||
dev: true
|
||||
|
||||
/into-stream@6.0.0:
|
||||
resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
from2: 2.3.0
|
||||
p-is-promise: 3.0.0
|
||||
dev: true
|
||||
|
||||
/invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
dependencies:
|
||||
@ -21102,6 +21160,12 @@ packages:
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
|
||||
/is-core-module@2.9.0:
|
||||
resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==}
|
||||
dependencies:
|
||||
has: 1.0.3
|
||||
dev: true
|
||||
|
||||
/is-data-descriptor@0.1.4:
|
||||
resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -25406,6 +25470,13 @@ packages:
|
||||
dns-packet: 5.4.0
|
||||
thunky: 1.1.0
|
||||
|
||||
/multistream@4.1.0:
|
||||
resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/mute-stdout@1.0.1:
|
||||
resolution: {integrity: sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -26244,6 +26315,11 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/p-is-promise@3.0.0:
|
||||
resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/p-limit@1.3.0:
|
||||
resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==}
|
||||
engines: {node: '>=4'}
|
||||
@ -26721,6 +26797,23 @@ packages:
|
||||
dependencies:
|
||||
find-up: 5.0.0
|
||||
|
||||
/pkg-fetch@3.4.2:
|
||||
resolution: {integrity: sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
fs-extra: 9.1.0
|
||||
https-proxy-agent: 5.0.1
|
||||
node-fetch: 2.6.11
|
||||
progress: 2.0.3
|
||||
semver: 7.5.1
|
||||
tar-fs: 2.1.1
|
||||
yargs: 16.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/pkg-types@1.0.3:
|
||||
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
||||
dependencies:
|
||||
@ -26728,6 +26821,34 @@ packages:
|
||||
mlly: 1.3.0
|
||||
pathe: 1.1.1
|
||||
|
||||
/pkg@5.8.1:
|
||||
resolution: {integrity: sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
node-notifier: '>=9.0.1'
|
||||
peerDependenciesMeta:
|
||||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/generator': 7.18.2
|
||||
'@babel/parser': 7.18.4
|
||||
'@babel/types': 7.19.0
|
||||
chalk: 4.1.2
|
||||
fs-extra: 9.1.0
|
||||
globby: 11.1.0
|
||||
into-stream: 6.0.0
|
||||
is-core-module: 2.9.0
|
||||
minimist: 1.2.8
|
||||
multistream: 4.1.0
|
||||
pkg-fetch: 3.4.2
|
||||
prebuild-install: 7.1.1
|
||||
resolve: 1.22.1
|
||||
stream-meter: 1.0.4
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/playwright-core@1.25.0:
|
||||
resolution: {integrity: sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q==}
|
||||
engines: {node: '>=14'}
|
||||
@ -30493,6 +30614,12 @@ packages:
|
||||
stream-chain: 2.2.5
|
||||
dev: false
|
||||
|
||||
/stream-meter@1.0.4:
|
||||
resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==}
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
dev: true
|
||||
|
||||
/stream-shift@1.0.0:
|
||||
resolution: {integrity: sha512-Afuc4BKirbx0fwm9bKOehZPG01DJkm/4qbklw4lo9nMPqd2x0kZTLcgwQUXdGiPPY489l3w8cQ5xEEAGbg8ACQ==}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user