diff options
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 Binary files differindex 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 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) { |