diff options
-rw-r--r-- | CONTRIBUTING.md | 94 | ||||
-rw-r--r-- | continuedev/src/continuedev/core/main.py | 6 | ||||
-rw-r--r-- | extension/react-app/src/App.tsx | 8 | ||||
-rw-r--r-- | extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts | 35 | ||||
-rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 93 | ||||
-rw-r--r-- | extension/react-app/src/hooks/useContinueGUIProtocol.ts | 95 | ||||
-rw-r--r-- | extension/react-app/src/hooks/useWebsocket.ts | 2 | ||||
-rw-r--r-- | extension/src/activation/activate.ts | 14 | ||||
-rw-r--r-- | extension/src/continueIdeClient.ts | 10 |
9 files changed, 222 insertions, 135 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7e49dc2d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing to Continue + +## Table of Contents + +- [Continue Architecture](#continue-architecture) +- [Core Concepts](#core-concepts) + - [Step](#step) +- [Continue VS Code Client](#continue-vs-code-client) +- [Continue IDE Websockets Protocol](#continue-ide-websockets-protocol) +- [Continue GUI Websockets Protocol](#continue-gui-websockets-protocol) +- [Ways to Contribute](#ways-to-contribute) + - [Report Bugs](#report-bugs) + - [Suggest Enhancements](#suggest-enhancements) + - [Updating / Improving Documentation](#updating--improving-documentation) + +## Continue Architecture + +Continue consists of 3 components, designed so that Continue can easily be extended to work in any IDE: + +1. **Continue Server** - The Continue Server is responsible for keeping state, running the autopilot loop which takes actions, and communicating between the IDE and GUI. + +2. **Continue IDE Client** - The Continue IDE Client is a plugin for the IDE which implements the Continue IDE Protocol. This allows the server to request actions to be taken within the IDE, for example if `sdk.ide.setFileOpen("main.py")` is called on the server, it will communicate over websocketes with the IDE, which will open the file `main.py`. The first IDE Client we have built is for VS Code, but we plan to build clients for other IDEs in the future. The IDE Client must 1. implement the websockets protocol, as is done [here](./extension/src/continueIdeClient.ts) for VS Code and 2. launch the Continue Server, like [here](./extension/src/activation/environmentSetup.ts), and 3. display the Continue GUI in a sidebar, like [here](./extension/src/debugPanel.ts). + +3. **Continue GUI** - The Continue GUI is a React application that gives the user control over Continue. It displays the history of Steps, shows what context is included in the current Step, and lets the users enter natural language or slash commands to initiate new Steps. The GUI communicates with the Continue Server over its own websocket connection + +It is important that the IDE Client and GUI never communicate except when the IDE Client initially sets up the GUI. This ensures that the server is the source-of-truth for state, and that we can easily extend Continue to work in other IDEs. + +![Continue Architecture](https://continue.dev/docs/assets/images/continue-architecture-146a90742e25f6524452c74fe44fa2a0.png) + +## Core Concepts + +All of Continue's logic happens inside of the server, and it is built around a few core concepts. Most of these are Pydantic Models defined in [core/main.py](./continuedev/src/continuedev/core/main.py). + +### `Step` + +Everything in Continue is a "Step". The `Step` class defines 2 methods: + +1. `async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]` - This method defines what happens when the Step is run. It has access to the Continue SDK, which lets you take actions in the IDE, call LLMs, run nested Steps, and more. Optionally, a Step can return an `Observation` object, which a `Policy` can use to make decisions about what to do next. + +2. `async def describe(self, models: Models) -> Coroutine[str, None, None]` - After each Step is run, this method is called to asynchronously generate a summary title for the step. A `Models` object is passed so that you have access to LLMs to summarize for you. + +Steps are designed to be composable, so that you can easily build new Steps by combining existing ones. And because they are Pydantic models, they can instantly be used as tools useable by an LLM, for example with OpenAI's function-calling functionality (see [ChatWithFunctions](./continuedev/src/continuedev/steps/chat.py) for an example of this). + +Some of the most commonly used Steps are: + +- [`SimpleChatStep`](./continuedev/src/continuedev/steps/chat.py) - This is the default Step that is run when the user enters natural language input. It takes the user's input and runs it through the default LLM, then displays the result in the GUI. + +- [`EditHighlightedCodeStep`](./continuedev/src/continuedev/steps/core/core.py) - This is the Step run when a user highlights code, enters natural language, and presses CMD/CTRL+ENTER, or uses the slash command '/edit'. It opens a side-by-side diff editor, where updated code is streamed to fulfil the user's request. + +### `Autopilot` + +### `Observation` + +### `Policy` + +### Continue VS Code Client + +The starting point for the VS Code extension is [activate.ts](./extension/src/activation/activate.ts). The `activateExtension` function here will: + +1. Check whether the current version of the extension is up-to-date and, if not, display a notification + +2. Initialize the Continue IDE Client and establish a connection with the Continue Server + +3. Load the Continue GUI in the sidebar of the IDE and begin a new session + +### Continue IDE Websockets Protocol + +On the IDE side, this is implemented in [continueIdeClient.ts](./extension/src/continueIdeClient.ts). On the server side, this is implemented in [ide.py](./continuedev/src/continuedev/server/ide.py). You can see [ide_protocol.py](./continuedev/src/continuedev/server/ide_protocol.py) for the protocol definition. + +### Continue GUI Websockets Protocol + +On the GUI side, this is implemented in [ContinueGUIClientProtocol.ts](./extension/react-app/src/hooks/ContinueGUIClientProtocol.ts). On the server side, this is implemented in [gui.py](./continuedev/src/continuedev/server/gui.py). You can see [gui_protocol.py](./continuedev/src/continuedev/server/gui_protocol.py) or [AbstractContinueGUIClientProtocol.ts](./extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts) for the protocol definition. + +When state is updated on the server, we currently send the entirety of the object over websockets to the GUI. This will of course have to be improved soon. The state object, `FullState`, is defined in [core/main.py](./continuedev/src/continuedev/core/main.py) and includes: + +- `history`, a record of previously run Steps. Displayed in order in the sidebar. +- `active`, whether the autopilot is currently running a step. Displayed as a loader while step is running. +- `user_input_queue`, the queue of user inputs that have not yet been processed due to waiting for previous Steps to complete. Displayed below the `active` loader until popped from the queue. +- `default_model`, the default model used for completions. Displayed as a toggleable button on the bottom of the GUI. +- `highlighted_ranges`, the ranges of code that have been selected to include as context. Displayed just above the main text input. +- `slash_commands`, the list of available slash commands. Displayed in the main text input dropdown. +- `adding_highlighted_code`, whether highlighting of new code for context is locked. Displayed as a button adjacent to `highlighted_ranges`. + +Updates are sent with `await sdk.update_ui()` when needed explicitly or `await autopilot.update_subscribers()` automatically between each Step. The GUI can listen for state updates with `ContinueGUIClientProtocol.onStateUpdate()`. + +## Ways to Contribute + +### Report Bugs + +### Suggest Enhancements + +### Updating / Improving Documentation + +Continue is continuously improving, but a feature isn't complete until it is reflected in the documentation! diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py index 5931d978..50d01f8d 100644 --- a/continuedev/src/continuedev/core/main.py +++ b/continuedev/src/continuedev/core/main.py @@ -259,10 +259,8 @@ class Step(ContinueBaseModel): def dict(self, *args, **kwargs): d = super().dict(*args, **kwargs) - if self.description is not None: - d["description"] = self.description - else: - d["description"] = "" + # Make sure description is always a string + d["description"] = self.description or "" return d @validator("name", pre=True, always=True) diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index c9bd42e0..aa462171 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -2,7 +2,7 @@ import DebugPanel from "./components/DebugPanel"; import GUI from "./pages/gui"; import { createContext } from "react"; import useContinueGUIProtocol from "./hooks/useWebsocket"; -import ContinueGUIClientProtocol from "./hooks/useContinueGUIProtocol"; +import ContinueGUIClientProtocol from "./hooks/ContinueGUIClientProtocol"; export const GUIClientContext = createContext< ContinueGUIClientProtocol | undefined @@ -13,11 +13,7 @@ function App() { return ( <GUIClientContext.Provider value={client}> - <DebugPanel - tabs={[ - { element: <GUI />, title: "GUI" } - ]} - /> + <DebugPanel tabs={[{ element: <GUI />, title: "GUI" }]} /> </GUIClientContext.Provider> ); } diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts new file mode 100644 index 00000000..6c0df8fc --- /dev/null +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -0,0 +1,35 @@ +abstract class AbstractContinueGUIClientProtocol { + abstract sendMainInput(input: string): void; + + abstract reverseToIndex(index: number): void; + + abstract sendRefinementInput(input: string, index: number): void; + + abstract sendStepUserInput(input: string, index: number): void; + + abstract onStateUpdate(state: any): void; + + abstract onAvailableSlashCommands( + callback: (commands: { name: string; description: string }[]) => void + ): void; + + abstract changeDefaultModel(model: string): void; + + abstract sendClear(): void; + + abstract retryAtIndex(index: number): void; + + abstract deleteAtIndex(index: number): void; + + abstract deleteContextAtIndices(indices: number[]): void; + + abstract setEditingAtIndices(indices: number[]): void; + + abstract setPinnedAtIndices(indices: number[]): void; + + abstract toggleAddingHighlightedCode(): void; + + abstract showLogsAtIndex(index: number): void; +} + +export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index 6c0df8fc..7d6c2a71 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -1,35 +1,92 @@ -abstract class AbstractContinueGUIClientProtocol { - abstract sendMainInput(input: string): void; +import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol"; +import { Messenger, WebsocketMessenger } from "./messenger"; +import { VscodeMessenger } from "./vscodeMessenger"; - abstract reverseToIndex(index: number): void; +class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { + messenger: Messenger; + // Server URL must contain the session ID param + serverUrlWithSessionId: string; - abstract sendRefinementInput(input: string, index: number): void; + constructor( + serverUrlWithSessionId: string, + useVscodeMessagePassing: boolean + ) { + super(); + this.serverUrlWithSessionId = serverUrlWithSessionId; + this.messenger = useVscodeMessagePassing + ? new VscodeMessenger(serverUrlWithSessionId) + : new WebsocketMessenger(serverUrlWithSessionId); + } - abstract sendStepUserInput(input: string, index: number): void; + sendMainInput(input: string) { + this.messenger.send("main_input", { input }); + } - abstract onStateUpdate(state: any): void; + reverseToIndex(index: number) { + this.messenger.send("reverse_to_index", { index }); + } - abstract onAvailableSlashCommands( + sendRefinementInput(input: string, index: number) { + this.messenger.send("refinement_input", { input, index }); + } + + sendStepUserInput(input: string, index: number) { + this.messenger.send("step_user_input", { input, index }); + } + + onStateUpdate(callback: (state: any) => void) { + this.messenger.onMessageType("state_update", (data: any) => { + if (data.state) { + callback(data.state); + } + }); + } + + onAvailableSlashCommands( callback: (commands: { name: string; description: string }[]) => void - ): void; + ) { + this.messenger.onMessageType("available_slash_commands", (data: any) => { + if (data.commands) { + callback(data.commands); + } + }); + } - abstract changeDefaultModel(model: string): void; + changeDefaultModel(model: string) { + this.messenger.send("change_default_model", { model }); + } - abstract sendClear(): void; + sendClear() { + this.messenger.send("clear_history", {}); + } - abstract retryAtIndex(index: number): void; + retryAtIndex(index: number) { + this.messenger.send("retry_at_index", { index }); + } - abstract deleteAtIndex(index: number): void; + deleteAtIndex(index: number) { + this.messenger.send("delete_at_index", { index }); + } - abstract deleteContextAtIndices(indices: number[]): void; + deleteContextAtIndices(indices: number[]) { + this.messenger.send("delete_context_at_indices", { indices }); + } - abstract setEditingAtIndices(indices: number[]): void; + setEditingAtIndices(indices: number[]) { + this.messenger.send("set_editing_at_indices", { indices }); + } - abstract setPinnedAtIndices(indices: number[]): void; + setPinnedAtIndices(indices: number[]) { + this.messenger.send("set_pinned_at_indices", { indices }); + } - abstract toggleAddingHighlightedCode(): void; + toggleAddingHighlightedCode(): void { + this.messenger.send("toggle_adding_highlighted_code", {}); + } - abstract showLogsAtIndex(index: number): void; + showLogsAtIndex(index: number): void { + this.messenger.send("show_logs_at_index", { index }); + } } -export default AbstractContinueGUIClientProtocol; +export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts deleted file mode 100644 index fef5b2e1..00000000 --- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts +++ /dev/null @@ -1,95 +0,0 @@ -import AbstractContinueGUIClientProtocol from "./ContinueGUIClientProtocol"; -// import { Messenger, WebsocketMessenger } from "../../../src/util/messenger"; -import { Messenger, WebsocketMessenger } from "./messenger"; -import { VscodeMessenger } from "./vscodeMessenger"; - -class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { - messenger: Messenger; - // Server URL must contain the session ID param - serverUrlWithSessionId: string; - - constructor( - serverUrlWithSessionId: string, - useVscodeMessagePassing: boolean - ) { - super(); - this.serverUrlWithSessionId = serverUrlWithSessionId; - if (useVscodeMessagePassing) { - this.messenger = new VscodeMessenger(serverUrlWithSessionId); - } else { - this.messenger = new WebsocketMessenger(serverUrlWithSessionId); - } - } - - sendMainInput(input: string) { - this.messenger.send("main_input", { input }); - } - - reverseToIndex(index: number) { - this.messenger.send("reverse_to_index", { index }); - } - - sendRefinementInput(input: string, index: number) { - this.messenger.send("refinement_input", { input, index }); - } - - sendStepUserInput(input: string, index: number) { - this.messenger.send("step_user_input", { input, index }); - } - - onStateUpdate(callback: (state: any) => void) { - this.messenger.onMessageType("state_update", (data: any) => { - if (data.state) { - callback(data.state); - } - }); - } - - onAvailableSlashCommands( - callback: (commands: { name: string; description: string }[]) => void - ) { - this.messenger.onMessageType("available_slash_commands", (data: any) => { - if (data.commands) { - callback(data.commands); - } - }); - } - - changeDefaultModel(model: string) { - this.messenger.send("change_default_model", { model }); - } - - sendClear() { - this.messenger.send("clear_history", {}); - } - - retryAtIndex(index: number) { - this.messenger.send("retry_at_index", { index }); - } - - deleteAtIndex(index: number) { - this.messenger.send("delete_at_index", { index }); - } - - deleteContextAtIndices(indices: number[]) { - this.messenger.send("delete_context_at_indices", { indices }); - } - - setEditingAtIndices(indices: number[]) { - this.messenger.send("set_editing_at_indices", { indices }); - } - - setPinnedAtIndices(indices: number[]) { - this.messenger.send("set_pinned_at_indices", { indices }); - } - - toggleAddingHighlightedCode(): void { - this.messenger.send("toggle_adding_highlighted_code", {}); - } - - showLogsAtIndex(index: number): void { - this.messenger.send("show_logs_at_index", { index }); - } -} - -export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/useWebsocket.ts b/extension/react-app/src/hooks/useWebsocket.ts index e762666f..6b36be97 100644 --- a/extension/react-app/src/hooks/useWebsocket.ts +++ b/extension/react-app/src/hooks/useWebsocket.ts @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { RootStore } from "../redux/store"; import { useSelector } from "react-redux"; -import ContinueGUIClientProtocol from "./useContinueGUIProtocol"; +import ContinueGUIClientProtocol from "./ContinueGUIClientProtocol"; import { postVscMessage } from "../vscode"; function useContinueGUIProtocol(useVscodeMessagePassing: boolean = true) { diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index 8ea08e89..a7f6c55b 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -36,8 +36,8 @@ export async function activateExtension(context: vscode.ExtensionContext) { }) .catch((e) => console.log("Error checking for extension updates: ", e)); - // Wrap the server start logic in a new Promise - const serverStartPromise = new Promise((resolve, reject) => { + // Start the server and display loader if taking > 2 seconds + await new Promise((resolve) => { let serverStarted = false; // Start the server and set serverStarted to true when done @@ -71,15 +71,6 @@ export async function activateExtension(context: vscode.ExtensionContext) { }, 2000); }); - // Await the server start promise - await serverStartPromise; - - // Register commands and providers - sendTelemetryEvent(TelemetryEvent.ExtensionActivated); - registerAllCodeLensProviders(context); - registerAllCommands(context); - registerQuickFixProvider(); - // Initialize IDE Protocol Client const serverUrl = getContinueServerUrl(); ideProtocolClient = new IdeProtocolClient( @@ -87,6 +78,7 @@ export async function activateExtension(context: vscode.ExtensionContext) { context ); + // Register Continue GUI as sidebar webview, and beging a new session { const sessionIdPromise = await ideProtocolClient.getSessionId(); const provider = new ContinueGUIWebviewViewProvider(sessionIdPromise); diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index 14a8df72..a1370a01 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -16,6 +16,10 @@ import fs = require("fs"); import { WebsocketMessenger } from "./util/messenger"; import { diffManager } from "./diffs"; import path = require("path"); +import { sendTelemetryEvent, TelemetryEvent } from "./telemetry"; +import { registerAllCodeLensProviders } from "./lang-server/codeLens"; +import { registerAllCommands } from "./commands"; +import registerQuickFixProvider from "./lang-server/codeActions"; const continueVirtualDocumentScheme = "continue"; @@ -76,6 +80,12 @@ class IdeProtocolClient { this._serverUrl = serverUrl; this._newWebsocketMessenger(); + // Register commands and providers + sendTelemetryEvent(TelemetryEvent.ExtensionActivated); + registerAllCodeLensProviders(context); + registerAllCommands(context); + registerQuickFixProvider(); + // Setup listeners for any file changes in open editors // vscode.workspace.onDidChangeTextDocument((event) => { // if (this._makingEdit === 0) { |