From 5709c9ae003ededb520ee641cae8c8570d4c93af Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sun, 9 Jul 2023 14:12:36 -0700 Subject: sdk.get_code_context --- extension/react-app/src/components/ComboBox.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index e6632360..801c3a03 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -224,8 +224,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { editing={section.editing} pinned={section.pinned} index={idx} - key={`${section.filepath}${idx}`} - title={`${section.range.filepath} (${ + key={`${section.display_name}${idx}`} + title={`${section.display_name} (${ section.range.range.start.line + 1 }-${section.range.range.end.line + 1})`} onDelete={() => { @@ -372,7 +372,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { > {highlightedCodeSections.map((section, idx) => ( <> -

{section.range.filepath}

+

{section.display_name}

{section.range.contents} -- cgit v1.2.3-70-g09d2 From b1a39567addc511ac0b477aaedaae4e10d7f5d31 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Tue, 11 Jul 2023 11:31:41 -0700 Subject: explain insert at cursor, better diff streaming --- continuedev/src/continuedev/steps/core/core.py | 44 ++++++++++++++++++++++--- extension/react-app/src/components/ComboBox.tsx | 32 +++++++++++++++--- extension/src/diffs.ts | 7 +++- 3 files changed, 72 insertions(+), 11 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py index b0d9d719..d4d067ba 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -265,6 +265,23 @@ class DefaultModelEditCodeStep(Step): return file_prefix, rif.contents, file_suffix, model_to_use, max_tokens def compile_prompt(self, file_prefix: str, contents: str, file_suffix: str, sdk: ContinueSDK) -> str: + if contents.strip() == "": + # Seperate prompt for insertion at the cursor, the other tends to cause it to repeat whole file + prompt = dedent(f"""\ + +{file_prefix} + + + +{file_suffix} + + +{self.user_input} + + +Please output the code to be inserted at the cursor in order to fulfill the user_request. Do NOT preface your answer or write anything other than code. You should not write any tags, just the code. Make sure to correctly indent the code:""") + return prompt + prompt = self._prompt if file_prefix.strip() != "": prompt += dedent(f""" @@ -306,15 +323,32 @@ class DefaultModelEditCodeStep(Step): prompt = self.compile_prompt(file_prefix, contents, file_suffix, sdk) full_file_contents_lines = full_file_contents.split("\n") - async def sendDiffUpdate(lines: List[str], sdk: ContinueSDK): - nonlocal full_file_contents_lines, rif + lines_to_display = [] + + async def sendDiffUpdate(lines: List[str], sdk: ContinueSDK, final: bool = False): + nonlocal full_file_contents_lines, rif, lines_to_display completion = "\n".join(lines) full_prefix_lines = full_file_contents_lines[:rif.range.start.line] full_suffix_lines = full_file_contents_lines[rif.range.end.line:] + + # Don't do this at the very end, just show the inserted code + if final: + lines_to_display = [] + # Only recalculate at every new-line, because this is sort of expensive + elif completion.endswith("\n"): + contents_lines = rif.contents.split("\n") + rewritten_lines = 0 + for line in lines: + for i in range(rewritten_lines, len(contents_lines)): + if difflib.SequenceMatcher(None, line, contents_lines[i]).ratio() > 0.7 and contents_lines[i].strip() != "": + rewritten_lines = i + 1 + break + lines_to_display = contents_lines[rewritten_lines:] + new_file_contents = "\n".join( - full_prefix_lines) + "\n" + completion + "\n" + "\n".join(full_suffix_lines) + full_prefix_lines) + "\n" + completion + "\n" + "\n".join(lines_to_display) + "\n" + "\n".join(full_suffix_lines) step_index = sdk.history.current_index @@ -495,7 +529,7 @@ class DefaultModelEditCodeStep(Step): completion_lines_covered += 1 current_line_in_file += 1 - await sendDiffUpdate(lines + [common_whitespace + unfinished_line], sdk) + await sendDiffUpdate(lines + [common_whitespace if unfinished_line.startswith("<") else (common_whitespace + unfinished_line)], sdk) # Add the unfinished line if unfinished_line != "" and not self.line_to_be_ignored(unfinished_line, completion_lines_covered == 0) and not self.is_end_line(unfinished_line): @@ -505,7 +539,7 @@ class DefaultModelEditCodeStep(Step): completion_lines_covered += 1 current_line_in_file += 1 - await sendDiffUpdate(lines, sdk) + await sendDiffUpdate(lines, sdk, final=True) if False: # If the current block isn't empty, add that suggestion diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 801c3a03..585a0584 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -180,6 +180,26 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { useImperativeHandle(ref, () => downshiftProps, [downshiftProps]); + const [metaKeyPressed, setMetaKeyPressed] = useState(false); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Meta") { + setMetaKeyPressed(true); + } + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === "Meta") { + setMetaKeyPressed(false); + } + }; + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + }; + }); + useEffect(() => { if (!inputRef.current) { return; @@ -272,7 +292,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { - {/* - Highlight code to include as context. Currently open file included by - default. {highlightedCodeSections.length === 0 && ""} - */} + {highlightedCodeSections.length === 0 && + (downshiftProps.inputValue?.startsWith("/edit") || metaKeyPressed) && ( +
+ Inserting at cursor +
+ )} { setHoveringContextDropdown(true); diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts index 1dc292e1..3ea6b4f8 100644 --- a/extension/src/diffs.ts +++ b/extension/src/diffs.ts @@ -164,7 +164,12 @@ class DiffManager { // Stop the step at step_index in case it is still streaming ideProtocolClient.deleteAtIndex(diffInfo.step_index); - this.cleanUpDiff(diffInfo); + vscode.workspace.textDocuments + .find((doc) => doc.uri.fsPath === newFilepath) + ?.save() + .then(() => { + this.cleanUpDiff(diffInfo); + }); } } -- cgit v1.2.3-70-g09d2 From 3fc16804fe74c38d4f94c25281025a84fd51771b Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Tue, 11 Jul 2023 14:13:14 -0700 Subject: little ui stuff --- extension/react-app/src/components/ComboBox.tsx | 3 ++- extension/react-app/src/tabs/gui.tsx | 11 +++-------- 2 files changed, 5 insertions(+), 9 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 585a0584..ac994b0a 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -378,7 +378,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { {highlightedCodeSections.length === 0 && - (downshiftProps.inputValue?.startsWith("/edit") || metaKeyPressed) && ( + (downshiftProps.inputValue?.startsWith("/edit") || + (metaKeyPressed && downshiftProps.inputValue?.length > 0)) && (
Inserting at cursor
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index 0e60e05c..619b91e1 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -170,6 +170,7 @@ function GUI(props: GUIProps) { const waitingForSteps = state.active && state.history.current_index < state.history.timeline.length && + state.history.timeline[state.history.current_index] && state.history.timeline[ state.history.current_index ].step.description?.trim() === ""; @@ -236,14 +237,14 @@ function GUI(props: GUIProps) { history.current_index < history.timeline.length ) { if ( - history.timeline[history.current_index].step.name === + history.timeline[history.current_index]?.step.name === "Waiting for user input" ) { if (input.trim() === "") return; onStepUserInput(input, history!.current_index); return; } else if ( - history.timeline[history.current_index].step.name === + history.timeline[history.current_index]?.step.name === "Waiting for user confirmation" ) { onStepUserInput("ok", history!.current_index); @@ -350,12 +351,6 @@ function GUI(props: GUIProps) { { onMainTextInput(e); -- cgit v1.2.3-70-g09d2 From 391764f1371dab06af30a29e10a826a516b69bb3 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Wed, 12 Jul 2023 21:53:06 -0700 Subject: persist state and reconnect automatically --- continuedev/src/continuedev/core/autopilot.py | 17 +++- continuedev/src/continuedev/libs/constants/main.py | 6 ++ continuedev/src/continuedev/libs/util/paths.py | 17 ++++ continuedev/src/continuedev/server/gui.py | 10 +- continuedev/src/continuedev/server/ide.py | 25 +++-- continuedev/src/continuedev/server/ide_protocol.py | 10 +- continuedev/src/continuedev/server/main.py | 16 ++- .../src/continuedev/server/session_manager.py | 41 ++++++-- docs/docs/concepts/ide.md | 4 +- extension/react-app/src/components/ComboBox.tsx | 1 - extension/react-app/src/hooks/messenger.ts | 10 -- extension/react-app/src/pages/gui.tsx | 1 - extension/src/activation/activate.ts | 2 +- extension/src/continueIdeClient.ts | 62 ++++++++--- extension/src/debugPanel.ts | 113 ++++----------------- extension/src/util/messenger.ts | 10 ++ 16 files changed, 192 insertions(+), 153 deletions(-) create mode 100644 continuedev/src/continuedev/libs/constants/main.py create mode 100644 continuedev/src/continuedev/libs/util/paths.py (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 1b074435..e1c8a076 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -1,13 +1,13 @@ from functools import cached_property import traceback import time -from typing import Any, Callable, Coroutine, Dict, List +from typing import Any, Callable, Coroutine, Dict, List, Union import os from aiohttp import ClientPayloadError +from pydantic import root_validator from ..models.filesystem import RangeInFileWithContents 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 @@ -16,7 +16,6 @@ from .main import Context, ContinueCustomException, HighlightedRangeContext, Pol from ..steps.core.core import ReversibleStep, ManualEditStep, UserInputStep from ..libs.util.telemetry import capture_event from .sdk import ContinueSDK -import asyncio from ..libs.util.step_name_to_steps import get_step_from_name from ..libs.util.traceback_parsers import get_python_traceback, get_javascript_traceback from openai import error as openai_errors @@ -46,6 +45,7 @@ class Autopilot(ContinueBaseModel): ide: AbstractIdeProtocolServer history: History = History.from_empty() context: Context = Context() + full_state: Union[FullState, None] = None _on_update_callbacks: List[Callable[[FullState], None]] = [] _active: bool = False @@ -63,8 +63,15 @@ class Autopilot(ContinueBaseModel): arbitrary_types_allowed = True keep_untouched = (cached_property,) + @root_validator(pre=True) + def fill_in_values(cls, values): + full_state: FullState = values.get('full_state') + if full_state is not None: + values['history'] = full_state.history + return values + def get_full_state(self) -> FullState: - return FullState( + full_state = FullState( history=self.history, active=self._active, user_input_queue=self._main_user_input_queue, @@ -73,6 +80,8 @@ class Autopilot(ContinueBaseModel): slash_commands=self.get_available_slash_commands(), adding_highlighted_code=self._adding_highlighted_code, ) + self.full_state = full_state + return full_state def get_available_slash_commands(self) -> List[Dict]: custom_commands = list(map(lambda x: { diff --git a/continuedev/src/continuedev/libs/constants/main.py b/continuedev/src/continuedev/libs/constants/main.py new file mode 100644 index 00000000..96eb6e69 --- /dev/null +++ b/continuedev/src/continuedev/libs/constants/main.py @@ -0,0 +1,6 @@ +## PATHS ## + +CONTINUE_GLOBAL_FOLDER = ".continue" +CONTINUE_SESSIONS_FOLDER = "sessions" +CONTINUE_SERVER_FOLDER = "server" + diff --git a/continuedev/src/continuedev/libs/util/paths.py b/continuedev/src/continuedev/libs/util/paths.py new file mode 100644 index 00000000..fddef887 --- /dev/null +++ b/continuedev/src/continuedev/libs/util/paths.py @@ -0,0 +1,17 @@ +import os + +from ..constants.main import CONTINUE_SESSIONS_FOLDER, CONTINUE_GLOBAL_FOLDER, CONTINUE_SERVER_FOLDER + +def getGlobalFolderPath(): + return os.path.join(os.path.expanduser("~"), CONTINUE_GLOBAL_FOLDER) + + + +def getSessionsFolderPath(): + return os.path.join(getGlobalFolderPath(), CONTINUE_SESSIONS_FOLDER) + +def getServerFolderPath(): + return os.path.join(getGlobalFolderPath(), CONTINUE_SERVER_FOLDER) + +def getSessionFilePath(session_id: str): + return os.path.join(getSessionsFolderPath(), f"{session_id}.json") \ No newline at end of file diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index 21089f30..8f6f68f6 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -31,12 +31,12 @@ class AppStatus: Server.handle_exit = AppStatus.handle_exit -def session(x_continue_session_id: str = Header("anonymous")) -> Session: - return session_manager.get_session(x_continue_session_id) +async def session(x_continue_session_id: str = Header("anonymous")) -> Session: + return await session_manager.get_session(x_continue_session_id) -def websocket_session(session_id: str) -> Session: - return session_manager.get_session(session_id) +async def websocket_session(session_id: str) -> Session: + return await session_manager.get_session(session_id) T = TypeVar("T", bound=BaseModel) @@ -199,4 +199,6 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we print("Closing gui websocket") if websocket.client_state != WebSocketState.DISCONNECTED: await websocket.close() + + session_manager.persist_session(session.session_id) session_manager.remove_session(session.session_id) diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index 4645b49e..12a21f19 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -52,9 +52,11 @@ class FileEditsUpdate(BaseModel): class OpenFilesResponse(BaseModel): openFiles: List[str] + class VisibleFilesResponse(BaseModel): visibleFiles: List[str] + class HighlightedCodeResponse(BaseModel): highlightedCode: List[RangeInFile] @@ -115,6 +117,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): websocket: WebSocket session_manager: SessionManager sub_queue: AsyncSubscriptionQueue = AsyncSubscriptionQueue() + session_id: Union[str, None] = None def __init__(self, session_manager: SessionManager, websocket: WebSocket): self.websocket = websocket @@ -132,8 +135,6 @@ class IdeProtocolServer(AbstractIdeProtocolServer): continue message_type = message["messageType"] data = message["data"] - # if message_type == "openGUI": - # await self.openGUI() if message_type == "workspaceDirectory": self.workspace_directory = data["workspaceDirectory"] break @@ -158,8 +159,8 @@ class IdeProtocolServer(AbstractIdeProtocolServer): return resp_model.parse_obj(resp) async def handle_json(self, message_type: str, data: Any): - if message_type == "openGUI": - await self.openGUI() + if message_type == "getSessionId": + await self.getSessionId() elif message_type == "setFileOpen": await self.setFileOpen(data["filepath"], data["open"]) elif message_type == "setSuggestionsLocked": @@ -217,9 +218,10 @@ class IdeProtocolServer(AbstractIdeProtocolServer): "locked": locked }) - async def openGUI(self): - session_id = self.session_manager.new_session(self) - await self._send_json("openGUI", { + async def getSessionId(self): + session_id = self.session_manager.new_session( + self, self.session_id).session_id + await self._send_json("getSessionId", { "sessionId": session_id }) @@ -304,7 +306,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): async def getOpenFiles(self) -> List[str]: resp = await self._send_and_receive_json({}, OpenFilesResponse, "openFiles") return resp.openFiles - + async def getVisibleFiles(self) -> List[str]: resp = await self._send_and_receive_json({}, VisibleFilesResponse, "visibleFiles") return resp.visibleFiles @@ -416,7 +418,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): @router.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): +async def websocket_endpoint(websocket: WebSocket, session_id: str = None): try: await websocket.accept() print("Accepted websocket connection from, ", websocket.client) @@ -434,6 +436,9 @@ async def websocket_endpoint(websocket: WebSocket): ideProtocolServer.handle_json(message_type, data)) ideProtocolServer = IdeProtocolServer(session_manager, websocket) + ideProtocolServer.session_id = session_id + if session_id is not None: + session_manager.registered_ides[session_id] = ideProtocolServer other_msgs = await ideProtocolServer.initialize() for other_msg in other_msgs: @@ -454,3 +459,5 @@ async def websocket_endpoint(websocket: WebSocket): finally: if websocket.client_state != WebSocketState.DISCONNECTED: await websocket.close() + + session_manager.registered_ides.pop(ideProtocolServer.session_id) diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 2783dc61..2f78cf0e 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -1,5 +1,6 @@ -from typing import Any, List +from typing import Any, List, Union from abc import ABC, abstractmethod, abstractproperty +from fastapi import WebSocket from ..models.main import Traceback from ..models.filesystem_edit import FileEdit, FileSystemEdit, EditDiff @@ -7,6 +8,9 @@ from ..models.filesystem import RangeInFile, RangeInFileWithContents class AbstractIdeProtocolServer(ABC): + websocket: WebSocket + session_id: Union[str, None] + @abstractmethod async def handle_json(self, data: Any): """Handle a json message""" @@ -24,8 +28,8 @@ class AbstractIdeProtocolServer(ABC): """Set whether suggestions are locked""" @abstractmethod - async def openGUI(self): - """Open a GUI""" + async def getSessionId(self): + """Get a new session ID""" @abstractmethod async def showSuggestionsAndWait(self, suggestions: List[FileEdit]) -> bool: diff --git a/continuedev/src/continuedev/server/main.py b/continuedev/src/continuedev/server/main.py index f4d82903..aa093853 100644 --- a/continuedev/src/continuedev/server/main.py +++ b/continuedev/src/continuedev/server/main.py @@ -4,7 +4,8 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from .ide import router as ide_router from .gui import router as gui_router -import logging +from .session_manager import session_manager +import atexit import uvicorn import argparse @@ -44,5 +45,16 @@ def run_server(): uvicorn.run(app, host="0.0.0.0", port=args.port) +def cleanup(): + print("Cleaning up sessions") + for session_id in session_manager.sessions: + session_manager.persist_session(session_id) + + +atexit.register(cleanup) if __name__ == "__main__": - run_server() + try: + run_server() + except Exception as e: + cleanup() + raise e diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py index 7147dcfa..fb8ac386 100644 --- a/continuedev/src/continuedev/server/session_manager.py +++ b/continuedev/src/continuedev/server/session_manager.py @@ -1,9 +1,12 @@ -from asyncio import BaseEventLoop +import os from fastapi import WebSocket from typing import Any, Dict, List, Union from uuid import uuid4 +import json +from ..libs.util.paths import getSessionFilePath, getSessionsFolderPath from ..models.filesystem_edit import FileEditWithFullContents +from ..libs.constants.main import CONTINUE_SESSIONS_FOLDER from ..core.policy import DemoPolicy from ..core.main import FullState from ..core.autopilot import Autopilot @@ -39,17 +42,35 @@ class DemoAutopilot(Autopilot): class SessionManager: sessions: Dict[str, Session] = {} + # Mapping of session_id to IDE, where the IDE is still alive + registered_ides: Dict[str, AbstractIdeProtocolServer] = {} - def get_session(self, session_id: str) -> Session: + async def get_session(self, session_id: str) -> Session: if session_id not in self.sessions: + # Check then whether it is persisted by listing all files in the sessions folder + # And only if the IDE is still alive + sessions_folder = getSessionsFolderPath() + session_files = os.listdir(sessions_folder) + if f"{session_id}.json" in session_files and session_id in self.registered_ides: + if self.registered_ides[session_id].session_id is not None: + return self.new_session(self.registered_ides[session_id], session_id=session_id) + raise KeyError("Session ID not recognized", session_id) return self.sessions[session_id] - def new_session(self, ide: AbstractIdeProtocolServer) -> str: - autopilot = DemoAutopilot(policy=DemoPolicy(), ide=ide) - session_id = str(uuid4()) + def new_session(self, ide: AbstractIdeProtocolServer, session_id: Union[str, None] = None) -> Session: + full_state = None + if session_id is not None and os.path.exists(getSessionFilePath(session_id)): + with open(getSessionFilePath(session_id), "r") as f: + full_state = FullState(**json.load(f)) + + autopilot = DemoAutopilot( + policy=DemoPolicy(), ide=ide, full_state=full_state) + session_id = session_id or str(uuid4()) + ide.session_id = session_id session = Session(session_id=session_id, autopilot=autopilot) self.sessions[session_id] = session + self.registered_ides[session_id] = ide async def on_update(state: FullState): await session_manager.send_ws_data(session_id, "state_update", { @@ -58,11 +79,19 @@ class SessionManager: autopilot.on_update(on_update) create_async_task(autopilot.run_policy()) - return session_id + return session def remove_session(self, session_id: str): del self.sessions[session_id] + def persist_session(self, session_id: str): + """Save the session's FullState as a json file""" + full_state = self.sessions[session_id].autopilot.get_full_state() + if not os.path.exists(getSessionsFolderPath()): + os.mkdir(getSessionsFolderPath()) + with open(getSessionFilePath(session_id), "w") as f: + json.dump(full_state.dict(), f) + def register_websocket(self, session_id: str, ws: WebSocket): self.sessions[session_id].ws = ws print("Registered websocket for session", session_id) diff --git a/docs/docs/concepts/ide.md b/docs/docs/concepts/ide.md index dc7b9e23..bd31481b 100644 --- a/docs/docs/concepts/ide.md +++ b/docs/docs/concepts/ide.md @@ -41,9 +41,9 @@ Get the workspace directory Set whether a file is open -### openGUI +### getSessionId -Open a gui +Get a new session ID ### showSuggestionsAndWait diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index ac994b0a..7d6541c7 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -331,7 +331,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ) { (event.nativeEvent as any).preventDownshiftDefault = true; } else if (event.key === "ArrowUp") { - console.log("OWJFOIJO"); if (positionInHistory == 0) return; else if ( positionInHistory == history.length && diff --git a/extension/react-app/src/hooks/messenger.ts b/extension/react-app/src/hooks/messenger.ts index e2a0bab8..00ce1fbb 100644 --- a/extension/react-app/src/hooks/messenger.ts +++ b/extension/react-app/src/hooks/messenger.ts @@ -1,6 +1,3 @@ -// console.log("Websocket import"); -// const WebSocket = require("ws"); - export abstract class Messenger { abstract send(messageType: string, data: object): void; @@ -28,13 +25,6 @@ export class WebsocketMessenger extends Messenger { private serverUrl: string; _newWebsocket(): WebSocket { - // // Dynamic import, because WebSocket is builtin with browser, but not with node. And can't use require in browser. - // if (typeof process === "object") { - // console.log("Using node"); - // // process is only available in Node - // var WebSocket = require("ws"); - // } - const newWebsocket = new WebSocket(this.serverUrl); for (const listener of this.onOpenListeners) { this.onOpen(listener); diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index b9382bd1..4ff260fa 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -262,7 +262,6 @@ function GUI(props: GUIProps) { const onStepUserInput = (input: string, index: number) => { if (!client) return; - console.log("Sending step user input", input, index); client.sendStepUserInput(input, index); }; diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index 559caf44..cd885b12 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -56,7 +56,7 @@ export async function activateExtension(context: vscode.ExtensionContext) { registerAllCodeLensProviders(context); registerAllCommands(context); - // Initialize IDE Protocol Client, then call "openGUI" + // Initialize IDE Protocol Client const serverUrl = getContinueServerUrl(); ideProtocolClient = new IdeProtocolClient( `${serverUrl.replace("http", "ws")}/ide/ws`, diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index b728833f..4c1fdf1e 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -1,10 +1,9 @@ -// import { ShowSuggestionRequest } from "../schema/ShowSuggestionRequest"; import { editorSuggestionsLocked, showSuggestion as showSuggestionInEditor, SuggestionRanges, } from "./suggestions"; -import { openEditorAndRevealRange, getRightViewColumn } from "./util/vscode"; +import { openEditorAndRevealRange } from "./util/vscode"; import { FileEdit } from "../schema/FileEdit"; import { RangeInFile } from "../schema/RangeInFile"; import * as vscode from "vscode"; @@ -15,8 +14,6 @@ import { import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; import fs = require("fs"); import { WebsocketMessenger } from "./util/messenger"; -import * as path from "path"; -import * as os from "os"; import { diffManager } from "./diffs"; class IdeProtocolClient { @@ -27,17 +24,54 @@ class IdeProtocolClient { private _highlightDebounce: NodeJS.Timeout | null = null; - constructor(serverUrl: string, context: vscode.ExtensionContext) { - this.context = context; + private _lastReloadTime: number = 16; + private _reconnectionTimeouts: NodeJS.Timeout[] = []; + + private _sessionId: string | null = null; + private _serverUrl: string; - let messenger = new WebsocketMessenger(serverUrl); + private _newWebsocketMessenger() { + const requestUrl = + this._serverUrl + + (this._sessionId ? `?session_id=${this._sessionId}` : ""); + const messenger = new WebsocketMessenger(requestUrl); this.messenger = messenger; - messenger.onClose(() => { + + const reconnect = () => { + console.log("Trying to reconnect IDE protocol websocket..."); this.messenger = null; + + // Exponential backoff to reconnect + this._reconnectionTimeouts.forEach((to) => clearTimeout(to)); + + const timeout = setTimeout(() => { + if (this.messenger?.websocket?.readyState === 1) { + return; + } + this._newWebsocketMessenger(); + }, this._lastReloadTime); + + this._reconnectionTimeouts.push(timeout); + this._lastReloadTime = Math.min(2 * this._lastReloadTime, 5000); + }; + messenger.onOpen(() => { + this._reconnectionTimeouts.forEach((to) => clearTimeout(to)); + }); + messenger.onClose(() => { + reconnect(); + }); + messenger.onError(() => { + reconnect(); }); messenger.onMessage((messageType, data, messenger) => { this.handleMessage(messageType, data, messenger); }); + } + + constructor(serverUrl: string, context: vscode.ExtensionContext) { + this.context = context; + this._serverUrl = serverUrl; + this._newWebsocketMessenger(); // Setup listeners for any file changes in open editors // vscode.workspace.onDidChangeTextDocument((event) => { @@ -171,7 +205,7 @@ class IdeProtocolClient { case "showDiff": this.showDiff(data.filepath, data.replacement, data.step_index); break; - case "openGUI": + case "getSessionId": case "connected": break; default: @@ -284,10 +318,6 @@ class IdeProtocolClient { // ------------------------------------ // // Initiate Request - async openGUI(asRightWebviewPanel: boolean = false) { - // Open the webview panel - } - async getSessionId(): Promise { await new Promise((resolve, reject) => { // Repeatedly try to connect to the server @@ -303,10 +333,10 @@ class IdeProtocolClient { } }, 1000); }); - const resp = await this.messenger?.sendAndReceive("openGUI", {}); - const sessionId = resp.sessionId; + const resp = await this.messenger?.sendAndReceive("getSessionId", {}); // console.log("New Continue session with ID: ", sessionId); - return sessionId; + this._sessionId = resp.sessionId; + return resp.sessionId; } acceptRejectSuggestion(accept: boolean, key: SuggestionRanges) { diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts index 487bbedf..5e1689d1 100644 --- a/extension/src/debugPanel.ts +++ b/extension/src/debugPanel.ts @@ -8,76 +8,6 @@ import { import { RangeInFile } from "./client"; const WebSocket = require("ws"); -class StreamManager { - private _fullText: string = ""; - private _insertionPoint: vscode.Position | undefined; - - private _addToEditor(update: string) { - let editor = - vscode.window.activeTextEditor || vscode.window.visibleTextEditors[0]; - - if (typeof this._insertionPoint === "undefined") { - if (editor?.selection.isEmpty) { - this._insertionPoint = editor?.selection.active; - } else { - this._insertionPoint = editor?.selection.end; - } - } - editor?.edit((editBuilder) => { - if (this._insertionPoint) { - editBuilder.insert(this._insertionPoint, update); - this._insertionPoint = this._insertionPoint.translate( - Array.from(update.matchAll(/\n/g)).length, - update.length - ); - } - }); - } - - public closeStream() { - this._fullText = ""; - this._insertionPoint = undefined; - this._codeBlockStatus = "closed"; - this._pendingBackticks = 0; - } - - private _codeBlockStatus: "open" | "closed" | "language-descriptor" = - "closed"; - private _pendingBackticks: number = 0; - public onStreamUpdate(update: string) { - let textToInsert = ""; - for (let i = 0; i < update.length; i++) { - switch (this._codeBlockStatus) { - case "closed": - if (update[i] === "`" && this._fullText.endsWith("``")) { - this._codeBlockStatus = "language-descriptor"; - } - break; - case "language-descriptor": - if (update[i] === " " || update[i] === "\n") { - this._codeBlockStatus = "open"; - } - break; - case "open": - if (update[i] === "`") { - if (this._fullText.endsWith("``")) { - this._codeBlockStatus = "closed"; - this._pendingBackticks = 0; - } else { - this._pendingBackticks += 1; - } - } else { - textToInsert += "`".repeat(this._pendingBackticks) + update[i]; - this._pendingBackticks = 0; - } - break; - } - this._fullText += update[i]; - } - this._addToEditor(textToInsert); - } -} - let websocketConnections: { [url: string]: WebsocketConnection | undefined } = {}; @@ -127,8 +57,6 @@ class WebsocketConnection { } } -let streamManager = new StreamManager(); - export let debugPanelWebview: vscode.Webview | undefined; export function setupDebugPanel( panel: vscode.WebviewPanel | vscode.WebviewView, @@ -147,10 +75,7 @@ export function setupDebugPanel( .toString(); const isProduction = true; // context?.extensionMode === vscode.ExtensionMode.Development; - if (!isProduction) { - scriptUri = "http://localhost:5173/src/main.tsx"; - styleMainUri = "http://localhost:5173/src/main.css"; - } else { + if (isProduction) { scriptUri = debugPanelWebview .asWebviewUri( vscode.Uri.joinPath(extensionUri, "react-app/dist/assets/index.js") @@ -161,6 +86,9 @@ export function setupDebugPanel( vscode.Uri.joinPath(extensionUri, "react-app/dist/assets/index.css") ) .toString(); + } else { + scriptUri = "http://localhost:5173/src/main.tsx"; + styleMainUri = "http://localhost:5173/src/main.css"; } panel.webview.options = { @@ -175,11 +103,11 @@ export function setupDebugPanel( return; } - let rangeInFile: RangeInFile = { + const rangeInFile: RangeInFile = { range: e.selections[0], filepath: e.textEditor.document.fileName, }; - let filesystem = { + const filesystem = { [rangeInFile.filepath]: e.textEditor.document.getText(), }; panel.webview.postMessage({ @@ -217,13 +145,19 @@ export function setupDebugPanel( url, }); }; - const connection = new WebsocketConnection( - url, - onMessage, - onOpen, - onClose - ); - websocketConnections[url] = connection; + try { + const connection = new WebsocketConnection( + url, + onMessage, + onOpen, + onClose + ); + websocketConnections[url] = connection; + resolve(null); + } catch (e) { + console.log("Caught it!: ", e); + reject(e); + } }); } @@ -292,15 +226,6 @@ export function setupDebugPanel( openEditorAndRevealRange(data.path, undefined, vscode.ViewColumn.One); break; } - case "streamUpdate": { - // Write code at the position of the cursor - streamManager.onStreamUpdate(data.update); - break; - } - case "closeStream": { - streamManager.closeStream(); - break; - } case "withProgress": { // This message allows withProgress to be used in the webview if (data.done) { diff --git a/extension/src/util/messenger.ts b/extension/src/util/messenger.ts index b1df161b..7fd71ddd 100644 --- a/extension/src/util/messenger.ts +++ b/extension/src/util/messenger.ts @@ -15,6 +15,8 @@ export abstract class Messenger { abstract onOpen(callback: () => void): void; abstract onClose(callback: () => void): void; + + abstract onError(callback: () => void): void; abstract sendAndReceive(messageType: string, data: any): Promise; } @@ -26,6 +28,7 @@ export class WebsocketMessenger extends Messenger { } = {}; private onOpenListeners: (() => void)[] = []; private onCloseListeners: (() => void)[] = []; + private onErrorListeners: (() => void)[] = []; private serverUrl: string; _newWebsocket(): WebSocket { @@ -43,6 +46,9 @@ export class WebsocketMessenger extends Messenger { for (const listener of this.onCloseListeners) { this.onClose(listener); } + for (const listener of this.onErrorListeners) { + this.onError(listener); + } for (const messageType in this.onMessageListeners) { for (const listener of this.onMessageListeners[messageType]) { this.onMessageType(messageType, listener); @@ -151,4 +157,8 @@ export class WebsocketMessenger extends Messenger { onClose(callback: () => void): void { this.websocket.addEventListener("close", callback); } + + onError(callback: () => void): void { + this.websocket.addEventListener("error", callback); + } } -- cgit v1.2.3-70-g09d2 From 5c6d2f1be8474d26124506e0c2a640fa68efe52d Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 14 Jul 2023 02:30:45 -0700 Subject: fixed unique_id being asyncio.run property --- continuedev/src/continuedev/server/ide.py | 22 +++++++++++----------- continuedev/src/continuedev/server/ide_protocol.py | 5 +---- continuedev/src/continuedev/steps/help.py | 8 +++++--- extension/package-lock.json | 4 ++-- extension/package.json | 2 +- extension/react-app/src/components/ComboBox.tsx | 17 ++--------------- extension/react-app/src/components/Onboarding.tsx | 1 + 7 files changed, 23 insertions(+), 36 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index 12a21f19..73cce201 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -123,10 +123,12 @@ class IdeProtocolServer(AbstractIdeProtocolServer): self.websocket = websocket self.session_manager = session_manager - workspace_directory: str + workspace_directory: str = None + unique_id: str = None async def initialize(self) -> List[str]: await self._send_json("workspaceDirectory", {}) + await self._send_json("uniqueId", {}) other_msgs = [] while True: msg_string = await self.websocket.receive_text() @@ -137,9 +139,13 @@ class IdeProtocolServer(AbstractIdeProtocolServer): data = message["data"] if message_type == "workspaceDirectory": self.workspace_directory = data["workspaceDirectory"] - break + elif message_type == "uniqueId": + self.unique_id = data["uniqueId"] else: other_msgs.append(msg_string) + + if self.workspace_directory is not None and self.unique_id is not None: + break return other_msgs async def _send_json(self, message_type: str, data: Any): @@ -183,10 +189,12 @@ class IdeProtocolServer(AbstractIdeProtocolServer): self.onMainUserInput(data["input"]) elif message_type == "deleteAtIndex": self.onDeleteAtIndex(data["index"]) - elif message_type in ["highlightedCode", "openFiles", "visibleFiles", "readFile", "editFile", "getUserSecret", "runCommand", "uniqueId"]: + elif message_type in ["highlightedCode", "openFiles", "visibleFiles", "readFile", "editFile", "getUserSecret", "runCommand"]: self.sub_queue.post(message_type, data) elif message_type == "workspaceDirectory": self.workspace_directory = data["workspaceDirectory"] + elif message_type == "uniqueId": + self.unique_id = data["uniqueId"] else: raise ValueError("Unknown message type", message_type) @@ -311,14 +319,6 @@ class IdeProtocolServer(AbstractIdeProtocolServer): resp = await self._send_and_receive_json({}, VisibleFilesResponse, "visibleFiles") return resp.visibleFiles - async def get_unique_id(self) -> str: - resp = await self._send_and_receive_json({}, UniqueIdResponse, "uniqueId") - return resp.uniqueId - - @cached_property_no_none - def unique_id(self) -> str: - return asyncio.run(self.get_unique_id()) - async def getHighlightedCode(self) -> List[RangeInFile]: resp = await self._send_and_receive_json({}, HighlightedCodeResponse, "highlightedCode") return resp.highlightedCode diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 2f78cf0e..d0fb0bf8 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -108,7 +108,4 @@ class AbstractIdeProtocolServer(ABC): """Show a diff""" workspace_directory: str - - @abstractproperty - def unique_id(self) -> str: - """Get a unique ID for this IDE""" + unique_id: str diff --git a/continuedev/src/continuedev/steps/help.py b/continuedev/src/continuedev/steps/help.py index fdfb986f..2dc3647c 100644 --- a/continuedev/src/continuedev/steps/help.py +++ b/continuedev/src/continuedev/steps/help.py @@ -6,7 +6,7 @@ from ..libs.util.telemetry import capture_event help = dedent("""\ Continue is an open-source coding autopilot. It is a VS Code extension that brings the power of ChatGPT to your IDE. - It gathers context for you and stores your interactions automatically, so that you can avoid copy/paste now and benefit from a customized LLM later. + It gathers context for you and stores your interactions automatically, so that you can avoid copy/paste now and benefit from a customized Large Language Model (LLM) later. Continue can be used to... 1. Edit chunks of code with specific instructions (e.g. "/edit migrate this digital ocean terraform file into one that works for GCP") @@ -25,6 +25,7 @@ help = dedent("""\ If you have feedback, please use /feedback to let us know how you would like to use Continue. We are excited to hear from you!""") + class HelpStep(Step): name: str = "Help" @@ -41,7 +42,7 @@ class HelpStep(Step): Information: {help}""") - + self.chat_context.append(ChatMessage( role="user", content=prompt, @@ -54,4 +55,5 @@ class HelpStep(Step): self.description += chunk["content"] await sdk.update_ui() - capture_event(sdk.ide.unique_id, "help", {"question": question, "answer": self.description}) \ No newline at end of file + capture_event(sdk.ide.unique_id, "help", { + "question": question, "answer": self.description}) diff --git a/extension/package-lock.json b/extension/package-lock.json index 65fdab12..9d5c73e1 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.162", + "version": "0.0.163", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.162", + "version": "0.0.163", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index ef39582b..2b0f6b94 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@ "displayName": "Continue", "pricing": "Free", "description": "The open-source coding autopilot", - "version": "0.0.162", + "version": "0.0.163", "publisher": "Continue", "engines": { "vscode": "^1.67.0" diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 7d6541c7..73db33ca 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,29 +1,16 @@ -import React, { - useCallback, - useEffect, - useImperativeHandle, - useState, -} from "react"; +import React, { useEffect, useImperativeHandle, useState } from "react"; import { useCombobox } from "downshift"; import styled from "styled-components"; import { - buttonColor, defaultBorderRadius, lightGray, secondaryDark, vscBackground, } from "."; import CodeBlock from "./CodeBlock"; -import { RangeInFile } from "../../../src/client"; import PillButton from "./PillButton"; import HeaderButtonWithText from "./HeaderButtonWithText"; -import { - Trash, - LockClosed, - LockOpen, - Plus, - DocumentPlus, -} from "@styled-icons/heroicons-outline"; +import { DocumentPlus } from "@styled-icons/heroicons-outline"; import { HighlightedRangeContext } from "../../../schema/FullState"; // #region styled components diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx index e2dd6f57..6bfb0ccd 100644 --- a/extension/react-app/src/components/Onboarding.tsx +++ b/extension/react-app/src/components/Onboarding.tsx @@ -22,6 +22,7 @@ const StyledSpan = styled.span` &:hover { background-color: #ffffff33; } + white-space: nowrap; `; const Onboarding = () => { -- cgit v1.2.3-70-g09d2 From 39cd2ef27d6ed439b00a9edec4a487343ff1c2c9 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 14 Jul 2023 03:24:46 -0700 Subject: warn of large highlighted ranges, cmd+k->m --- continuedev/src/continuedev/core/policy.py | 2 +- continuedev/src/continuedev/steps/help.py | 2 +- extension/package-lock.json | 4 +- extension/package.json | 6 +- extension/react-app/src/components/ComboBox.tsx | 5 + extension/react-app/src/components/Onboarding.tsx | 1 + extension/react-app/src/components/PillButton.tsx | 167 +++++++++++++--------- extension/react-app/src/pages/gui.tsx | 27 ++-- extension/src/commands.ts | 17 ++- extension/src/lang-server/codeLens.ts | 4 +- 10 files changed, 141 insertions(+), 94 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py index 59ea78b1..bc897357 100644 --- a/continuedev/src/continuedev/core/policy.py +++ b/continuedev/src/continuedev/core/policy.py @@ -59,7 +59,7 @@ class DemoPolicy(Policy): return ( MessageStep(name="Welcome to Continue", message=dedent("""\ - Highlight code and ask a question or give instructions - - Use `cmd+k` (Mac) / `ctrl+k` (Windows) to open Continue + - Use `cmd+m` (Mac) / `ctrl+m` (Windows) to open Continue - Use `/help` to ask questions about how to use Continue""")) >> WelcomeStep() >> # SetupContinueWorkspaceStep() >> diff --git a/continuedev/src/continuedev/steps/help.py b/continuedev/src/continuedev/steps/help.py index 2dc3647c..ba1e6087 100644 --- a/continuedev/src/continuedev/steps/help.py +++ b/continuedev/src/continuedev/steps/help.py @@ -19,7 +19,7 @@ help = dedent("""\ Continue passes all of the sections of code you highlight, the code above and below the to-be edited highlighted code section, and all previous steps above input box as context to the LLM. - You can use cmd+k (Mac) / ctrl+k (Windows) to open Continue. You can use cmd+shift+e / ctrl+shift+e to open file Explorer. You can add your own OpenAI API key to VS Code Settings with `cmd+,` + You can use cmd+m (Mac) / ctrl+m (Windows) to open Continue. You can use cmd+shift+e / ctrl+shift+e to open file Explorer. You can add your own OpenAI API key to VS Code Settings with `cmd+,` If Continue is stuck loading, try using `cmd+shift+p` to open the command palette, search "Reload Window", and then select it. This will reload VS Code and Continue and often fixes issues. diff --git a/extension/package-lock.json b/extension/package-lock.json index 9d5c73e1..a79dd6b4 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.163", + "version": "0.0.164", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.163", + "version": "0.0.164", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index 2b0f6b94..de1f395d 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@ "displayName": "Continue", "pricing": "Free", "description": "The open-source coding autopilot", - "version": "0.0.163", + "version": "0.0.164", "publisher": "Continue", "engines": { "vscode": "^1.67.0" @@ -111,8 +111,8 @@ "keybindings": [ { "command": "continue.focusContinueInput", - "mac": "cmd+k", - "key": "ctrl+k" + "mac": "cmd+m", + "key": "ctrl+m" }, { "command": "continue.suggestionDown", diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 73db33ca..bd0d59b5 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -228,6 +228,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { )} */} {highlightedCodeSections.map((section, idx) => ( 4000 && section.editing + ? "Editing such a large range may be slow" + : undefined + } editing={section.editing} pinned={section.pinned} index={idx} diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx index 6bfb0ccd..231c1e93 100644 --- a/extension/react-app/src/components/Onboarding.tsx +++ b/extension/react-app/src/components/Onboarding.tsx @@ -109,6 +109,7 @@ const Onboarding = () => { paddingBottom: "50px", textAlign: "center", cursor: "pointer", + whiteSpace: "nowrap", }} > ` } `; +const CircleDiv = styled.div` + position: absolute; + top: -10px; + right: -10px; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: red; + color: white; + display: flex; + align-items: center; + justify-content: center; + padding: 2px; +`; + interface PillButtonProps { onHover?: (arg0: boolean) => void; onDelete?: () => void; @@ -55,6 +68,7 @@ interface PillButtonProps { index: number; editing: boolean; pinned: boolean; + warning?: string; } const PillButton = (props: PillButtonProps) => { @@ -63,75 +77,96 @@ const PillButton = (props: PillButtonProps) => { return ( <> - + + {props.editing + ? "Editing this range (with rest of file as context)" + : "Edit this range"} + + Delete + {props.warning && ( + <> + + + + + {props.warning} - { - if (props.onDelete) { - props.onDelete(); - } - }} - > - - - + )} - {props.title} - - - {props.editing - ? "Editing this range (with rest of file as context)" - : "Edit this range"} - - Delete + ); }; diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index 4ff260fa..57cebac3 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -95,11 +95,8 @@ function GUI(props: GUIProps) { name: "Welcome to Continue", hide: false, description: `- Highlight code and ask a question or give instructions -- Use \`cmd+k\` (Mac) / \`ctrl+k\` (Windows) to open Continue -- Use \`cmd+shift+e\` / \`ctrl+shift+e\` to open file Explorer -- Add your own OpenAI API key to VS Code Settings with \`cmd+,\` -- Use slash commands when you want fine-grained control -- Past steps are included as part of the context by default`, + - Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue + - Use \`/help\` to ask questions about how to use Continue`, system_message: null, chat_context: [], manage_own_chat_context: false, @@ -269,15 +266,17 @@ function GUI(props: GUIProps) { return ( <> - { - client?.sendMainInput(`/feedback ${text}`); - setShowFeedbackDialog(false); - }} - onClose={() => { - setShowFeedbackDialog(false); - }} - message={feedbackDialogMessage} /> + { + client?.sendMainInput(`/feedback ${text}`); + setShowFeedbackDialog(false); + }} + onClose={() => { + setShowFeedbackDialog(false); + }} + message={feedbackDialogMessage} + /> any } = { "continue.suggestionDown": suggestionDownCommand, @@ -30,10 +32,15 @@ const commandsMap: { [command: string]: (...args: any) => any } = { "continue.acceptAllSuggestions": acceptAllSuggestionsCommand, "continue.rejectAllSuggestions": rejectAllSuggestionsCommand, "continue.focusContinueInput": async () => { - vscode.commands.executeCommand("continue.continueGUIView.focus"); - debugPanelWebview?.postMessage({ - type: "focusContinueInput", - }); + if (focusedOnContinueInput) { + vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup"); + } else { + vscode.commands.executeCommand("continue.continueGUIView.focus"); + debugPanelWebview?.postMessage({ + type: "focusContinueInput", + }); + } + focusedOnContinueInput = !focusedOnContinueInput; }, "continue.quickTextEntry": async () => { const text = await vscode.window.showInputBox({ @@ -53,4 +60,4 @@ export function registerAllCommands(context: vscode.ExtensionContext) { vscode.commands.registerCommand(command, callback) ); } -} \ No newline at end of file +} diff --git a/extension/src/lang-server/codeLens.ts b/extension/src/lang-server/codeLens.ts index 5800a00e..1cfef5d5 100644 --- a/extension/src/lang-server/codeLens.ts +++ b/extension/src/lang-server/codeLens.ts @@ -60,12 +60,12 @@ class DiffViewerCodeLensProvider implements vscode.CodeLensProvider { } codeLenses.push( new vscode.CodeLens(range, { - title: "Accept ✅ (⌘⇧↩)", + title: "Accept All ✅ (⌘⇧↩)", command: "continue.acceptDiff", arguments: [document.uri.fsPath], }), new vscode.CodeLens(range, { - title: "Reject ❌ (⌘⇧⌫)", + title: "Reject All ❌ (⌘⇧⌫)", command: "continue.rejectDiff", arguments: [document.uri.fsPath], }) -- cgit v1.2.3-70-g09d2 From b19076ddb6d11acb5ffd54046d9e5cad549c00c1 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 14 Jul 2023 11:01:06 -0700 Subject: command m reliable toggle --- extension/package-lock.json | 4 ++-- extension/package.json | 2 +- extension/react-app/src/components/ComboBox.tsx | 4 ++++ extension/src/commands.ts | 5 ++++- extension/src/debugPanel.ts | 5 +++++ extension/src/diffs.ts | 21 ++++++++++++--------- 6 files changed, 28 insertions(+), 13 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/extension/package-lock.json b/extension/package-lock.json index a79dd6b4..12aa27c9 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.164", + "version": "0.0.165", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.164", + "version": "0.0.165", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index de1f395d..05bd4d84 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@ "displayName": "Continue", "pricing": "Free", "description": "The open-source coding autopilot", - "version": "0.0.164", + "version": "0.0.165", "publisher": "Continue", "engines": { "vscode": "^1.67.0" diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index bd0d59b5..5d9b5109 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -12,6 +12,7 @@ import PillButton from "./PillButton"; import HeaderButtonWithText from "./HeaderButtonWithText"; import { DocumentPlus } from "@styled-icons/heroicons-outline"; import { HighlightedRangeContext } from "../../../schema/FullState"; +import { postVscMessage } from "../vscode"; // #region styled components const mainInputFontSize = 13; @@ -297,6 +298,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // setShowContextDropdown(target.value.endsWith("@")); }, + onBlur: (e) => { + postVscMessage("blurContinueInput", {}); + }, onKeyDown: (event) => { if (event.key === "Enter" && event.shiftKey) { // Prevent Downshift's default 'Enter' behavior. diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 0025340a..888f01ed 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -16,11 +16,14 @@ import { import { acceptDiffCommand, rejectDiffCommand } from "./diffs"; import * as bridge from "./bridge"; import { debugPanelWebview } from "./debugPanel"; -import { sendTelemetryEvent, TelemetryEvent } from "./telemetry"; import { ideProtocolClient } from "./activation/activate"; let focusedOnContinueInput = false; +export const setFocusedOnContinueInput = (value: boolean) => { + focusedOnContinueInput = value; +}; + // Copy everything over from extension.ts const commandsMap: { [command: string]: (...args: any) => any } = { "continue.suggestionDown": suggestionDownCommand, diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts index 5e1689d1..dd24a8d8 100644 --- a/extension/src/debugPanel.ts +++ b/extension/src/debugPanel.ts @@ -6,6 +6,7 @@ import { openEditorAndRevealRange, } from "./util/vscode"; import { RangeInFile } from "./client"; +import { setFocusedOnContinueInput } from "./commands"; const WebSocket = require("ws"); let websocketConnections: { [url: string]: WebsocketConnection | undefined } = @@ -226,6 +227,10 @@ export function setupDebugPanel( openEditorAndRevealRange(data.path, undefined, vscode.ViewColumn.One); break; } + case "blurContinueInput": { + setFocusedOnContinueInput(false); + break; + } case "withProgress": { // This message allows withProgress to be used in the webview if (data.done) { diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts index 910c30f2..37943de4 100644 --- a/extension/src/diffs.ts +++ b/extension/src/diffs.ts @@ -104,6 +104,17 @@ class DiffManager { return editor; } + private _findFirstDifferentLine(contentA: string, contentB: string): number { + const linesA = contentA.split("\n"); + const linesB = contentB.split("\n"); + for (let i = 0; i < linesA.length && i < linesB.length; i++) { + if (linesA[i] !== linesB[i]) { + return i; + } + } + return 0; + } + writeDiff( originalFilepath: string, newContent: string, @@ -119,15 +130,7 @@ class DiffManager { if (!this.diffs.has(newFilepath)) { // Figure out the first line that is different const oldContent = fs.readFileSync(originalFilepath).toString("utf-8"); - let line = 0; - const newLines = newContent.split("\n"); - const oldLines = oldContent.split("\n"); - for (let i = 0; i < newLines.length && i < oldLines.length; i++) { - if (newLines[i] !== oldLines[i]) { - line = i; - break; - } - } + const line = this._findFirstDifferentLine(oldContent, newContent); const diffInfo: DiffInfo = { originalFilepath, -- cgit v1.2.3-70-g09d2 From c5102b0997baa81ce544514d6b5b4d5a2eae804f Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 14 Jul 2023 13:45:10 -0700 Subject: insidious client_state vs application_state err --- continuedev/src/continuedev/core/autopilot.py | 2 ++ continuedev/src/continuedev/server/gui.py | 2 +- continuedev/src/continuedev/server/ide.py | 2 +- continuedev/src/continuedev/steps/core/core.py | 9 ++++++++- extension/react-app/src/components/ComboBox.tsx | 9 ++++++++- extension/react-app/src/components/StepContainer.tsx | 9 ++++++--- extension/src/continueIdeClient.ts | 8 ++++++-- extension/src/diffs.ts | 2 +- extension/src/util/messenger.ts | 2 +- 9 files changed, 34 insertions(+), 11 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index e1c8a076..82439f49 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -37,6 +37,8 @@ def get_error_title(e: Exception) -> str: return "The request failed. Please check your internet connection and try again. If this issue persists, you can use our API key for free by going to VS Code settings and changing the value of continue.OPENAI_API_KEY to \"\"" elif isinstance(e, openai_errors.InvalidRequestError): return 'Your API key does not have access to GPT-4. You can use ours for free by going to VS Code settings and changing the value of continue.OPENAI_API_KEY to ""' + elif e.__str__().startswith("Cannot connect to host"): + return "The request failed. Please check your internet connection and try again." return e.__str__() or e.__repr__() diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index 238273b2..9a411fbe 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -53,7 +53,7 @@ class GUIProtocolServer(AbstractGUIProtocolServer): self.session = session async def _send_json(self, message_type: str, data: Any): - if self.websocket.client_state == WebSocketState.DISCONNECTED: + if self.websocket.application_state == WebSocketState.DISCONNECTED: return await self.websocket.send_json({ "messageType": message_type, diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index 73cce201..7875c94d 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -149,7 +149,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): return other_msgs async def _send_json(self, message_type: str, data: Any): - if self.websocket.client_state == WebSocketState.DISCONNECTED: + if self.websocket.application_state == WebSocketState.DISCONNECTED: return await self.websocket.send_json({ "messageType": message_type, diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py index 787da316..75f8e460 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -9,7 +9,7 @@ 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 -from ...core.main import ChatMessage, Step, SequentialStep +from ...core.main import ChatMessage, ContinueCustomException, Step, SequentialStep from ...libs.util.count_tokens import MAX_TOKENS_FOR_MODEL, DEFAULT_MAX_TOKENS from ...libs.util.dedent import dedent_and_get_common_whitespace import difflib @@ -608,6 +608,13 @@ Please output the code to be inserted at the cursor in order to fulfill the user rif_dict[rif.filepath] = rif.contents for rif in rif_with_contents: + # If the file doesn't exist, ask them to save it first + if not os.path.exists(rif.filepath): + message = f"The file {rif.filepath} does not exist. Please save it first." + raise ContinueCustomException( + title=message, message=message + ) + await sdk.ide.setFileOpen(rif.filepath) await sdk.ide.setSuggestionsLocked(rif.filepath, True) await self.stream_rif(rif, sdk) diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 5d9b5109..754c9445 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -169,6 +169,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { useImperativeHandle(ref, () => downshiftProps, [downshiftProps]); const [metaKeyPressed, setMetaKeyPressed] = useState(false); + const [focused, setFocused] = useState(false); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Meta") { @@ -298,7 +299,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // setShowContextDropdown(target.value.endsWith("@")); }, + onFocus: (e) => { + setFocused(true); + }, onBlur: (e) => { + setFocused(false); postVscMessage("blurContinueInput", {}); }, onKeyDown: (event) => { @@ -374,7 +379,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { {highlightedCodeSections.length === 0 && (downshiftProps.inputValue?.startsWith("/edit") || - (metaKeyPressed && downshiftProps.inputValue?.length > 0)) && ( + (focused && + metaKeyPressed && + downshiftProps.inputValue?.length > 0)) && (
Inserting at cursor
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index 6fa4ba13..14e9b854 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -253,9 +253,12 @@ function StepContainer(props: StepContainerProps) { )} {props.historyNode.observation?.error ? ( -
-              {props.historyNode.observation.error as string}
-            
+
+ View Traceback +
+                {props.historyNode.observation.error as string}
+              
+
) : ( void): void; abstract onClose(callback: () => void): void; - + abstract onError(callback: () => void): void; abstract sendAndReceive(messageType: string, data: any): Promise; -- cgit v1.2.3-70-g09d2 From 45ee33f7fd84c0bc49d35d9d1a7a3a8e9f6addd7 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Sat, 15 Jul 2023 15:06:32 -0700 Subject: use correct label for meta key --- extension/react-app/src/components/ComboBox.tsx | 3 ++- .../react-app/src/components/StepContainer.tsx | 7 +++++- extension/react-app/src/util/index.ts | 17 +++++++++++-- extension/src/diffs.ts | 3 ++- extension/src/lang-server/codeLens.ts | 7 +++--- extension/src/util/util.ts | 29 ++++++++++++++++++++++ 6 files changed, 58 insertions(+), 8 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 754c9445..f11e07af 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -13,6 +13,7 @@ import HeaderButtonWithText from "./HeaderButtonWithText"; import { DocumentPlus } from "@styled-icons/heroicons-outline"; import { HighlightedRangeContext } from "../../../schema/FullState"; import { postVscMessage } from "../vscode"; +import { getMetaKeyLabel } from "../util"; // #region styled components const mainInputFontSize = 13; @@ -286,7 +287,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { )} - { - setHoveringContextDropdown(true); - }} - onMouseLeave={() => { - setHoveringContextDropdown(false); - }} - hidden={true || (!hoveringContextDropdown && !hoveringButton)} - > - {highlightedCodeSections.map((section, idx) => ( - <> -

{section.display_name}

- - {section.range.contents} - - - ))} -
); }); -- cgit v1.2.3-70-g09d2 From 88c1f16c597e0a55271e622a5283562ccb7a80a1 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Mon, 17 Jul 2023 00:00:31 -0700 Subject: only show delete when only one range selected --- extension/react-app/src/components/ComboBox.tsx | 2 ++ extension/react-app/src/components/PillButton.tsx | 34 +++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 773fcf72..dbebd534 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -218,6 +218,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ? "Editing such a large range may be slow" : undefined } + onlyShowDelete={highlightedCodeSections.length <= 1} editing={section.editing} pinned={section.pinned} index={idx} @@ -334,6 +335,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { })} showAbove={showAbove()} ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + hidden={!downshiftProps.isOpen || items.length === 0} > {downshiftProps.isOpen && items.map((item, index) => ( diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index d9d779d1..5a16516e 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -27,7 +27,6 @@ const GridDiv = styled.div` height: 100%; display: grid; grid-gap: 0; - grid-template-columns: 1fr 1fr; align-items: center; border-radius: ${defaultBorderRadius}; @@ -69,6 +68,7 @@ interface PillButtonProps { editing: boolean; pinned: boolean; warning?: string; + onlyShowDelete?: boolean; } const PillButton = (props: PillButtonProps) => { @@ -105,19 +105,25 @@ const PillButton = (props: PillButtonProps) => { }} > {isHovered && ( - - { - client?.setEditingAtIndices([props.index]); - }} - > - - + + {props.onlyShowDelete || ( + { + client?.setEditingAtIndices([props.index]); + }} + > + + + )} {/* Date: Mon, 17 Jul 2023 11:36:21 -0500 Subject: align on `code section` --- continuedev/src/continuedev/core/policy.py | 2 +- extension/react-app/src/components/ComboBox.tsx | 6 +++--- extension/react-app/src/components/PillButton.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'extension/react-app/src/components/ComboBox.tsx') diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py index bc897357..d007c92b 100644 --- a/continuedev/src/continuedev/core/policy.py +++ b/continuedev/src/continuedev/core/policy.py @@ -58,7 +58,7 @@ class DemoPolicy(Policy): if history.get_current() is None: return ( MessageStep(name="Welcome to Continue", message=dedent("""\ - - Highlight code and ask a question or give instructions + - Highlight code section and ask a question or give instructions - Use `cmd+m` (Mac) / `ctrl+m` (Windows) to open Continue - Use `/help` to ask questions about how to use Continue""")) >> WelcomeStep() >> diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index dbebd534..8136399a 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -245,11 +245,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { props.onToggleAddContext(); }} > - Highlight to Add Context + Highlight code section ) : ( { props.onToggleAddContext(); }} @@ -261,7 +261,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {