summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--continuedev/poetry.lock34
-rw-r--r--continuedev/pyproject.toml1
-rw-r--r--continuedev/src/continuedev/core/abstract_sdk.py8
-rw-r--r--continuedev/src/continuedev/core/autopilot.py19
-rw-r--r--continuedev/src/continuedev/core/config.py34
-rw-r--r--continuedev/src/continuedev/core/main.py6
-rw-r--r--continuedev/src/continuedev/core/policy.py98
-rw-r--r--continuedev/src/continuedev/core/sdk.py20
-rw-r--r--continuedev/src/continuedev/libs/llm/openai.py38
-rw-r--r--continuedev/src/continuedev/libs/util/calculate_diff.py166
-rw-r--r--continuedev/src/continuedev/libs/util/copy_codebase.py38
-rw-r--r--continuedev/src/continuedev/libs/util/step_name_to_steps.py27
-rw-r--r--continuedev/src/continuedev/models/filesystem_edit.py4
-rw-r--r--continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py2
-rw-r--r--continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py3
-rw-r--r--continuedev/src/continuedev/server/gui.py17
-rw-r--r--continuedev/src/continuedev/server/gui_protocol.py14
-rw-r--r--continuedev/src/continuedev/server/session_manager.py1
-rw-r--r--continuedev/src/continuedev/server/state_manager.py21
-rw-r--r--continuedev/src/continuedev/steps/chat.py19
-rw-r--r--continuedev/src/continuedev/steps/core/core.py47
-rw-r--r--continuedev/src/continuedev/steps/draft/migration.py2
-rw-r--r--continuedev/src/continuedev/steps/find_and_replace.py2
-rw-r--r--continuedev/src/continuedev/steps/input/nl_multiselect.py3
-rw-r--r--continuedev/src/continuedev/steps/main.py32
-rw-r--r--continuedev/src/continuedev/steps/react.py39
-rw-r--r--continuedev/src/continuedev/steps/steps_on_startup.py24
-rw-r--r--docs/docs/concepts/sdk.md2
-rw-r--r--extension/package-lock.json64
-rw-r--r--extension/package.json83
-rw-r--r--extension/react-app/package-lock.json60
-rw-r--r--extension/react-app/package.json1
-rw-r--r--extension/react-app/src/components/CodeBlock.tsx1
-rw-r--r--extension/react-app/src/components/ComboBox.tsx146
-rw-r--r--extension/react-app/src/components/StepContainer.tsx67
-rw-r--r--extension/react-app/src/components/index.ts18
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts10
-rw-r--r--extension/react-app/src/hooks/useContinueGUIProtocol.ts18
-rw-r--r--extension/react-app/src/tabs/chat/MessageDiv.tsx4
-rw-r--r--extension/react-app/src/tabs/gui.tsx96
-rw-r--r--extension/scripts/continuedev-0.1.1-py3-none-any.whlbin74791 -> 78916 bytes
-rw-r--r--extension/src/continueIdeClient.ts6
-rw-r--r--extension/src/decorations.ts19
43 files changed, 978 insertions, 336 deletions
diff --git a/continuedev/poetry.lock b/continuedev/poetry.lock
index 857a7c99..4aedce87 100644
--- a/continuedev/poetry.lock
+++ b/continuedev/poetry.lock
@@ -345,6 +345,21 @@ typing-inspect = ">=0.4.0"
dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest (>=6.2.3)", "simplejson", "types-dataclasses"]
[[package]]
+name = "diff-match-patch"
+version = "20230430"
+description = "Diff Match and Patch"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "diff-match-patch-20230430.tar.gz", hash = "sha256:953019cdb9c9d2c9e47b5b12bcff3cf4746fc4598eb406076fa1fc27e6a1f15c"},
+ {file = "diff_match_patch-20230430-py3-none-any.whl", hash = "sha256:dce43505fb7b1b317de7195579388df0746d90db07015ed47a85e5e44930ef93"},
+]
+
+[package.extras]
+dev = ["attribution (==1.6.2)", "black (==23.3.0)", "flit (==3.8.0)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
+
+[[package]]
name = "fastapi"
version = "0.95.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
@@ -1252,23 +1267,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
-name = "setuptools"
-version = "67.7.2"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"},
- {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
-[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
@@ -1739,4 +1737,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "9f9254c954b7948c49debba86bc81a4a9c3f50694424f5940d0058725b1bf0fb"
+content-hash = "0f5f759bac0e44a1fbcc9babeccdea8688ea2226a4bae7a13858542ae03a3228"
diff --git a/continuedev/pyproject.toml b/continuedev/pyproject.toml
index 631742ec..7315e79d 100644
--- a/continuedev/pyproject.toml
+++ b/continuedev/pyproject.toml
@@ -7,6 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.9"
+diff-match-patch = "^20230430"
fastapi = "^0.95.1"
typer = "^0.7.0"
openai = "^0.27.5"
diff --git a/continuedev/src/continuedev/core/abstract_sdk.py b/continuedev/src/continuedev/core/abstract_sdk.py
index 417971cd..0658f1b8 100644
--- a/continuedev/src/continuedev/core/abstract_sdk.py
+++ b/continuedev/src/continuedev/core/abstract_sdk.py
@@ -76,8 +76,8 @@ class AbstractContinueSDK(ABC):
async def get_user_secret(self, env_var: str, prompt: str) -> str:
pass
- @abstractmethod
- async def get_config(self) -> ContinueConfig:
+ @abstractproperty
+ def config(self) -> ContinueConfig:
pass
@abstractmethod
@@ -88,6 +88,6 @@ class AbstractContinueSDK(ABC):
def add_chat_context(self, content: str, role: ChatMessageRole = "assistent"):
pass
- @abstractproperty
- def chat_context(self) -> List[ChatMessage]:
+ @abstractmethod
+ async def get_chat_context(self) -> List[ChatMessage]:
pass
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index c979d53a..0874bbc5 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -40,6 +40,15 @@ class Autopilot(ContinueBaseModel):
def get_full_state(self) -> FullState:
return FullState(history=self.history, active=self._active, user_input_queue=self._main_user_input_queue)
+ async def get_available_slash_commands(self) -> List[Dict]:
+ return list(map(lambda x: {"name": x.name, "description": x.description}, self.continue_sdk.config.slash_commands)) or []
+
+ async def clear_history(self):
+ self.history = History.from_empty()
+ self._main_user_input_queue = []
+ self._active = False
+ await self.update_subscribers()
+
def on_update(self, callback: Coroutine["FullState", None, None]):
"""Subscribe to changes to state"""
self._on_update_callbacks.append(callback)
@@ -88,6 +97,10 @@ class Autopilot(ContinueBaseModel):
async def retry_at_index(self, index: int):
self._retry_queue.post(str(index), None)
+ async def delete_at_index(self, index: int):
+ self.history.timeline[index].step.hide = True
+ await self.update_subscribers()
+
async def _run_singular_step(self, step: "Step", is_future_step: bool = False) -> Coroutine[Observation, None, None]:
capture_event(
'step run', {'step_name': step.name, 'params': step.dict()})
@@ -192,7 +205,7 @@ class Autopilot(ContinueBaseModel):
await self._run_singular_step(next_step, is_future_step)
- if next_step := self.policy.next(self.history):
+ if next_step := self.policy.next(self.continue_sdk.config, self.history):
is_future_step = False
elif next_step := self.history.take_next_step():
is_future_step = True
@@ -205,11 +218,11 @@ class Autopilot(ContinueBaseModel):
await self.update_subscribers()
async def run_from_observation(self, observation: Observation):
- next_step = self.policy.next(self.history)
+ next_step = self.policy.next(self.continue_sdk.config, self.history)
await self.run_from_step(next_step)
async def run_policy(self):
- first_step = self.policy.next(self.history)
+ first_step = self.policy.next(self.continue_sdk.config, self.history)
await self.run_from_step(first_step)
async def _request_halt(self):
diff --git a/continuedev/src/continuedev/core/config.py b/continuedev/src/continuedev/core/config.py
index 8ed41a82..cf723984 100644
--- a/continuedev/src/continuedev/core/config.py
+++ b/continuedev/src/continuedev/core/config.py
@@ -1,9 +1,18 @@
import json
import os
-from pydantic import BaseModel
+from pydantic import BaseModel, validator
from typing import List, Optional, Dict
import yaml
+from .main import Step
+
+
+class SlashCommand(BaseModel):
+ name: str
+ description: str
+ step_name: str
+ params: Optional[Dict] = {}
+
class ContinueConfig(BaseModel):
"""
@@ -12,6 +21,29 @@ class ContinueConfig(BaseModel):
steps_on_startup: Optional[Dict[str, Dict]] = {}
server_url: Optional[str] = None
allow_anonymous_telemetry: Optional[bool] = True
+ slash_commands: Optional[List[SlashCommand]] = [
+ # SlashCommand(
+ # name="pytest",
+ # description="Write pytest unit tests for the current file",
+ # step_name="WritePytestsRecipe",
+ # params=??)
+
+ SlashCommand(
+ name="dlt",
+ description="Create a dlt pipeline",
+ step_name="CreatePipelineRecipe",
+ ),
+ SlashCommand(
+ name="ddtobq",
+ description="Create a dlt pipeline to load data from a data source into BigQuery",
+ step_name="DDtoBQRecipe",
+ ),
+ SlashCommand(
+ name="deployairflow",
+ description="Deploy a dlt pipeline to Airflow",
+ step_name="DeployPipelineAirflowRecipe",
+ ),
+ ]
def load_config(config_file: str) -> ContinueConfig:
diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py
index 81aaaf2e..f6b26d69 100644
--- a/continuedev/src/continuedev/core/main.py
+++ b/continuedev/src/continuedev/core/main.py
@@ -118,11 +118,15 @@ class Models:
pass
+class ContinueConfig:
+ pass
+
+
class Policy(ContinueBaseModel):
"""A rule that determines which step to take next"""
# Note that history is mutable, kinda sus
- def next(self, history: History = History.from_empty()) -> "Step":
+ def next(self, config: ContinueConfig, history: History = History.from_empty()) -> "Step":
raise NotImplementedError
diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py
index 8e43bf55..37a10e36 100644
--- a/continuedev/src/continuedev/core/policy.py
+++ b/continuedev/src/continuedev/core/policy.py
@@ -1,5 +1,6 @@
from typing import List, Tuple, Type
+from .config import ContinueConfig
from ..steps.chroma import AnswerQuestionChroma, EditFileChroma, CreateCodebaseIndexChroma
from ..steps.steps_on_startup import StepsOnStartupStep
from ..recipes.CreatePipelineRecipe.main import CreatePipelineRecipe
@@ -11,14 +12,17 @@ from ..steps.main import EditHighlightedCodeStep, SolveTracebackStep, RunCodeSte
from ..recipes.WritePytestsRecipe.main import WritePytestsRecipe
from ..recipes.ContinueRecipeRecipe.main import ContinueStepStep
from ..steps.comment_code import CommentCodeStep
+from ..steps.react import NLDecisionStep
+from ..steps.chat import SimpleChatStep
from ..recipes.DDtoBQRecipe.main import DDtoBQRecipe
from ..steps.core.core import MessageStep
+from ..libs.util.step_name_to_steps import get_step_from_name
class DemoPolicy(Policy):
ran_code_last: bool = False
- def next(self, history: History) -> Step:
+ def next(self, config: ContinueConfig, history: History) -> Step:
# At the very start, run initial Steps spcecified in the config
if history.get_current() is None:
return (
@@ -30,25 +34,36 @@ class DemoPolicy(Policy):
observation = history.get_current().observation
if observation is not None and isinstance(observation, UserInputObservation):
# This could be defined with ObservationTypePolicy. Ergonomics not right though.
+ user_input = observation.user_input
+
+ if user_input.startswith("/"):
+ command_name = user_input.split(" ")[0]
+ after_command = " ".join(user_input.split(" ")[1:])
+ for slash_command in config.slash_commands:
+ if slash_command.name == command_name[1:]:
+ return get_step_from_name(slash_command.step_name, slash_command.params)
+
+ if "/pytest" in user_input.lower():
+ return WritePytestsRecipe(instructions=user_input)
if "/pytest" in observation.user_input.lower():
return WritePytestsRecipe(instructions=observation.user_input)
- elif "/dlt" in observation.user_input.lower():
- return CreatePipelineRecipe()
- elif "/ddtobq" in observation.user_input.lower():
- return DDtoBQRecipe()
- elif "/airflow" in observation.user_input.lower():
- return DeployPipelineAirflowRecipe()
- elif "/transform" in observation.user_input.lower():
- return AddTransformRecipe()
elif "/comment" in observation.user_input.lower():
return CommentCodeStep()
- elif "/ask" in observation.user_input:
- return AnswerQuestionChroma(question=" ".join(observation.user_input.split(" ")[1:]))
- elif "/edit" in observation.user_input:
- return EditFileChroma(request=" ".join(observation.user_input.split(" ")[1:]))
- elif "/step" in observation.user_input:
- return ContinueStepStep(prompt=" ".join(observation.user_input.split(" ")[1:]))
- return EditHighlightedCodeStep(user_input=observation.user_input)
+ elif "/ask" in user_input:
+ return AnswerQuestionChroma(question=" ".join(user_input.split(" ")[1:]))
+ elif "/edit" in user_input:
+ return EditFileChroma(request=" ".join(user_input.split(" ")[1:]))
+ elif "/step" in user_input:
+ return ContinueStepStep(prompt=" ".join(user_input.split(" ")[1:]))
+ # return EditHighlightedCodeStep(user_input=user_input)
+ return NLDecisionStep(user_input=user_input, steps=[
+ (EditHighlightedCodeStep(user_input=user_input),
+ "Edit the highlighted code"),
+ # AnswerQuestionChroma(question=user_input),
+ # EditFileChroma(request=user_input),
+ (SimpleChatStep(user_input=user_input),
+ "Respond to the user with a chat message"),
+ ], default_step=EditHighlightedCodeStep(user_input=user_input))
state = history.get_current()
@@ -57,54 +72,3 @@ class DemoPolicy(Policy):
return SolveTracebackStep(traceback=observation.traceback)
else:
return None
-
-
-class ObservationTypePolicy(Policy):
- def __init__(self, base_policy: Policy, observation_type: Type[Observation], step_type: Type[Step]):
- self.observation_type = observation_type
- self.step_type = step_type
- self.base_policy = base_policy
-
- def next(self, history: History) -> Step:
- observation = history.last_observation()
- if observation is not None and isinstance(observation, self.observation_type):
- return self.step_type(observation)
- return self.base_policy.next(history)
-
-
-class PolicyWrappedWithValidators(Policy):
- """Default is to stop, unless the validator tells what to do next"""
- index: int
- stage: int
-
- def __init__(self, base_policy: Policy, pairs: List[Tuple[Validator, Type[Step]]]):
- # Want to pass Type[Validator], or just the Validator? Question of where params are coming from.
- self.pairs = pairs
- self.index = len(pairs)
- self.validating = 0
- self.base_policy = base_policy
-
- def next(self, history: History) -> Step:
- if self.index == len(self.pairs):
- self.index = 0
- return self.base_policy.next(history)
-
- if self.stage == 0:
- # Running the validator at the current index for the first time
- validator, step = self.pairs[self.index]
- self.stage = 1
- return validator
- elif self.stage == 1:
- # Previously ran the validator at the current index, now receiving its ValidatorObservation
- observation = history.last_observation()
- if observation.passed:
- self.stage = 0
- self.index += 1
- if self.index == len(self.pairs):
- self.index = 0
- return self.base_policy.next(history)
- else:
- return self.pairs[self.index][0]
- else:
- _, step_type = self.pairs[self.index]
- return step_type(observation)
diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py
index 59bfc0f2..1f4cdfb2 100644
--- a/continuedev/src/continuedev/core/sdk.py
+++ b/continuedev/src/continuedev/core/sdk.py
@@ -1,4 +1,3 @@
-from abc import ABC, abstractmethod
import asyncio
from functools import cached_property
from typing import Coroutine, Union
@@ -119,8 +118,9 @@ class ContinueSDK(AbstractContinueSDK):
async def get_user_secret(self, env_var: str, prompt: str) -> str:
return await self.ide.getUserSecret(env_var)
- async def get_config(self) -> ContinueConfig:
- dir = await self.ide.getWorkspaceDirectory()
+ @property
+ def config(self) -> ContinueConfig:
+ dir = self.ide.workspace_directory
yaml_path = os.path.join(dir, '.continue', 'config.yaml')
json_path = os.path.join(dir, '.continue', 'config.json')
if os.path.exists(yaml_path):
@@ -141,6 +141,14 @@ class ContinueSDK(AbstractContinueSDK):
self.history.timeline[self.history.current_index].step.chat_context.append(
ChatMessage(content=content, role=role))
- @property
- def chat_context(self) -> List[ChatMessage]:
- return self.history.to_chat_history()
+ async def get_chat_context(self) -> List[ChatMessage]:
+ history_context = self.history.to_chat_history()
+ highlighted_code = await self.ide.getHighlightedCode()
+ for rif in highlighted_code:
+ code = await self.ide.readRangeInFile(rif)
+ history_context.append(ChatMessage(
+ content=f"The following code is highlighted:\n```\n{code}\n```", role="user"))
+ return history_context
+
+ async def update_ui(self):
+ await self.__autopilot.update_subscribers()
diff --git a/continuedev/src/continuedev/libs/llm/openai.py b/continuedev/src/continuedev/libs/llm/openai.py
index 6a537afd..9b8d3447 100644
--- a/continuedev/src/continuedev/libs/llm/openai.py
+++ b/continuedev/src/continuedev/libs/llm/openai.py
@@ -23,7 +23,7 @@ class OpenAI(LLM):
def with_system_message(self, system_message: Union[str, None]):
return OpenAI(api_key=self.api_key, system_message=system_message)
- def stream_chat(self, messages, **kwargs) -> Generator[Union[Any, List, Dict], None, None]:
+ def stream_chat(self, prompt, with_history: List[ChatMessage] = [], **kwargs) -> Generator[Union[Any, List, Dict], None, None]:
self.completion_count += 1
args = {"max_tokens": DEFAULT_MAX_TOKENS, "temperature": 0.5, "top_p": 1,
"frequency_penalty": 0, "presence_penalty": 0} | kwargs
@@ -31,7 +31,7 @@ class OpenAI(LLM):
args["model"] = "gpt-3.5-turbo"
for chunk in openai.ChatCompletion.create(
- messages=messages,
+ messages=self.compile_chat_messages(with_history, prompt),
**args,
):
if "content" in chunk.choices[0].delta:
@@ -39,7 +39,21 @@ class OpenAI(LLM):
else:
continue
- def stream_complete(self, prompt: str, **kwargs) -> Generator[Union[Any, List, Dict], None, None]:
+ def compile_chat_messages(self, msgs: List[ChatMessage], prompt: str) -> List[Dict]:
+ history = []
+ if self.system_message:
+ history.append({
+ "role": "system",
+ "content": self.system_message
+ })
+ history += [msg.dict() for msg in msgs]
+ history.append({
+ "role": "user",
+ "content": prompt
+ })
+ return history
+
+ def stream_complete(self, prompt: str, with_history: List[ChatMessage] = [], **kwargs) -> Generator[Union[Any, List, Dict], None, None]:
self.completion_count += 1
args = {"model": self.default_model, "max_tokens": DEFAULT_MAX_TOKENS, "temperature": 0.5,
"top_p": 1, "frequency_penalty": 0, "presence_penalty": 0, "suffix": None} | kwargs
@@ -47,10 +61,7 @@ class OpenAI(LLM):
if args["model"] == "gpt-3.5-turbo":
generator = openai.ChatCompletion.create(
- messages=[{
- "role": "user",
- "content": prompt
- }],
+ messages=self.compile_chat_messages(with_history, prompt),
**args,
)
for chunk in generator:
@@ -71,19 +82,8 @@ class OpenAI(LLM):
"frequency_penalty": 0, "presence_penalty": 0, "stream": False} | kwargs
if args["model"] == "gpt-3.5-turbo":
- messages = []
- if self.system_message:
- messages.append({
- "role": "system",
- "content": self.system_message
- })
- messages += [msg.dict() for msg in with_history]
- messages.append({
- "role": "user",
- "content": prompt
- })
resp = openai.ChatCompletion.create(
- messages=messages,
+ messages=self.compile_chat_messages(with_history, prompt),
**args,
).choices[0].message.content
else:
diff --git a/continuedev/src/continuedev/libs/util/calculate_diff.py b/continuedev/src/continuedev/libs/util/calculate_diff.py
new file mode 100644
index 00000000..ff0a135f
--- /dev/null
+++ b/continuedev/src/continuedev/libs/util/calculate_diff.py
@@ -0,0 +1,166 @@
+import difflib
+from typing import List
+from ...models.main import Position, Range
+from ...models.filesystem import FileEdit
+from diff_match_patch import diff_match_patch
+
+
+def calculate_diff_match_patch(filepath: str, original: str, updated: str) -> List[FileEdit]:
+ dmp = diff_match_patch()
+ diffs = dmp.diff_main(original, updated)
+ dmp.diff_cleanupSemantic(diffs)
+
+ replacements = []
+
+ current_index = 0
+ deleted_length = 0
+
+ for diff in diffs:
+ if diff[0] == diff_match_patch.DIFF_EQUAL:
+ current_index += len(diff[1])
+ deleted_length = 0
+ elif diff[0] == diff_match_patch.DIFF_INSERT:
+ current_index += deleted_length
+ replacements.append((current_index, current_index, diff[1]))
+ current_index += len(diff[1])
+ deleted_length = 0
+ elif diff[0] == diff_match_patch.DIFF_DELETE:
+ replacements.append(
+ (current_index, current_index + len(diff[1]), ''))
+ deleted_length += len(diff[1])
+ elif diff[0] == diff_match_patch.DIFF_REPLACE:
+ replacements.append(
+ (current_index, current_index + len(diff[1]), ''))
+ current_index += deleted_length
+ replacements.append((current_index, current_index, diff[2]))
+ current_index += len(diff[2])
+ deleted_length = 0
+
+ return [FileEdit(filepath=filepath, range=Range.from_indices(original, r[0], r[1]), replacement=r[2]) for r in replacements]
+
+
+def calculate_diff(filepath: str, original: str, updated: str) -> List[FileEdit]:
+ s = difflib.SequenceMatcher(None, original, updated)
+ offset = 0 # The indices are offset by previous deletions/insertions
+ edits = []
+ for tag, i1, i2, j1, j2 in s.get_opcodes():
+ i1, i2, j1, j2 = i1 + offset, i2 + offset, j1 + offset, j2 + offset
+ replacement = updated[j1:j2]
+ if tag == "equal":
+ pass
+ elif tag == "delete":
+ edits.append(FileEdit.from_deletion(
+ filepath, Range.from_indices(original, i1, i2)))
+ offset -= i2 - i1
+ elif tag == "insert":
+ edits.append(FileEdit.from_insertion(
+ filepath, Position.from_index(original, i1), replacement))
+ offset += j2 - j1
+ elif tag == "replace":
+ edits.append(FileEdit(filepath=filepath, range=Range.from_indices(
+ original, i1, i2), replacement=replacement))
+ offset += (j2 - j1) - (i2 - i1)
+ else:
+ raise Exception("Unexpected difflib.SequenceMatcher tag: " + tag)
+
+ return edits
+
+
+def calculate_diff2(filepath: str, original: str, updated: str) -> List[FileEdit]:
+ # original_lines = original.splitlines()
+ # updated_lines = updated.splitlines()
+ # offset = 0
+ # while len(original_lines) and len(updated_lines) and original_lines[0] == updated_lines[0]:
+ # original_lines = original_lines[1:]
+ # updated_lines = updated_lines[1:]
+
+ # while len(original_lines) and len(updated_lines) and original_lines[-1] == updated_lines[-1]:
+ # original_lines = original_lines[:-1]
+ # updated_lines = updated_lines[:-1]
+
+ # original = "\n".join(original_lines)
+ # updated = "\n".join(updated_lines)
+
+ edits = []
+ max_iterations = 1000
+ i = 0
+ while not original == updated:
+ # TODO - For some reason it can't handle a single newline at the end of the file?
+ s = difflib.SequenceMatcher(None, original, updated)
+ opcodes = s.get_opcodes()
+ for edit_index in range(len(opcodes)):
+ tag, i1, i2, j1, j2 = s.get_opcodes()[edit_index]
+ replacement = updated[j1:j2]
+ if tag == "equal":
+ continue
+ elif tag == "delete":
+ edits.append(FileEdit.from_deletion(
+ filepath, Range.from_indices(original, i1, i2)))
+ elif tag == "insert":
+ edits.append(FileEdit.from_insertion(
+ filepath, Position.from_index(original, i1), replacement))
+ elif tag == "replace":
+ edits.append(FileEdit(filepath=filepath, range=Range.from_indices(
+ original, i1, i2), replacement=replacement))
+ else:
+ raise Exception(
+ "Unexpected difflib.SequenceMatcher tag: " + tag)
+ break
+
+ original = apply_edit_to_str(original, edits[-1])
+
+ i += 1
+ if i > max_iterations:
+ raise Exception("Max iterations reached")
+
+ return edits
+
+
+def read_range_in_str(s: str, r: Range) -> str:
+ lines = s.splitlines()[r.start.line:r.end.line + 1]
+ if len(lines) == 0:
+ return ""
+
+ lines[0] = lines[0][r.start.character:]
+ lines[-1] = lines[-1][:r.end.character + 1]
+ return "\n".join(lines)
+
+
+def apply_edit_to_str(s: str, edit: FileEdit) -> str:
+ original = read_range_in_str(s, edit.range)
+
+ # Split lines and deal with some edge cases (could obviously be nicer)
+ lines = s.splitlines()
+ if s.startswith("\n"):
+ lines.insert(0, "")
+ if s.endswith("\n"):
+ lines.append("")
+
+ if len(lines) == 0:
+ lines = [""]
+
+ end = Position(line=edit.range.end.line,
+ character=edit.range.end.character)
+ if edit.range.end.line == len(lines) and edit.range.end.character == 0:
+ end = Position(line=edit.range.end.line - 1,
+ character=len(lines[min(len(lines) - 1, edit.range.end.line - 1)]))
+
+ before_lines = lines[:edit.range.start.line]
+ after_lines = lines[end.line + 1:]
+ between_str = lines[min(len(lines) - 1, edit.range.start.line)][:edit.range.start.character] + \
+ edit.replacement + \
+ lines[min(len(lines) - 1, end.line)][end.character + 1:]
+
+ new_range = Range(
+ start=edit.range.start,
+ end=Position(
+ line=edit.range.start.line +
+ len(edit.replacement.splitlines()) - 1,
+ character=edit.range.start.character +
+ len(edit.replacement.splitlines()
+ [-1]) if edit.replacement != "" else 0
+ )
+ )
+
+ lines = before_lines + between_str.splitlines() + after_lines
+ return "\n".join(lines)
diff --git a/continuedev/src/continuedev/libs/util/copy_codebase.py b/continuedev/src/continuedev/libs/util/copy_codebase.py
index af957a34..97143faf 100644
--- a/continuedev/src/continuedev/libs/util/copy_codebase.py
+++ b/continuedev/src/continuedev/libs/util/copy_codebase.py
@@ -3,13 +3,12 @@ from pathlib import Path
from typing import Iterable, List, Union
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
-from ..models.main import FileEdit, DeleteDirectory, DeleteFile, AddDirectory, AddFile, FileSystemEdit, Position, Range, RenameFile, RenameDirectory, SequentialFileSystemEdit
-from ..models.filesystem import FileSystem
-from ..libs.main import Autopilot
-from ..libs.map_path import map_path
-from ..libs.steps.main import ManualEditAction
+from ...models.main import FileEdit, DeleteDirectory, DeleteFile, AddDirectory, AddFile, FileSystemEdit, RenameFile, RenameDirectory, SequentialFileSystemEdit
+from ...models.filesystem import FileSystem
+from ...core.autopilot import Autopilot
+from .map_path import map_path
+from ...core.sdk import ManualEditStep
import shutil
-import difflib
def create_copy(orig_root: str, copy_root: str = None, ignore: Iterable[str] = []):
@@ -36,33 +35,6 @@ def create_copy(orig_root: str, copy_root: str = None, ignore: Iterable[str] = [
os.symlink(child, map_path(child))
-def calculate_diff(filepath: str, original: str, updated: str) -> List[FileEdit]:
- s = difflib.SequenceMatcher(None, original, updated)
- offset = 0 # The indices are offset by previous deletions/insertions
- edits = []
- for tag, i1, i2, j1, j2 in s.get_opcodes():
- i1, i2, j1, j2 = i1 + offset, i2 + offset, j1 + offset, j2 + offset
- replacement = updated[j1:j2]
- if tag == "equal":
- pass
- elif tag == "delete":
- edits.append(FileEdit.from_deletion(
- filepath, Range.from_indices(original, i1, i2)))
- offset -= i2 - i1
- elif tag == "insert":
- edits.append(FileEdit.from_insertion(
- filepath, Position.from_index(original, i1), replacement))
- offset += j2 - j1
- elif tag == "replace":
- edits.append(FileEdit(filepath, Range.from_indices(
- original, i1, i2), replacement))
- offset += (j2 - j1) - (i2 + i1)
- else:
- raise Exception("Unexpected difflib.SequenceMatcher tag: " + tag)
-
- return edits
-
-
# The whole usage of watchdog here should only be specific to RealFileSystem, you want to have a different "Observer" class for VirtualFileSystem, which would depend on being sent notifications
class CopyCodebaseEventHandler(PatternMatchingEventHandler):
def __init__(self, ignore_directories: List[str], ignore_patterns: List[str], autopilot: Autopilot, orig_root: str, copy_root: str, filesystem: FileSystem):
diff --git a/continuedev/src/continuedev/libs/util/step_name_to_steps.py b/continuedev/src/continuedev/libs/util/step_name_to_steps.py
new file mode 100644
index 00000000..4023b73b
--- /dev/null
+++ b/continuedev/src/continuedev/libs/util/step_name_to_steps.py
@@ -0,0 +1,27 @@
+from typing import Dict
+
+from ...core.main import Step
+from ...steps.core.core import UserInputStep
+from ...recipes.CreatePipelineRecipe.main import CreatePipelineRecipe
+from ...recipes.DDtoBQRecipe.main import DDtoBQRecipe
+from ...recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe
+from ...recipes.DDtoBQRecipe.main import DDtoBQRecipe
+from ...recipes.AddTransformRecipe.main import AddTransformRecipe
+
+step_name_to_step_class = {
+ "UserInputStep": UserInputStep,
+ "CreatePipelineRecipe": CreatePipelineRecipe,
+ "DDtoBQRecipe": DDtoBQRecipe,
+ "DeployPipelineAirflowRecipe": DeployPipelineAirflowRecipe,
+ "AddTransformRecipe": AddTransformRecipe,
+ "DDtoBQRecipe": DDtoBQRecipe
+}
+
+
+def get_step_from_name(step_name: str, params: Dict) -> Step:
+ try:
+ return step_name_to_step_class[step_name](**params)
+ except:
+ print(
+ f"Incorrect parameters for step {step_name}. Parameters provided were: {params}")
+ raise
diff --git a/continuedev/src/continuedev/models/filesystem_edit.py b/continuedev/src/continuedev/models/filesystem_edit.py
index 8e74b819..b06ca2b3 100644
--- a/continuedev/src/continuedev/models/filesystem_edit.py
+++ b/continuedev/src/continuedev/models/filesystem_edit.py
@@ -30,8 +30,8 @@ class FileEdit(AtomicFileSystemEdit):
return FileEdit(map_path(self.filepath, orig_root, copy_root), self.range, self.replacement)
@staticmethod
- def from_deletion(filepath: str, start: Position, end: Position) -> "FileEdit":
- return FileEdit(filepath, Range(start, end), "")
+ def from_deletion(filepath: str, range: Range) -> "FileEdit":
+ return FileEdit(filepath=filepath, range=range, replacement="")
@staticmethod
def from_insertion(filepath: str, position: Position, content: str) -> "FileEdit":
diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py
index 818168ba..92bddc98 100644
--- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py
+++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py
@@ -1,7 +1,7 @@
from textwrap import dedent
-from ...core.main import Step
from ...core.sdk import ContinueSDK
+from ...core.main import Step
from ...steps.core.core import WaitForUserInputStep
from ...steps.core.core import MessageStep
from .steps import SetupPipelineStep, ValidatePipelineStep, RunQueryStep
diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
index e59cc51c..ea4607da 100644
--- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
+++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
@@ -6,11 +6,10 @@ import time
from ...models.main import Range
from ...models.filesystem import RangeInFile
from ...steps.core.core import MessageStep
-from ...core.sdk import Models
from ...core.observation import DictObservation, InternalErrorObservation
from ...models.filesystem_edit import AddFile, FileEdit
from ...core.main import Step
-from ...core.sdk import ContinueSDK
+from ...core.sdk import ContinueSDK, Models
AI_ASSISTED_STRING = "(✨ AI-Assisted ✨)"
diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py
index b873a88f..cf046734 100644
--- a/continuedev/src/continuedev/server/gui.py
+++ b/continuedev/src/continuedev/server/gui.py
@@ -77,6 +77,10 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
self.on_reverse_to_index(data["index"])
elif message_type == "retry_at_index":
self.on_retry_at_index(data["index"])
+ elif message_type == "clear_history":
+ self.on_clear_history()
+ elif message_type == "delete_at_index":
+ self.on_delete_at_index(data["index"])
except Exception as e:
print(e)
@@ -86,6 +90,12 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
"state": state
})
+ async def send_available_slash_commands(self):
+ commands = await self.session.autopilot.get_available_slash_commands()
+ await self._send_json("available_slash_commands", {
+ "commands": commands
+ })
+
def on_main_input(self, input: str):
# Do something with user input
asyncio.create_task(self.session.autopilot.accept_user_input(input))
@@ -106,6 +116,12 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
asyncio.create_task(
self.session.autopilot.retry_at_index(index))
+ def on_clear_history(self):
+ asyncio.create_task(self.session.autopilot.clear_history())
+
+ def on_delete_at_index(self, index: int):
+ asyncio.create_task(self.session.autopilot.delete_at_index(index))
+
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(websocket_session)):
@@ -117,6 +133,7 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we
protocol.websocket = websocket
# Update any history that may have happened before connection
+ await protocol.send_available_slash_commands()
await protocol.send_state_update()
while AppStatus.should_exit is False:
diff --git a/continuedev/src/continuedev/server/gui_protocol.py b/continuedev/src/continuedev/server/gui_protocol.py
index 287f9e3b..d9506c6f 100644
--- a/continuedev/src/continuedev/server/gui_protocol.py
+++ b/continuedev/src/continuedev/server/gui_protocol.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Dict, List
from abc import ABC, abstractmethod
@@ -28,5 +28,17 @@ class AbstractGUIProtocolServer(ABC):
"""Send a state update to the client"""
@abstractmethod
+ async def send_available_slash_commands(self, commands: List[Dict]):
+ """Send a list of available slash commands to the client"""
+
+ @abstractmethod
def on_retry_at_index(self, index: int):
"""Called when the user requests a retry at a previous index"""
+
+ @abstractmethod
+ def on_clear_history(self):
+ """Called when the user requests to clear the history"""
+
+ @abstractmethod
+ def on_delete_at_index(self, index: int):
+ """Called when the user requests to delete a step at a given index"""
diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py
index 0dbfaf38..ebea08a5 100644
--- a/continuedev/src/continuedev/server/session_manager.py
+++ b/continuedev/src/continuedev/server/session_manager.py
@@ -28,6 +28,7 @@ class DemoAutopilot(Autopilot):
cumulative_edit_string = ""
def handle_manual_edits(self, edits: List[FileEditWithFullContents]):
+ return
for edit in edits:
self.cumulative_edit_string += edit.fileEdit.replacement
self._manual_edits_buffer.append(edit)
diff --git a/continuedev/src/continuedev/server/state_manager.py b/continuedev/src/continuedev/server/state_manager.py
new file mode 100644
index 00000000..c9bd760b
--- /dev/null
+++ b/continuedev/src/continuedev/server/state_manager.py
@@ -0,0 +1,21 @@
+from typing import Any, List, Tuple, Union
+from fastapi import WebSocket
+from pydantic import BaseModel
+from ..core.main import FullState
+
+# State updates represented as (path, replacement) pairs
+StateUpdate = Tuple[List[Union[str, int]], Any]
+
+
+class StateManager:
+ """
+ A class that acts as the source of truth for state, ingesting changes to the entire object and streaming only the updated portions to client.
+ """
+
+ def __init__(self, ws: WebSocket):
+ self.ws = ws
+
+ def _send_update(self, updates: List[StateUpdate]):
+ self.ws.send_json(
+ [update.dict() for update in updates]
+ )
diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py
new file mode 100644
index 00000000..aadcfa8e
--- /dev/null
+++ b/continuedev/src/continuedev/steps/chat.py
@@ -0,0 +1,19 @@
+from textwrap import dedent
+from typing import List
+from ..core.main import Step
+from ..core.sdk import ContinueSDK
+from .core.core import MessageStep
+
+
+class SimpleChatStep(Step):
+ user_input: str
+ name: str = "Chat"
+
+ async def run(self, sdk: ContinueSDK):
+ self.description = ""
+ for chunk in sdk.models.gpt35.stream_chat(self.user_input, with_history=await sdk.get_chat_context()):
+ self.description += chunk
+ await sdk.update_ui()
+
+ self.name = sdk.models.gpt35.complete(
+ f"Write a short title for the following chat message: {self.description}").strip()
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index 392339c6..8dc2478b 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -3,8 +3,10 @@ import os
import subprocess
from textwrap import dedent
from typing import Coroutine, List, Union
-from ...libs.llm.prompt_utils import MarkdownStyleEncoderDecoder
+from ...models.main import Range
+from ...libs.util.calculate_diff import calculate_diff2, apply_edit_to_str
+from ...libs.llm.prompt_utils import MarkdownStyleEncoderDecoder
from ...models.filesystem_edit import EditDiff, FileEdit, FileEditWithFullContents, FileSystemEdit
from ...models.filesystem import FileSystem, RangeInFile, RangeInFileWithContents
from ...core.observation import Observation, TextObservation, TracebackObservation, UserInputObservation
@@ -85,7 +87,7 @@ class ShellCommandsStep(Step):
{output}
```
- This is a brief summary of the error followed by a suggestion on how it can be fixed:"""), with_history=sdk.chat_context)
+ This is a brief summary of the error followed by a suggestion on how it can be fixed:"""), with_history=await sdk.get_chat_context())
sdk.raise_exception(
title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=f"{suggestion}\n\nYou can click the retry button on the failed step to try again.")
@@ -149,7 +151,11 @@ class Gpt35EditCodeStep(Step):
_prompt_and_completion: str = ""
async def describe(self, models: Models) -> Coroutine[str, None, None]:
- return models.gpt35.complete(f"{self._prompt_and_completion}\n\nPlease give brief a description of the changes made above using markdown bullet points:")
+ description = models.gpt35.complete(
+ f"{self._prompt_and_completion}\n\nPlease give brief a description of the changes made above using markdown bullet points. Be concise and only mention changes made to the commit before, not prefix or suffix:")
+ self.name = models.gpt35.complete(
+ f"Write a short title for this description: {description}")
+ return description
async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:
rif_with_contents = []
@@ -174,11 +180,40 @@ class Gpt35EditCodeStep(Step):
self._prompt_and_completion += prompt + completion
- await sdk.ide.applyFileSystemEdit(
- FileEdit(filepath=rif.filepath, range=rif.range, replacement=completion))
- await sdk.ide.saveFile(rif.filepath)
+ # Calculate diff, open file, apply edits, and highlight changed lines
+ edits = calculate_diff2(
+ rif.filepath, rif.contents, completion.removesuffix("\n"))
+
await sdk.ide.setFileOpen(rif.filepath)
+ lines_to_highlight = set()
+ for edit in edits:
+ edit.range.start.line += rif.range.start.line
+ edit.range.start.character += rif.range.start.character
+ edit.range.end.line += rif.range.start.line
+ edit.range.end.character += rif.range.start.character if edit.range.end.line == 0 else 0
+
+ for line in range(edit.range.start.line, edit.range.end.line + 1 + len(edit.replacement.splitlines()) - (edit.range.end.line - edit.range.start.line + 1)):
+ lines_to_highlight.add(line)
+
+ await sdk.ide.applyFileSystemEdit(edit)
+
+ current_start = None
+ last_line = None
+ for line in sorted(list(lines_to_highlight)):
+ if current_start is None:
+ current_start = line
+ elif line != last_line + 1:
+ await sdk.ide.highlightCode(RangeInFile(filepath=edit.filepath, range=Range.from_shorthand(current_start, 0, last_line, 0)))
+ current_start = line
+
+ last_line = line
+
+ if current_start is not None:
+ await sdk.ide.highlightCode(RangeInFile(filepath=edit.filepath, range=Range.from_shorthand(current_start, 0, last_line, 0)))
+
+ await sdk.ide.saveFile(rif.filepath)
+
class EditFileStep(Step):
filepath: str
diff --git a/continuedev/src/continuedev/steps/draft/migration.py b/continuedev/src/continuedev/steps/draft/migration.py
index f3b36b5e..7c4b7eb5 100644
--- a/continuedev/src/continuedev/steps/draft/migration.py
+++ b/continuedev/src/continuedev/steps/draft/migration.py
@@ -13,7 +13,7 @@ class MigrationStep(Step):
recent_edits = await sdk.ide.get_recent_edits(self.edited_file)
recent_edits_string = "\n\n".join(
map(lambda x: x.to_string(), recent_edits))
- description = await sdk.models.gpt35.complete(f"{recent_edits_string}\n\nGenerate a short description of the migration made in the above changes:\n")
+ description = sdk.models.gpt35.complete(f"{recent_edits_string}\n\nGenerate a short description of the migration made in the above changes:\n")
await sdk.run([
"cd libs",
"poetry run alembic revision --autogenerate -m " + description,
diff --git a/continuedev/src/continuedev/steps/find_and_replace.py b/continuedev/src/continuedev/steps/find_and_replace.py
index fec33997..690872c0 100644
--- a/continuedev/src/continuedev/steps/find_and_replace.py
+++ b/continuedev/src/continuedev/steps/find_and_replace.py
@@ -10,7 +10,7 @@ class FindAndReplaceStep(Step):
replacement: str
async def describe(self, models: Models):
- return f"Replace all instances of `{self.pattern}` with `{self.replacement}` in `{self.filepath}`"
+ return f"Replaced all instances of `{self.pattern}` with `{self.replacement}` in `{self.filepath}`"
async def run(self, sdk: ContinueSDK):
file_content = await sdk.ide.readFile(self.filepath)
diff --git a/continuedev/src/continuedev/steps/input/nl_multiselect.py b/continuedev/src/continuedev/steps/input/nl_multiselect.py
index c3c832f5..36c489c7 100644
--- a/continuedev/src/continuedev/steps/input/nl_multiselect.py
+++ b/continuedev/src/continuedev/steps/input/nl_multiselect.py
@@ -23,5 +23,6 @@ class NLMultiselectStep(Step):
if first_try is not None:
return first_try
- gpt_parsed = await sdk.models.gpt35.complete(f"These are the available options are: [{', '.join(self.options)}]. The user requested {user_response}. This is the exact string from the options array that they selected:")
+ gpt_parsed = sdk.models.gpt35.complete(
+ f"These are the available options are: [{', '.join(self.options)}]. The user requested {user_response}. This is the exact string from the options array that they selected:")
return extract_option(gpt_parsed) or self.options[0]
diff --git a/continuedev/src/continuedev/steps/main.py b/continuedev/src/continuedev/steps/main.py
index 24335b4f..36e4f519 100644
--- a/continuedev/src/continuedev/steps/main.py
+++ b/continuedev/src/continuedev/steps/main.py
@@ -16,6 +16,7 @@ from ..core.sdk import ContinueSDK, Models
from ..core.observation import Observation
import subprocess
from .core.core import Gpt35EditCodeStep
+from ..libs.util.calculate_diff import calculate_diff2
class SetupContinueWorkspaceStep(Step):
@@ -62,10 +63,10 @@ class RunPolicyUntilDoneStep(Step):
policy: "Policy"
async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:
- next_step = self.policy.next(sdk.history)
+ next_step = self.policy.next(sdk.config, sdk.history)
while next_step is not None:
observation = await sdk.run_step(next_step)
- next_step = self.policy.next(sdk.history)
+ next_step = self.policy.next(sdk.config, sdk.history)
return observation
@@ -216,7 +217,8 @@ class StarCoderEditHighlightedCodeStep(Step):
async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:
range_in_files = await sdk.ide.getHighlightedCode()
- if len(range_in_files) == 0:
+ found_highlighted_code = len(range_in_files) > 0
+ if not found_highlighted_code:
# Get the full contents of all open files
files = await sdk.ide.getOpenFiles()
contents = {}
@@ -239,15 +241,29 @@ class StarCoderEditHighlightedCodeStep(Step):
for rif in rif_with_contents:
prompt = self._prompt.format(
code=rif.contents, user_request=self.user_input)
- completion = str(sdk.models.starcoder.complete(prompt))
+
+ if found_highlighted_code:
+ full_file_contents = await sdk.ide.readFile(rif.filepath)
+ segs = full_file_contents.split(rif.contents)
+ prompt = f"<file_prefix>{segs[0]}<file_suffix>{segs[1]}" + prompt
+
+ completion = str((await sdk.models.starcoder()).complete(prompt))
eot_token = "<|endoftext|>"
- if completion.endswith(eot_token):
- completion = completion[:completion.rindex(eot_token)]
+ completion = completion.removesuffix(eot_token)
+
+ if found_highlighted_code:
+ rif.contents = segs[0] + rif.contents + segs[1]
+ completion = segs[0] + completion + segs[1]
self._prompt_and_completion += prompt + completion
- await sdk.ide.applyFileSystemEdit(
- FileEdit(filepath=rif.filepath, range=rif.range, replacement=completion))
+ edits = calculate_diff2(
+ rif.filepath, rif.contents, completion.removesuffix("\n"))
+ for edit in edits:
+ await sdk.ide.applyFileSystemEdit(edit)
+
+ # await sdk.ide.applyFileSystemEdit(
+ # FileEdit(filepath=rif.filepath, range=rif.range, replacement=completion))
await sdk.ide.saveFile(rif.filepath)
await sdk.ide.setFileOpen(rif.filepath)
diff --git a/continuedev/src/continuedev/steps/react.py b/continuedev/src/continuedev/steps/react.py
new file mode 100644
index 00000000..d825d424
--- /dev/null
+++ b/continuedev/src/continuedev/steps/react.py
@@ -0,0 +1,39 @@
+from textwrap import dedent
+from typing import List, Union, Tuple
+from ..core.main import Step
+from ..core.sdk import ContinueSDK
+from .core.core import MessageStep
+
+
+class NLDecisionStep(Step):
+ user_input: str
+ default_step: Union[Step, None] = None
+ steps: List[Tuple[Step, str]]
+
+ hide: bool = True
+
+ async def run(self, sdk: ContinueSDK):
+ step_descriptions = "\n".join([
+ f"- {step[0].name}: {step[1]}"
+ for step in self.steps
+ ])
+ prompt = dedent(f"""\
+ The following steps are available, in the format "- [step name]: [step description]":
+ {step_descriptions}
+
+ The user gave the following input:
+
+ {self.user_input}
+
+ Select the step which should be taken next to satisfy the user input. Say only the name of the selected step. You must choose one:""")
+
+ resp = sdk.models.gpt35.complete(prompt).lower()
+
+ step_to_run = None
+ for step in self.steps:
+ if step[0].name.lower() in resp:
+ step_to_run = step[0]
+
+ step_to_run = step_to_run or self.default_step or self.steps[0]
+
+ await sdk.run_step(step_to_run)
diff --git a/continuedev/src/continuedev/steps/steps_on_startup.py b/continuedev/src/continuedev/steps/steps_on_startup.py
index eae8b558..365cbe1a 100644
--- a/continuedev/src/continuedev/steps/steps_on_startup.py
+++ b/continuedev/src/continuedev/steps/steps_on_startup.py
@@ -1,19 +1,12 @@
-from ..core.main import ContinueSDK, Models, Step
+from ..core.main import Step
+from ..core.sdk import Models, ContinueSDK
from .main import UserInputStep
from ..recipes.CreatePipelineRecipe.main import CreatePipelineRecipe
from ..recipes.DDtoBQRecipe.main import DDtoBQRecipe
from ..recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe
from ..recipes.DDtoBQRecipe.main import DDtoBQRecipe
from ..recipes.AddTransformRecipe.main import AddTransformRecipe
-
-step_name_to_step_class = {
- "UserInputStep": UserInputStep,
- "CreatePipelineRecipe": CreatePipelineRecipe,
- "DDtoBQRecipe": DDtoBQRecipe,
- "DeployPipelineAirflowRecipe": DeployPipelineAirflowRecipe,
- "AddTransformRecipe": AddTransformRecipe,
- "DDtoBQRecipe": DDtoBQRecipe
-}
+from ..libs.util.step_name_to_steps import get_step_from_name
class StepsOnStartupStep(Step):
@@ -23,13 +16,8 @@ class StepsOnStartupStep(Step):
return "Running steps on startup"
async def run(self, sdk: ContinueSDK):
- steps_descriptions = (await sdk.get_config()).steps_on_startup
+ steps_on_startup = sdk.config.steps_on_startup
- for step_name, step_params in steps_descriptions.items():
- try:
- step = step_name_to_step_class[step_name](**step_params)
- except:
- print(
- f"Incorrect parameters for step {step_name}. Parameters provided were: {step_params}")
- continue
+ for step_name, step_params in steps_on_startup.items():
+ step = get_step_from_name(step_name, step_params)
await sdk.run_step(step)
diff --git a/docs/docs/concepts/sdk.md b/docs/docs/concepts/sdk.md
index 30da2e79..21190aa8 100644
--- a/docs/docs/concepts/sdk.md
+++ b/docs/docs/concepts/sdk.md
@@ -23,7 +23,7 @@ The **Continue SDK** gives you all the tools you need to automate software devel
### `sdk.models`
-`sdk.models` is an instance of the `Models` class, containing many of the most commonly used LLMs or other foundation models. You can instantiate a model (starcoder for example) (this is too awkward rn, I know) by calling `starcoder = await sdk.models.starcoder()`. Right now, all of the models are `LLM`s, meaning that they offer the `complete` method, used like `bubble_sort_code = await starcoder.complete("# Write a bubble sort function below, in Python:\n")`.
+`sdk.models` is an instance of the `Models` class, containing many of the most commonly used LLMs or other foundation models. You can access a model (starcoder for example) like `starcoder = sdk.models.starcoder`. Right now, all of the models are `LLM`s, meaning that they offer the `complete` method, used like `bubble_sort_code = await starcoder.complete("# Write a bubble sort function below, in Python:\n")`.
### `sdk.history`
diff --git a/extension/package-lock.json b/extension/package-lock.json
index aebd0803..b02c4544 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "continue",
- "version": "0.0.34",
+ "version": "0.0.35",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "continue",
- "version": "0.0.34",
+ "version": "0.0.35",
"license": "Apache-2.0",
"dependencies": {
"@electron/rebuild": "^3.2.10",
@@ -15,6 +15,7 @@
"@styled-icons/heroicons-outline": "^10.47.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"axios": "^1.2.5",
+ "downshift": "^7.6.0",
"highlight.js": "^11.7.0",
"posthog-js": "^1.63.3",
"react-markdown": "^8.0.7",
@@ -2845,6 +2846,11 @@
"integrity": "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==",
"dev": true
},
+ "node_modules/compute-scroll-into-view": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+ "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3261,6 +3267,31 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/downshift": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
+ "integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.14.8",
+ "compute-scroll-into-view": "^2.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.12.0"
+ }
+ },
+ "node_modules/downshift/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ },
+ "node_modules/downshift/node_modules/tslib": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+ "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
+ },
"node_modules/dset": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz",
@@ -10615,6 +10646,11 @@
"integrity": "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==",
"dev": true
},
+ "compute-scroll-into-view": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+ "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -10911,6 +10947,30 @@
"domhandler": "^5.0.1"
}
},
+ "downshift": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
+ "integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
+ "requires": {
+ "@babel/runtime": "^7.14.8",
+ "compute-scroll-into-view": "^2.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "tslib": "^2.3.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ },
+ "tslib": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+ "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
+ }
+ }
+ },
"dset": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz",
diff --git a/extension/package.json b/extension/package.json
index 1d2fd995..8ccb4b13 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -14,7 +14,7 @@
"displayName": "Continue",
"pricing": "Free",
"description": "Refine code 10x faster",
- "version": "0.0.34",
+ "version": "0.0.35",
"publisher": "Continue",
"engines": {
"vscode": "^1.74.0"
@@ -55,93 +55,13 @@
},
"commands": [
{
- "command": "continue.writeDocstring",
- "category": "Continue",
- "title": "Write a docstring for the current function"
- },
- {
"command": "continue.openContinueGUI",
"category": "Continue",
"title": "Open Continue GUI"
- },
- {
- "command": "continue.askQuestionFromInput",
- "Category": "Continue",
- "title": "Ask a question from input box"
- },
- {
- "command": "continue.openCapturedTerminal",
- "Category": "Continue",
- "title": "Open Captured Terminal"
- },
- {
- "command": "continue.askQuestion",
- "Category": "Continue",
- "title": "Ask a question from webview"
- },
- {
- "command": "continue.createTerminal",
- "category": "Continue",
- "title": "Create Terminal"
- },
- {
- "command": "continue.debugTest",
- "category": "Continue",
- "title": "Debug Test"
- },
- {
- "command": "continue.suggestionDown",
- "category": "Continue",
- "title": "Suggestion Down"
- },
- {
- "command": "continue.suggestionUp",
- "category": "Continue",
- "title": "Suggestion Up"
- },
- {
- "command": "continue.acceptSuggestion",
- "category": "Continue",
- "title": "Accept Suggestion"
- },
- {
- "command": "continue.rejectSuggestion",
- "category": "Continue",
- "title": "Reject Suggestion"
- },
- {
- "command": "continue.writeUnitTest",
- "title": "Write Unit Test",
- "category": "Continue"
- },
- {
- "command": "continue.findSuspiciousCode",
- "title": "Find Suspicious Code",
- "category": "Continue"
- },
- {
- "command": "continue.focusContinueInput",
- "title": "Focus Continue Input",
- "category": "Continue"
}
],
"keybindings": [
{
- "command": "continue.suggestionDown",
- "mac": "shift+ctrl+down",
- "key": "shift+ctrl+down"
- },
- {
- "command": "continue.suggestionUp",
- "mac": "shift+ctrl+up",
- "key": "shift+ctrl+up"
- },
- {
- "command": "continue.acceptSuggestion",
- "mac": "shift+ctrl+enter",
- "key": "shift+ctrl+enter"
- },
- {
"command": "continue.focusContinueInput",
"mac": "cmd+k",
"key": "ctrl+k"
@@ -214,6 +134,7 @@
"@styled-icons/heroicons-outline": "^10.47.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"axios": "^1.2.5",
+ "downshift": "^7.6.0",
"highlight.js": "^11.7.0",
"posthog-js": "^1.63.3",
"react-markdown": "^8.0.7",
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index dbcbc5cc..64440da6 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@styled-icons/heroicons-outline": "^10.47.0",
"@types/vscode-webview": "^1.57.1",
+ "downshift": "^7.6.0",
"posthog-js": "^1.58.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -1288,6 +1289,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/compute-scroll-into-view": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+ "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+ },
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -1405,6 +1411,26 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
+ "node_modules/downshift": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
+ "integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
+ "dependencies": {
+ "@babel/runtime": "^7.14.8",
+ "compute-scroll-into-view": "^2.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.12.0"
+ }
+ },
+ "node_modules/downshift/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ },
"node_modules/electron-to-chromium": {
"version": "1.4.311",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz",
@@ -3003,6 +3029,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/tslib": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+ "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
+ },
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
@@ -4050,6 +4081,11 @@
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="
},
+ "compute-scroll-into-view": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
+ "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
+ },
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -4131,6 +4167,25 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
+ "downshift": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
+ "integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
+ "requires": {
+ "@babel/runtime": "^7.14.8",
+ "compute-scroll-into-view": "^2.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^17.0.2",
+ "tslib": "^2.3.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+ }
+ }
+ },
"electron-to-chromium": {
"version": "1.4.311",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.311.tgz",
@@ -5138,6 +5193,11 @@
"resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
"integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g=="
},
+ "tslib": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+ "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
+ },
"typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index 7d1211de..a53fbec8 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@styled-icons/heroicons-outline": "^10.47.0",
"@types/vscode-webview": "^1.57.1",
+ "downshift": "^7.6.0",
"posthog-js": "^1.58.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx
index e0336554..eedae3fb 100644
--- a/extension/react-app/src/components/CodeBlock.tsx
+++ b/extension/react-app/src/components/CodeBlock.tsx
@@ -11,6 +11,7 @@ const StyledPre = styled.pre`
border: 1px solid gray;
border-radius: ${defaultBorderRadius};
background-color: ${vscBackground};
+ padding: 8px;
`;
const StyledCode = styled.code`
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
new file mode 100644
index 00000000..1b7c60e6
--- /dev/null
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -0,0 +1,146 @@
+import React, { useCallback } from "react";
+import { useCombobox } from "downshift";
+import styled from "styled-components";
+import {
+ buttonColor,
+ defaultBorderRadius,
+ secondaryDark,
+ vscBackground,
+} from ".";
+
+const mainInputFontSize = 16;
+const MainTextInput = styled.input`
+ padding: 8px;
+ font-size: ${mainInputFontSize}px;
+ border-radius: ${defaultBorderRadius};
+ border: 1px solid #ccc;
+ margin: 8px auto;
+ width: 100%;
+ background-color: ${vscBackground};
+ color: white;
+ outline: 1px solid orange;
+`;
+
+const UlMaxHeight = 200;
+const Ul = styled.ul<{
+ hidden: boolean;
+ showAbove: boolean;
+ ulHeightPixels: number;
+}>`
+ ${(props) =>
+ props.showAbove
+ ? `transform: translateY(-${props.ulHeightPixels + 8}px);`
+ : `transform: translateY(${2 * mainInputFontSize}px);`}
+ position: absolute;
+ background: ${vscBackground};
+ background-color: ${secondaryDark};
+ color: white;
+ font-family: "Fira Code", monospace;
+ max-height: ${UlMaxHeight}px;
+ overflow: scroll;
+ padding: 0;
+ ${({ hidden }) => hidden && "display: none;"}
+ border-radius: ${defaultBorderRadius};
+ overflow: hidden;
+ border: 0.5px solid gray;
+`;
+
+const Li = styled.li<{
+ highlighted: boolean;
+ selected: boolean;
+ isLastItem: boolean;
+}>`
+ ${({ highlighted }) => highlighted && "background: #aa0000;"}
+ ${({ selected }) => selected && "font-weight: bold;"}
+ padding: 0.5rem 0.75rem;
+ display: flex;
+ flex-direction: column;
+ ${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
+ border-top: 1px solid gray;
+ cursor: pointer;
+`;
+
+interface ComboBoxProps {
+ items: { name: string; description: string }[];
+ onInputValueChange: (inputValue: string) => void;
+ disabled?: boolean;
+ onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
+}
+
+const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+ const [items, setItems] = React.useState(props.items);
+ const {
+ isOpen,
+ getToggleButtonProps,
+ getLabelProps,
+ getMenuProps,
+ getInputProps,
+ highlightedIndex,
+ getItemProps,
+ selectedItem,
+ setInputValue,
+ } = useCombobox({
+ onInputValueChange({ inputValue }) {
+ if (!inputValue) return;
+ props.onInputValueChange(inputValue);
+ setItems(
+ props.items.filter((item) =>
+ item.name.toLowerCase().startsWith(inputValue.toLowerCase())
+ )
+ );
+ },
+ items,
+ itemToString(item) {
+ return item ? item.name : "";
+ },
+ });
+
+ const divRef = React.useRef<HTMLDivElement>(null);
+ const ulRef = React.useRef<HTMLUListElement>(null);
+ const showAbove = () => {
+ return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight;
+ };
+
+ return (
+ <div className="flex px-2" ref={divRef} hidden={!isOpen}>
+ <MainTextInput
+ disabled={props.disabled}
+ placeholder="Ask anything:"
+ {...getInputProps({
+ onKeyDown: (event) => {
+ if (event.key === "Enter" && (!isOpen || items.length === 0)) {
+ // Prevent Downshift's default 'Enter' behavior.
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ if (props.onEnter) props.onEnter(event);
+ setInputValue("");
+ }
+ },
+ ref: ref as any,
+ })}
+ />
+ <Ul
+ {...getMenuProps({
+ ref: ulRef,
+ })}
+ showAbove={showAbove()}
+ ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
+ >
+ {isOpen &&
+ items.map((item, index) => (
+ <Li
+ key={`${item.name}${index}`}
+ {...getItemProps({ item, index })}
+ highlighted={highlightedIndex === index}
+ selected={selectedItem === item}
+ >
+ <span>
+ {item.name}: {item.description}
+ </span>
+ </Li>
+ ))}
+ </Ul>
+ </div>
+ );
+});
+
+export default ComboBox;
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 3408053b..ca142b06 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -7,6 +7,7 @@ import {
vscBackground,
GradientBorder,
vscBackgroundTransparent,
+ HeaderButton,
} from ".";
import { RangeInFile, FileEdit } from "../../../src/client";
import CodeBlock from "./CodeBlock";
@@ -15,7 +16,7 @@ import SubContainer from "./SubContainer";
import {
ChevronDown,
ChevronRight,
- Backward,
+ XMark,
ArrowPath,
} from "@styled-icons/heroicons-outline";
import { HistoryNode } from "../../../schema/HistoryNode";
@@ -31,6 +32,7 @@ interface StepContainerProps {
onRefinement: (input: string) => void;
onUserInput: (input: string) => void;
onRetry: () => void;
+ onDelete: () => void;
open?: boolean;
}
@@ -54,8 +56,10 @@ const HeaderDiv = styled.div<{ error: boolean }>`
background-color: ${(props) =>
props.error ? "#522" : vscBackgroundTransparent};
display: grid;
- grid-template-columns: 1fr auto;
+ grid-template-columns: 1fr auto auto;
+ grid-gap: 8px;
align-items: center;
+ padding-right: 8px;
`;
const ContentDiv = styled.div`
@@ -64,26 +68,21 @@ const ContentDiv = styled.div`
background-color: ${vscBackground};
`;
-const HeaderButton = styled.button`
- background-color: transparent;
- border: 1px solid white;
- border-radius: ${defaultBorderRadius};
- padding: 2px;
- cursor: pointer;
- color: white;
-
- &:hover {
- background-color: white;
- color: black;
- }
-`;
-
const OnHoverDiv = styled.div`
text-align: center;
padding: 10px;
animation: ${appear} 0.3s ease-in-out;
`;
+const MarkdownPre = styled.pre`
+ background-color: ${secondaryDark};
+ padding: 10px;
+ border-radius: ${defaultBorderRadius};
+ border: 0.5px solid white;
+`;
+
+const MarkdownCode = styled.code``;
+
function StepContainer(props: StepContainerProps) {
const [open, setOpen] = useState(
typeof props.open === "undefined" ? true : props.open
@@ -152,18 +151,28 @@ function StepContainer(props: StepContainerProps) {
<Backward size="1.6em" onClick={props.onReverse}></Backward>
</HeaderButton> */}
- {props.historyNode.observation?.error ? (
+ <>
<HeaderButton
onClick={(e) => {
e.stopPropagation();
- props.onRetry();
+ props.onDelete();
}}
>
- <ArrowPath size="1.6em" onClick={props.onRetry}></ArrowPath>
+ <XMark size="1.6em" onClick={props.onDelete} />
</HeaderButton>
- ) : (
- <></>
- )}
+ {props.historyNode.observation?.error ? (
+ <HeaderButton
+ onClick={(e) => {
+ e.stopPropagation();
+ props.onRetry();
+ }}
+ >
+ <ArrowPath size="1.6em" onClick={props.onRetry} />
+ </HeaderButton>
+ ) : (
+ <></>
+ )}
+ </>
</HeaderDiv>
</GradientBorder>
<ContentDiv hidden={!open}>
@@ -182,7 +191,19 @@ function StepContainer(props: StepContainerProps) {
{props.historyNode.observation.error as string}
</pre>
) : (
- <ReactMarkdown key={1} className="overflow-scroll">
+ <ReactMarkdown
+ key={1}
+ className="overflow-scroll"
+ components={{
+ pre: ({ node, ...props }) => {
+ return (
+ <CodeBlock
+ children={props.children[0] as string}
+ ></CodeBlock>
+ );
+ },
+ }}
+ >
{props.historyNode.step.description as any}
</ReactMarkdown>
)}
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 4966f3e8..525989af 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -143,3 +143,21 @@ export const appear = keyframes`
transform: translateY(0px);
}
`;
+
+export const HeaderButton = styled.button`
+ background-color: transparent;
+ border: 1px solid white;
+ border-radius: ${defaultBorderRadius};
+ cursor: pointer;
+ color: white;
+
+ &:hover {
+ background-color: white;
+ color: black;
+ }
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 1px;
+`;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 18a91de7..824bb086 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -8,6 +8,16 @@ abstract class AbstractContinueGUIClientProtocol {
abstract sendStepUserInput(input: string, index: number): void;
abstract onStateUpdate(state: any): void;
+
+ abstract onAvailableSlashCommands(
+ callback: (commands: { name: string; description: string }[]) => void
+ ): void;
+
+ abstract sendClear(): void;
+
+ abstract retryAtIndex(index: number): void;
+
+ abstract deleteAtIndex(index: number): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
index f27895fb..59397742 100644
--- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts
+++ b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
@@ -45,9 +45,27 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
});
}
+ onAvailableSlashCommands(
+ callback: (commands: { name: string; description: string }[]) => void
+ ) {
+ this.messenger.onMessageType("available_slash_commands", (data: any) => {
+ if (data.commands) {
+ callback(data.commands);
+ }
+ });
+ }
+
+ 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 });
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/tabs/chat/MessageDiv.tsx b/extension/react-app/src/tabs/chat/MessageDiv.tsx
index 1d7bb5f5..3543dd93 100644
--- a/extension/react-app/src/tabs/chat/MessageDiv.tsx
+++ b/extension/react-app/src/tabs/chat/MessageDiv.tsx
@@ -58,7 +58,9 @@ function MessageDiv(props: ChatMessage) {
}, [richContent, isStreaming]);
useEffect(() => {
- setRichContent([<ReactMarkdown key={1}>{props.content}</ReactMarkdown>]);
+ setRichContent([
+ <ReactMarkdown key={1} children={props.content}></ReactMarkdown>,
+ ]);
}, [props.content]);
return (
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index 5c75579b..a3a48410 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -4,6 +4,7 @@ import {
vscBackground,
Loader,
MainTextInput,
+ HeaderButton,
} from "../components";
import ContinueButton from "../components/ContinueButton";
import { useCallback, useEffect, useRef, useState } from "react";
@@ -11,7 +12,8 @@ import { History } from "../../../schema/History";
import { HistoryNode } from "../../../schema/HistoryNode";
import StepContainer from "../components/StepContainer";
import useContinueGUIProtocol from "../hooks/useWebsocket";
-
+import { BookOpen, Trash } from "@styled-icons/heroicons-outline";
+import ComboBox from "../components/ComboBox";
let TopGUIDiv = styled.div`
display: grid;
grid-template-columns: 1fr;
@@ -26,6 +28,14 @@ let UserInputQueueItem = styled.div`
text-align: center;
`;
+const TopBar = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 8px;
+ align-items: center;
+`;
+
interface GUIProps {
firstObservation?: any;
}
@@ -33,15 +43,18 @@ interface GUIProps {
function GUI(props: GUIProps) {
const [waitingForSteps, setWaitingForSteps] = useState(false);
const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
+ const [availableSlashCommands, setAvailableSlashCommands] = useState<
+ { name: string; description: string }[]
+ >([]);
const [history, setHistory] = useState<History | undefined>();
- // {
+ // {
// timeline: [
// {
// step: {
// name: "Waiting for user input",
// cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py",
// description:
- // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`",
+ // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py` and ```\nprint(sum(first, second))\n```\n- Testing\n- Testing 2\n- Testing 3",
// },
// observation: {
// title: "ERROR FOUND",
@@ -92,7 +105,7 @@ function GUI(props: GUIProps) {
// prompt:
// "I ran into this problem with my Python code:\n\n Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'\n\n Below are the files that might need to be fixed:\n\n {code}\n\n This is what the code should be in order to avoid the problem:\n",
// description:
- // "Editing files: /Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py` and\n```python\nprint(sum(first, second))\n```\n- Testing\n- Testing 2\n- Testing 3",
// },
// output: [
// null,
@@ -154,22 +167,30 @@ function GUI(props: GUIProps) {
// output: [null, null],
// },
// ],
- // current_index: 0,
+ // current_index: 3,
// } as any);
const topGuiDivRef = useRef<HTMLDivElement>(null);
const client = useContinueGUIProtocol();
+ const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
+ null
+ );
const scrollToBottom = useCallback(() => {
+ if (scrollTimeout) {
+ clearTimeout(scrollTimeout);
+ }
+ // Debounced smooth scroll to bottom of screen
if (topGuiDivRef.current) {
- setTimeout(() => {
+ const timeout = setTimeout(() => {
window.scrollTo({
top: window.outerHeight,
behavior: "smooth",
});
- }, 100);
+ }, 200);
+ setScrollTimeout(timeout);
}
- }, [topGuiDivRef.current]);
+ }, [topGuiDivRef.current, scrollTimeout]);
useEffect(() => {
console.log("CLIENT ON STATE UPDATE: ", client, client?.onStateUpdate);
@@ -181,13 +202,24 @@ function GUI(props: GUIProps) {
scrollToBottom();
});
+ client?.onAvailableSlashCommands((commands) => {
+ console.log("Received available slash commands: ", commands);
+ setAvailableSlashCommands(
+ commands.map((c) => {
+ return {
+ name: "/" + c.name,
+ description: c.description,
+ };
+ })
+ );
+ });
}, [client]);
useEffect(() => {
scrollToBottom();
}, [waitingForSteps]);
- const mainTextInputRef = useRef<HTMLTextAreaElement>(null);
+ const mainTextInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (mainTextInputRef.current) {
@@ -230,8 +262,6 @@ function GUI(props: GUIProps) {
return [...queue, input];
});
}
- mainTextInputRef.current.value = "";
- mainTextInputRef.current.style.height = "";
}
setWaitingForSteps(true);
@@ -253,6 +283,24 @@ function GUI(props: GUIProps) {
}
}}
>
+ <TopBar>
+ <a href="https://continue.dev/docs" className="no-underline">
+ <HeaderButton style={{ padding: "3px" }}>
+ Continue Docs
+ <BookOpen size="1.6em" />
+ </HeaderButton>
+ </a>
+ <HeaderButton
+ onClick={() => {
+ client?.sendClear();
+ }}
+ style={{ padding: "3px" }}
+ >
+ Clear History
+ <Trash size="1.6em" />
+ </HeaderButton>
+ </TopBar>
+
{typeof client === "undefined" && (
<>
<Loader></Loader>
@@ -280,6 +328,9 @@ function GUI(props: GUIProps) {
client?.retryAtIndex(index);
setWaitingForSteps(true);
}}
+ onDelete={() => {
+ client?.deleteAtIndex(index);
+ }}
/>
);
})}
@@ -291,7 +342,7 @@ function GUI(props: GUIProps) {
})}
</div>
- <MainTextInput
+ <ComboBox
disabled={
history
? history.timeline[history.current_index].step.name ===
@@ -299,22 +350,13 @@ function GUI(props: GUIProps) {
: false
}
ref={mainTextInputRef}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- onMainTextInput();
- e.stopPropagation();
- e.preventDefault();
- }
- }}
- rows={1}
- onChange={() => {
- const textarea = mainTextInputRef.current!;
- textarea.style.height = ""; /* Reset the height*/
- textarea.style.height = `${Math.min(
- textarea.scrollHeight - 15,
- 500
- )}px`;
+ onEnter={(e) => {
+ onMainTextInput();
+ e.stopPropagation();
+ e.preventDefault();
}}
+ onInputValueChange={() => {}}
+ items={availableSlashCommands}
/>
<ContinueButton onClick={onMainTextInput} />
</TopGUIDiv>
diff --git a/extension/scripts/continuedev-0.1.1-py3-none-any.whl b/extension/scripts/continuedev-0.1.1-py3-none-any.whl
index 42f3d4a3..3d7639a9 100644
--- a/extension/scripts/continuedev-0.1.1-py3-none-any.whl
+++ b/extension/scripts/continuedev-0.1.1-py3-none-any.whl
Binary files differ
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index 42671ade..92af6b10 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -13,6 +13,7 @@ import { FileEditWithFullContents } from "../schema/FileEditWithFullContents";
import fs = require("fs");
import { WebsocketMessenger } from "./util/messenger";
import { CapturedTerminal } from "./terminal/terminalEmulator";
+import { decorationManager } from "./decorations";
class IdeProtocolClient {
private messenger: WebsocketMessenger | null = null;
@@ -277,12 +278,13 @@ class IdeProtocolClient {
undefined,
vscode.ViewColumn.One
).then((editor) => {
- let range = new vscode.Range(
+ const range = new vscode.Range(
edit.range.start.line,
edit.range.start.character,
edit.range.end.line,
- edit.range.end.character + 1
+ edit.range.end.character
);
+
editor.edit((editBuilder) => {
this._makingEdit += 2; // editBuilder.replace takes 2 edits: delete and insert
editBuilder.replace(range, edit.replacement);
diff --git a/extension/src/decorations.ts b/extension/src/decorations.ts
index 456f0c10..d2c94135 100644
--- a/extension/src/decorations.ts
+++ b/extension/src/decorations.ts
@@ -94,15 +94,22 @@ class DecorationManager {
decorationTypes = new Map();
decorationTypes.set(key.decorationType, [key.options]);
this.editorToDecorations.set(key.editorUri, decorationTypes);
- }
-
- const decorations = decorationTypes.get(key.decorationType);
- if (!decorations) {
- decorationTypes.set(key.decorationType, [key.options]);
} else {
- decorations.push(key.options);
+ const decorations = decorationTypes.get(key.decorationType);
+ if (!decorations) {
+ decorationTypes.set(key.decorationType, [key.options]);
+ } else {
+ decorations.push(key.options);
+ }
}
+
this.rerenderDecorations(key.editorUri, key.decorationType);
+
+ vscode.window.onDidChangeTextEditorSelection((event) => {
+ if (event.textEditor.document.fileName === key.editorUri) {
+ this.deleteAllDecorations(key.editorUri);
+ }
+ });
}
deleteDecoration(key: DecorationKey) {