diff options
| -rw-r--r-- | continuedev/src/continuedev/core/autopilot.py | 34 | ||||
| -rw-r--r-- | continuedev/src/continuedev/core/main.py | 30 | ||||
| -rw-r--r-- | continuedev/src/continuedev/core/policy.py | 7 | ||||
| -rw-r--r-- | continuedev/src/continuedev/core/sdk.py | 21 | ||||
| -rw-r--r-- | continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py | 2 | ||||
| -rw-r--r-- | continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py | 62 | ||||
| -rw-r--r-- | continuedev/src/continuedev/server/ide.py | 11 | ||||
| -rw-r--r-- | continuedev/src/continuedev/server/ide_protocol.py | 8 | ||||
| -rw-r--r-- | continuedev/src/continuedev/steps/core/core.py | 27 | ||||
| -rw-r--r-- | extension/package.json | 4 | ||||
| -rw-r--r-- | extension/react-app/src/components/DebugPanel.tsx | 15 | ||||
| -rw-r--r-- | extension/react-app/src/components/StepContainer.tsx | 52 | ||||
| -rw-r--r-- | extension/react-app/src/components/index.ts | 11 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/chat/MessageDiv.tsx | 1 | ||||
| -rw-r--r-- | extension/react-app/src/tabs/gui.tsx | 74 | ||||
| -rw-r--r-- | extension/src/continueIdeClient.ts | 39 | 
16 files changed, 286 insertions, 112 deletions
| diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index db06c975..d55e521b 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -1,13 +1,13 @@  import traceback  import time -from typing import Callable, Coroutine, List +from typing import Any, Callable, Coroutine, Dict, List  from ..models.filesystem_edit import FileEditWithFullContents  from ..libs.llm import LLM  from .observation import Observation, InternalErrorObservation  from ..server.ide_protocol import AbstractIdeProtocolServer  from ..libs.util.queue import AsyncSubscriptionQueue  from ..models.main import ContinueBaseModel -from .main import Policy, History, FullState, Step, HistoryNode +from .main import Context, ContinueCustomException, Policy, History, FullState, Step, HistoryNode  from ..steps.core.core import ReversibleStep, ManualEditStep, UserInputStep  from ..libs.util.telemetry import capture_event  from .sdk import ContinueSDK @@ -18,6 +18,7 @@ class Autopilot(ContinueBaseModel):      policy: Policy      ide: AbstractIdeProtocolServer      history: History = History.from_empty() +    context: Context = Context()      _on_update_callbacks: List[Callable[[FullState], None]] = []      _active: bool = False @@ -26,6 +27,10 @@ class Autopilot(ContinueBaseModel):      _user_input_queue = AsyncSubscriptionQueue() +    @property +    def continue_sdk(self) -> ContinueSDK: +        return ContinueSDK(self) +      class Config:          arbitrary_types_allowed = True @@ -60,7 +65,7 @@ class Autopilot(ContinueBaseModel):                  current_step = self.history.get_current().step                  self.history.step_back()                  if issubclass(current_step.__class__, ReversibleStep): -                    await current_step.reverse(ContinueSDK(self)) +                    await current_step.reverse(self.continue_sdk)                  await self.update_subscribers()          except Exception as e: @@ -105,7 +110,16 @@ class Autopilot(ContinueBaseModel):          self._step_depth += 1          try: -            observation = await step(ContinueSDK(self)) +            observation = await step(self.continue_sdk) +        except ContinueCustomException as e: +            # Attach an InternalErrorObservation to the step and unhide it. +            error_string = e.message +            print( +                f"\n{error_string}\n{e}") + +            observation = InternalErrorObservation( +                error=error_string) +            step.hide = False          except Exception as e:              # Attach an InternalErrorObservation to the step and unhide it.              error_string = '\n\n'.join( @@ -125,11 +139,13 @@ class Autopilot(ContinueBaseModel):          await self.update_subscribers()          # Update its description -        async def update_description(): -            step._set_description(await step.describe(ContinueSDK(self).models)) -            # Update subscribers with new description -            await self.update_subscribers() -        asyncio.create_task(update_description()) +        if step.description is None: +            async def update_description(): +                step.description = await step.describe(self.continue_sdk.models) +                # Update subscribers with new description +                await self.update_subscribers() + +            asyncio.create_task(update_description())          return observation diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py index b2b97bae..17b30e96 100644 --- a/continuedev/src/continuedev/core/main.py +++ b/continuedev/src/continuedev/core/main.py @@ -105,7 +105,7 @@ class Policy(ContinueBaseModel):  class Step(ContinueBaseModel):      name: str = None      hide: bool = False -    _description: Union[str, None] = None +    description: Union[str, None] = None      system_message: Union[str, None] = None @@ -113,17 +113,14 @@ class Step(ContinueBaseModel):          copy_on_model_validation = False      async def describe(self, models: Models) -> Coroutine[str, None, None]: -        if self._description is not None: -            return self._description +        if self.description is not None: +            return self.description          return "Running step: " + self.name -    def _set_description(self, description: str): -        self._description = description -      def dict(self, *args, **kwargs):          d = super().dict(*args, **kwargs) -        if self._description is not None: -            d["description"] = self._description +        if self.description is not None: +            d["description"] = self.description          else:              d["description"] = "`Description loading...`"          return d @@ -173,4 +170,21 @@ class Validator(Step):          raise NotImplementedError +class Context: +    key_value: Dict[str, str] = {} + +    def set(self, key: str, value: str): +        self.key_value[key] = value + +    def get(self, key: str) -> str: +        return self.key_value[key] + + +class ContinueCustomException(Exception): +    message: str + +    def __init__(self, message: str): +        self.message = message + +  HistoryNode.update_forward_refs() diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py index e65b6c9d..4934497d 100644 --- a/continuedev/src/continuedev/core/policy.py +++ b/continuedev/src/continuedev/core/policy.py @@ -17,9 +17,10 @@ class DemoPolicy(Policy):      def next(self, history: History) -> Step:          # At the very start, run initial Steps spcecified in the config          if history.get_current() is None: -            return (MessageStep(message="Welcome to Continue!") >> -                    SetupContinueWorkspaceStep() >> -                    CreateCodebaseIndexChroma() >> +            return ( +                    MessageStep(name="Welcome to Continue!", message="") >> +                    # SetupContinueWorkspaceStep() >> +                    # CreateCodebaseIndexChroma() >>                      StepsOnStartupStep())          observation = history.get_current().observation diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py index 8317a3d1..690949f1 100644 --- a/continuedev/src/continuedev/core/sdk.py +++ b/continuedev/src/continuedev/core/sdk.py @@ -11,7 +11,7 @@ from ..libs.llm.hf_inference_api import HuggingFaceInferenceAPI  from ..libs.llm.openai import OpenAI  from .observation import Observation  from ..server.ide_protocol import AbstractIdeProtocolServer -from .main import History, Step +from .main import Context, ContinueCustomException, History, Step  from ..steps.core.core import * @@ -44,6 +44,7 @@ class ContinueSDK(AbstractContinueSDK):      ide: AbstractIdeProtocolServer      steps: ContinueSDKSteps      models: Models +    context: Context      __autopilot: Autopilot      def __init__(self, autopilot: Autopilot): @@ -51,6 +52,7 @@ class ContinueSDK(AbstractContinueSDK):          self.__autopilot = autopilot          self.steps = ContinueSDKSteps(self)          self.models = Models(self) +        self.context = autopilot.context      @property      def history(self) -> History: @@ -64,8 +66,8 @@ class ContinueSDK(AbstractContinueSDK):      async def run_step(self, step: Step) -> Coroutine[Observation, None, None]:          return await self.__autopilot._run_singular_step(step) -    async def apply_filesystem_edit(self, edit: FileSystemEdit): -        return await self.run_step(FileSystemEditStep(edit=edit)) +    async def apply_filesystem_edit(self, edit: FileSystemEdit, name: str = None, description: str = None): +        return await self.run_step(FileSystemEditStep(edit=edit, description=description, **({'name': name} if name else {})))      async def wait_for_user_input(self) -> str:          return await self.__autopilot.wait_for_user_input() @@ -73,18 +75,20 @@ class ContinueSDK(AbstractContinueSDK):      async def wait_for_user_confirmation(self, prompt: str):          return await self.run_step(WaitForUserConfirmationStep(prompt=prompt)) -    async def run(self, commands: Union[List[str], str], cwd: str = None): +    async def run(self, commands: Union[List[str], str], cwd: str = None, name: str = None, description: str = None):          commands = commands if isinstance(commands, List) else [commands] -        return await self.run_step(ShellCommandsStep(cmds=commands, cwd=cwd)) +        return await self.run_step(ShellCommandsStep(cmds=commands, cwd=cwd, description=description, **({'name': name} if name else {}))) -    async def edit_file(self, filename: str, prompt: str): +    async def edit_file(self, filename: str, prompt: str, name: str = None, description: str = None):          filepath = await self._ensure_absolute_path(filename)          await self.ide.setFileOpen(filepath)          contents = await self.ide.readFile(filepath)          await self.run_step(EditCodeStep(              range_in_files=[RangeInFile.from_entire_file(filepath, contents)], -            prompt=f'Here is the code before:\n\n{{code}}\n\nHere is the user request:\n\n{prompt}\n\nHere is the code edited to perfectly solve the user request:\n\n' +            prompt=f'Here is the code before:\n\n{{code}}\n\nHere is the user request:\n\n{prompt}\n\nHere is the code edited to perfectly solve the user request:\n\n', +            description=description, +            **({'name': name} if name else {})          ))      async def append_to_file(self, filename: str, content: str): @@ -126,3 +130,6 @@ class ContinueSDK(AbstractContinueSDK):      def set_loading_message(self, message: str):          # self.__autopilot.set_loading_message(message)          raise NotImplementedError() + +    def raise_exception(self, message: str): +        raise ContinueCustomException(message) diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py index 55c25da4..1206db0e 100644 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py +++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py @@ -12,7 +12,7 @@ class CreatePipelineRecipe(Step):      async def run(self, sdk: ContinueSDK):          text_observation = await sdk.run_step( -            MessageStep(message=dedent("""\ +            MessageStep(name="Building your first dlt pipeline", message=dedent("""\                  This recipe will walk you through the process of creating a dlt pipeline for your chosen data source. With the help of Continue, you will:                  - Create a Python virtual environment with dlt installed                  - Run `dlt init` to generate a pipeline template diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py index ef5e3b43..3c8277c0 100644 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py +++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py @@ -1,8 +1,12 @@ +import os +import subprocess  from textwrap import dedent +from ...models.main import Range +from ...models.filesystem import RangeInFile  from ...steps.main import MessageStep  from ...core.sdk import Models -from ...core.observation import DictObservation +from ...core.observation import DictObservation, InternalErrorObservation  from ...models.filesystem_edit import AddFile  from ...core.main import Step  from ...core.sdk import ContinueSDK @@ -33,45 +37,69 @@ class SetupPipelineStep(Step):              f'dlt init {source_name} duckdb',              'Y',              'pip install -r requirements.txt' -        ]) - +        ], description=dedent(f"""\ +            Running the following commands: +            - `python3 -m venv env`: Create a Python virtual environment +            - `source env/bin/activate`: Activate the virtual environment +            - `pip install dlt`: Install dlt +            - `dlt init {source_name} duckdb`: Create a new dlt pipeline called {source_name} that loads data into a local DuckDB instance +            - `pip install -r requirements.txt`: Install the Python dependencies for the pipeline""")) + +        await sdk.wait_for_user_confirmation("Wait for the commands to finish running, then press `Continue`")          # editing the resource function to call the requested API +        await sdk.ide.highlightCode(RangeInFile(filepath=os.path.join(await sdk.ide.getWorkspaceDirectory(), filename), range=Range.from_shorthand(15, 0, 30, 0)), "#00ff0022") +          await sdk.edit_file(              filename=filename, -            prompt=f'Edit the resource function to call the API described by this: {self.api_description}' +            prompt=f'Edit the resource function to call the API described by this: {self.api_description}', +            name="Edit the resource function to call the API"          )          # wait for user to put API key in secrets.toml          await sdk.ide.setFileOpen(await sdk.ide.getWorkspaceDirectory() + "/.dlt/secrets.toml")          await sdk.wait_for_user_confirmation("If this service requires an API key, please add it to the `secrets.toml` file and then press `Continue`") -        return DictObservation(values={"source_name": source_name}) + +        sdk.context.set("source_name", source_name)  class ValidatePipelineStep(Step):      hide: bool = True      async def run(self, sdk: ContinueSDK): -        source_name = sdk.history.last_observation().values["source_name"] +        workspace_dir = await sdk.ide.getWorkspaceDirectory() +        source_name = sdk.context.get("source_name")          filename = f'{source_name}.py' -        await sdk.run_step(MessageStep(message=dedent("""\ -                This step will validate that your dlt pipeline is working as expected: -                - Test that the API call works -                - Load the data into a local DuckDB instance -                - Write a query to view the data -                """))) +        # await sdk.run_step(MessageStep(name="Validate the pipeline", message=dedent("""\ +        #         Next, we will validate that your dlt pipeline is working as expected: +        #         - Test that the API call works +        #         - Load the data into a local DuckDB instance +        #         - Write a query to view the data +        #         """)))          # test that the API call works -        await sdk.run(f'python3 {filename}') + +        p = subprocess.run( +            ['python3', f'{filename}'], capture_output=True, text=True, cwd=workspace_dir) +        err = p.stderr + +        # If it fails, return the error +        if err is not None and err != "": +            sdk.raise_exception( +                f"Error while running pipeline. Fix the resource function in {filename} and rerun this step: \n\n" + err) + +        await sdk.run(f'python3 {filename}', name="Test the pipeline", description=f"Running python3 {filename} to test loading data from the API")          # remove exit() from the main main function          await sdk.edit_file(              filename=filename, -            prompt='Remove exit() from the main function' +            prompt='Remove exit() from the main function', +            name="Remove early exit() from main function", +            description="Remove the `exit()` call from the main function in the pipeline file so that the data is loaded into DuckDB"          )          # load the data into the DuckDB instance -        await sdk.run(f'python3 {filename}') +        await sdk.run(f'python3 {filename}', name="Load data into DuckDB", description=f"Running python3 {filename} to load data into DuckDB")          table_name = f"{source_name}.{source_name}_resource"          tables_query_code = dedent(f'''\ @@ -89,5 +117,5 @@ class ValidatePipelineStep(Step):          ''')          query_filename = (await sdk.ide.getWorkspaceDirectory()) + "/query.py" -        await sdk.apply_filesystem_edit(AddFile(filepath=query_filename, content=tables_query_code)) -        await sdk.run('env/bin/python3 query.py') +        await sdk.apply_filesystem_edit(AddFile(filepath=query_filename, content=tables_query_code), name="Add query.py file", description="Adding a file called `query.py` to the workspace that will run a test query on the DuckDB instance") +        await sdk.run('env/bin/python3 query.py', name="Run test query", description="Running `env/bin/python3 query.py` to test that the data was loaded into DuckDB as expected") diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index eec5b607..073e1dba 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -133,6 +133,17 @@ class IdeProtocolServer(AbstractIdeProtocolServer):              "sessionId": session_id          }) +    async def highlightCode(self, range_in_file: RangeInFile, color: str): +        await self._send_json("highlightCode", { +            "rangeInFile": range_in_file.dict(), +            "color": color +        }) + +    async def runCommand(self, command: str): +        await self._send_json("runCommand", { +            "command": command +        }) +      async def showSuggestionsAndWait(self, suggestions: List[FileEdit]) -> bool:          ids = [str(uuid.uuid4()) for _ in suggestions]          for i in range(len(suggestions)): diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 8f155415..f42de68f 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -82,3 +82,11 @@ class AbstractIdeProtocolServer(ABC):      @abstractmethod      async def getUserSecret(self, key: str):          """Get a user secret""" + +    @abstractmethod +    async def highlightCode(self, range_in_file: RangeInFile, color: str): +        """Highlight code""" + +    @abstractmethod +    async def runCommand(self, command: str): +        """Run a command""" diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py index ad468595..99786b00 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -55,18 +55,21 @@ class ShellCommandsStep(Step):      async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]:          cwd = await sdk.ide.getWorkspaceDirectory() if self.cwd is None else self.cwd -        process = subprocess.Popen( -            '/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd) +        for cmd in self.cmds: +            await sdk.ide.runCommand(cmd) -        stdin_input = "\n".join(self.cmds) -        out, err = process.communicate(stdin_input.encode()) +        # process = subprocess.Popen( +        #     '/bin/bash', stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd) -        # If it fails, return the error -        if err is not None and err != "": -            self._err_text = err -            return TextObservation(text=err) +        # stdin_input = "\n".join(self.cmds) +        # out, err = process.communicate(stdin_input.encode()) -        return None +        # # If it fails, return the error +        # if err is not None and err != "": +        #     self._err_text = err +        #     return TextObservation(text=err) + +        # return None  class EditCodeStep(Step): @@ -197,10 +200,10 @@ class WaitForUserInputStep(Step):          if self._response is None:              return self.prompt          else: -            return self.prompt + "\n\n" + self._response +            return f"{self.prompt}\n\n`{self._response}`"      async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: -        self._description = self.prompt +        self.description = self.prompt          resp = await sdk.wait_for_user_input()          self._response = resp          return TextObservation(text=resp) @@ -214,7 +217,7 @@ class WaitForUserConfirmationStep(Step):          return self.prompt      async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: -        self._description = self.prompt +        self.description = self.prompt          resp = await sdk.wait_for_user_input()          self.hide = True          return TextObservation(text=resp) diff --git a/extension/package.json b/extension/package.json index 7bd48f98..c834f402 100644 --- a/extension/package.json +++ b/extension/package.json @@ -42,12 +42,12 @@            "description": "The URL of the Continue server to use."          },          "continue.OPENAI_API_KEY": { -          "type": "string", +          "type": "password",            "default": "",            "description": "The OpenAI API key to use for code generation."          },          "continue.HUGGING_FACE_TOKEN": { -          "type": "string", +          "type": "password",            "default": "",            "description": "The Hugging Face API token to use for code generation."          } diff --git a/extension/react-app/src/components/DebugPanel.tsx b/extension/react-app/src/components/DebugPanel.tsx index 9dacc624..11ec2fe2 100644 --- a/extension/react-app/src/components/DebugPanel.tsx +++ b/extension/react-app/src/components/DebugPanel.tsx @@ -9,7 +9,7 @@ import {  } from "../redux/slices/configSlice";  import { setHighlightedCode } from "../redux/slices/miscSlice";  import { updateFileSystem } from "../redux/slices/debugContexSlice"; -import { buttonColor, defaultBorderRadius, vscBackground } from "."; +import { defaultBorderRadius, secondaryDark, vscBackground } from ".";  interface DebugPanelProps {    tabs: {      element: React.ReactElement; @@ -19,14 +19,15 @@ interface DebugPanelProps {  const GradientContainer = styled.div`    // Uncomment to get gradient border -  background: linear-gradient( +  /* background: linear-gradient(      101.79deg,      #12887a 0%,      #87245c 37.64%,      #e12637 65.98%,      #ffb215 110.45% -  ); +  ); */    /* padding: 10px; */ +  background-color: ${secondaryDark};    margin: 0;    height: 100%;    /* border: 1px solid white; */ @@ -36,11 +37,8 @@ const GradientContainer = styled.div`  const MainDiv = styled.div`    height: 100%;    border-radius: ${defaultBorderRadius}; -  overflow-y: scroll; -  scrollbar-gutter: stable both-edges;    scrollbar-base-color: transparent; -  /* background: ${vscBackground}; */ -  background-color: #1e1e1ede; +  background-color: ${vscBackground};  `;  const TabBar = styled.div<{ numTabs: number }>` @@ -105,9 +103,6 @@ function DebugPanel(props: DebugPanelProps) {                <div                  key={index}                  hidden={index !== currentTab} -                className={ -                  tab.title === "Chat" ? "overflow-hidden" : "overflow-scroll" -                }                  style={{ scrollbarGutter: "stable both-edges" }}                >                  {tab.element} diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index f54d4d75..dab5a752 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -2,13 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";  import styled, { keyframes } from "styled-components";  import {    appear, -  buttonColor,    defaultBorderRadius, -  MainContainerWithBorder, -  MainTextInput,    secondaryDark,    vscBackground,    GradientBorder, +  vscBackgroundTransparent,  } from ".";  import { RangeInFile, FileEdit } from "../../../src/client";  import CodeBlock from "./CodeBlock"; @@ -32,6 +30,7 @@ interface StepContainerProps {    onRefinement: (input: string) => void;    onUserInput: (input: string) => void;    onRetry: () => void; +  open?: boolean;  }  const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>` @@ -44,17 +43,25 @@ const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>`  `;  const StepContainerDiv = styled.div<{ open: boolean }>` -  background-color: ${(props) => (props.open ? vscBackground : secondaryDark)}; -  border-radius: ${defaultBorderRadius}; -  padding: 8px; +  /* background-color: ${(props) => +    props.open ? vscBackground : secondaryDark}; */ +  /* border-radius: ${defaultBorderRadius}; */ +  /* padding: 8px; */  `;  const HeaderDiv = styled.div` +  background-color: ${vscBackgroundTransparent};    display: grid;    grid-template-columns: 1fr auto;    align-items: center;  `; +const ContentDiv = styled.div` +  padding: 8px; +  padding-left: 16px; +  background-color: ${vscBackground}; +`; +  const HeaderButton = styled.button`    background-color: transparent;    border: 1px solid white; @@ -75,12 +82,10 @@ const OnHoverDiv = styled.div`    animation: ${appear} 0.3s ease-in-out;  `; -const NaturalLanguageInput = styled(MainTextInput)` -  width: 80%; -`; -  function StepContainer(props: StepContainerProps) { -  const [open, setOpen] = useState(false); +  const [open, setOpen] = useState( +    typeof props.open === "undefined" ? true : props.open +  );    const [isHovered, setIsHovered] = useState(false);    const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null);    const userInputRef = useRef<HTMLInputElement>(null); @@ -116,11 +121,11 @@ function StepContainer(props: StepContainerProps) {        }}        hidden={props.historyNode.step.hide as any}      > -      <GradientBorder -        className="m-2 overflow-hidden cursor-pointer" -        onClick={() => setOpen((prev) => !prev)} -      > -        <StepContainerDiv open={open}> +      <StepContainerDiv open={open}> +        <GradientBorder +          className="overflow-hidden cursor-pointer" +          onClick={() => setOpen((prev) => !prev)} +        >            <HeaderDiv>              <h4 className="m-2">                {open ? ( @@ -128,7 +133,7 @@ function StepContainer(props: StepContainerProps) {                ) : (                  <ChevronRight size="1.4em" />                )} -              {props.historyNode.step.name as any}: +              {props.historyNode.step.name as any}              </h4>              {/* <HeaderButton                onClick={(e) => { @@ -152,8 +157,9 @@ function StepContainer(props: StepContainerProps) {                <></>              )}            </HeaderDiv> - -          {open && ( +        </GradientBorder> +        <ContentDiv hidden={!open}> +          {open && false && (              <>                <pre className="overflow-scroll">                  Step Details: @@ -177,7 +183,7 @@ function StepContainer(props: StepContainerProps) {              </ReactMarkdown>            )} -          {props.historyNode.step.name === "Waiting for user input" && ( +          {/* {props.historyNode.step.name === "Waiting for user input" && (              <InputAndButton                onUserInput={(value) => {                  props.onUserInput(value); @@ -202,9 +208,9 @@ function StepContainer(props: StepContainerProps) {                  value="Confirm"                />              </> -          )} -        </StepContainerDiv> -      </GradientBorder> +          )} */} +        </ContentDiv> +      </StepContainerDiv>        {/* <OnHoverDiv hidden={!open}>          <NaturalLanguageInput diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index 7ba60467..ac5faa41 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -3,6 +3,7 @@ import styled, { keyframes } from "styled-components";  export const defaultBorderRadius = "5px";  export const secondaryDark = "rgb(37 37 38)";  export const vscBackground = "rgb(30 30 30)"; +export const vscBackgroundTransparent = "#1e1e1ede";  export const buttonColor = "rgb(113 28 59)";  export const buttonColorHover = "rgb(113 28 59 0.67)"; @@ -94,9 +95,13 @@ export const Loader = styled.div`    margin: auto;  `; -export const GradientBorder = styled.div<{ borderWidth?: string }>` -  border-radius: ${defaultBorderRadius}; -  padding: ${(props) => props.borderWidth || "1px"}; +export const GradientBorder = styled.div<{ +  borderWidth?: string; +  borderRadius?: string; +}>` +  border-radius: ${(props) => props.borderRadius || "0"}; +  padding-top: ${(props) => props.borderWidth || "1px"}; +  padding-bottom: ${(props) => props.borderWidth || "1px"};    background: linear-gradient(      101.79deg,      #12887a 0%, diff --git a/extension/react-app/src/tabs/chat/MessageDiv.tsx b/extension/react-app/src/tabs/chat/MessageDiv.tsx index ad81f5e9..1d7bb5f5 100644 --- a/extension/react-app/src/tabs/chat/MessageDiv.tsx +++ b/extension/react-app/src/tabs/chat/MessageDiv.tsx @@ -21,7 +21,6 @@ const Container = styled.div`    width: fit-content;    max-width: 75%;    overflow-y: scroll; -  scrollbar-gutter: stable both-edges;    word-wrap: break-word;    -ms-word-wrap: break-word;    height: fit-content; diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index 7dd30acb..c66172a9 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -1,25 +1,21 @@  import styled from "styled-components";  import { -  Button,    defaultBorderRadius,    vscBackground, -  MainTextInput,    Loader, +  MainTextInput,  } from "../components";  import ContinueButton from "../components/ContinueButton";  import { useCallback, useEffect, useRef, useState } from "react";  import { History } from "../../../schema/History";  import { HistoryNode } from "../../../schema/HistoryNode";  import StepContainer from "../components/StepContainer"; -import { useSelector } from "react-redux"; -import { RootStore } from "../redux/store"; -import useContinueWebsocket from "../hooks/useWebsocket";  import useContinueGUIProtocol from "../hooks/useWebsocket";  let TopGUIDiv = styled.div`    display: grid;    grid-template-columns: 1fr; -  overflow: scroll; +  background-color: ${vscBackground};  `;  let UserInputQueueItem = styled.div` @@ -156,8 +152,20 @@ function GUI(props: GUIProps) {    //   current_index: 0,    // } as any); +  const topGuiDivRef = useRef<HTMLDivElement>(null);    const client = useContinueGUIProtocol(); +  const scrollToBottom = useCallback(() => { +    if (topGuiDivRef.current) { +      setTimeout(() => { +        window.scrollTo({ +          top: window.outerHeight, +          behavior: "smooth", +        }); +      }, 100); +    } +  }, [topGuiDivRef.current]); +    useEffect(() => {      console.log("CLIENT ON STATE UPDATE: ", client, client?.onStateUpdate);      client?.onStateUpdate((state) => { @@ -165,9 +173,15 @@ function GUI(props: GUIProps) {        setWaitingForSteps(state.active);        setHistory(state.history);        setUserInputQueue(state.user_input_queue); + +      scrollToBottom();      });    }, [client]); +  useEffect(() => { +    scrollToBottom(); +  }, [waitingForSteps]); +    const mainTextInputRef = useRef<HTMLTextAreaElement>(null);    useEffect(() => { @@ -189,16 +203,33 @@ function GUI(props: GUIProps) {      if (mainTextInputRef.current) {        if (!client) return;        let input = mainTextInputRef.current.value; -      if (input.trim() === "") return; -      setWaitingForSteps(true); -      client.sendMainInput(input); -      setUserInputQueue((queue) => { -        return [...queue, input]; -      }); -      mainTextInputRef.current.value = ""; -      mainTextInputRef.current.style.height = ""; +      if ( +        history && +        history.timeline[history.current_index].step.name === +          "Waiting for user input" +      ) { +        if (input.trim() === "") return; +        onStepUserInput(input, history!.current_index); +      } else if ( +        history && +        history.timeline[history.current_index].step.name === +          "Waiting for user confirmation" +      ) { +        onStepUserInput("ok", history!.current_index); +      } else { +        if (input.trim() === "") return; + +        client.sendMainInput(input); +        setUserInputQueue((queue) => { +          return [...queue, input]; +        }); +        mainTextInputRef.current.value = ""; +        mainTextInputRef.current.style.height = ""; +      }      } + +    setWaitingForSteps(true);    };    const onStepUserInput = (input: string, index: number) => { @@ -209,7 +240,14 @@ function GUI(props: GUIProps) {    // const iterations = useSelector(selectIterations);    return ( -    <TopGUIDiv> +    <TopGUIDiv +      ref={topGuiDivRef} +      onKeyDown={(e) => { +        if (e.key === "Enter" && e.ctrlKey) { +          onMainTextInput(); +        } +      }} +    >        {typeof client === "undefined" && (          <>            <Loader></Loader> @@ -249,6 +287,12 @@ function GUI(props: GUIProps) {        </div>        <MainTextInput +        disabled={ +          history +            ? history.timeline[history.current_index].step.name === +              "Waiting for user confirmation" +            : false +        }          ref={mainTextInputRef}          onKeyDown={(e) => {            if (e.key === "Enter") { diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index a5a1c5dc..25287d32 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -96,6 +96,12 @@ class IdeProtocolClient {            fileEdit,          });          break; +      case "highlightCode": +        this.highlightCode(data.rangeInFile, data.color); +        break; +      case "runCommand": +        this.runCommand(data.command); +        break;        case "saveFile":          this.saveFile(data.filepath);          break; @@ -117,6 +123,28 @@ class IdeProtocolClient {    // ------------------------------------ //    // On message handlers +  async highlightCode(rangeInFile: RangeInFile, color: string) { +    const range = new vscode.Range( +      rangeInFile.range.start.line, +      rangeInFile.range.start.character, +      rangeInFile.range.end.line, +      rangeInFile.range.end.character +    ); +    const editor = await openEditorAndRevealRange( +      rangeInFile.filepath, +      range, +      vscode.ViewColumn.One +    ); +    if (editor) { +      editor.setDecorations( +        vscode.window.createTextEditorDecorationType({ +          backgroundColor: color, +        }), +        [range] +      ); +    } +  } +    showSuggestion(edit: FileEdit) {      // showSuggestion already exists      showSuggestion( @@ -289,7 +317,16 @@ class IdeProtocolClient {    }    runCommand(command: string) { -    vscode.window.terminals[0].sendText(command, true); +    if (vscode.window.terminals.length === 0) { +      const terminal = vscode.window.createTerminal(); +      terminal.show(); +      terminal.sendText("bash", true); +      terminal.sendText(command, true); +      return; +    } +    const terminal = vscode.window.terminals[0]; +    terminal.show(); +    terminal.sendText(command, true);      // But need to know when it's done executing...    }  } | 
