diff options
28 files changed, 448 insertions, 117 deletions
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 03c33dba..94f7073b 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -4,7 +4,6 @@ on: push: branches: - main - - package-python jobs: pyinstaller: @@ -15,6 +14,8 @@ jobs: runs-on: ${{ matrix.os }} steps: + # Install Python requirements and build+upload binaries for each platform + - name: Check-out repository uses: actions/checkout@v3 @@ -44,33 +45,46 @@ jobs: name: ${{ runner.os }} Build path: dist/* - publish: + test-and-package: needs: pyinstaller - runs-on: ubuntu-latest - permissions: - contents: write + strategy: + matrix: + os: [macos-latest, ubuntu-20.04, windows-latest] + + runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v2 + # Download corresponding binary artifact for the platform + + - name: Create exe directory + run: | + mkdir extension/server/exe + - name: Download Linux build uses: actions/download-artifact@v2 with: name: Linux Build - path: exe/linux + path: extension/server/exe/linux + if: matrix.os == 'ubuntu-20.04' - name: Download macOS build uses: actions/download-artifact@v2 with: name: macOS Build - path: exe/mac + path: extension/server/exe/mac + if: matrix.os == 'macos-latest' - name: Download Windows build uses: actions/download-artifact@v2 with: name: Windows Build - path: exe/windows + path: extension/server/exe/windows + if: matrix.os == 'windows-latest' + + # Setup Node.js and install dependencies - name: Use Node.js 19.0.0 uses: actions/setup-node@v3 @@ -99,11 +113,83 @@ jobs: cd extension/react-app npm ci --legacy-peer-deps - - name: Build and Publish + # Run tests + + - name: Package the extension run: | cd extension npm run package - npx vsce publish patch -p ${{ secrets.VSCE_TOKEN }} + + - name: Install Xvfb for Linux and run tests + run: | + sudo apt-get install -y xvfb # Install Xvfb + Xvfb :99 & # Start Xvfb + export DISPLAY=:99 # Export the display number to the environment + cd extension + npm run test + if: matrix.os == 'ubuntu-20.04' + + - name: Run extension tests + run: | + cd extension + npm run test + if: matrix.os != 'ubuntu-20.04' + + # Upload .vsix artifact + + - name: Upload .vsix as an artifact + uses: actions/upload-artifact@v2 + with: + name: vsix-artifact + path: extension/build/* + if: matrix.os == 'ubuntu-20.04' + + publish: + needs: test-and-package + runs-on: ubuntu-20.04 + permissions: + contents: write + + steps: + # Checkout and download .vsix artifact + + - name: Checkout + uses: actions/checkout@v2 + + - name: Download .vsix artifact + uses: actions/download-artifact@v2 + with: + name: vsix-artifact + path: extension/build + + # Publish the extension and commit/push the version change + + - name: Use Node.js 19.0.0 + uses: actions/setup-node@v3 + with: + node-version: 19.0.0 + + - name: Cache extension node_modules + uses: actions/cache@v2 + with: + path: extension/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('extension/package-lock.json') }} + + - name: Install extension Dependencies + run: | + cd extension + npm ci + + - name: Publish + run: | + cd extension + npx vsce publish -p ${{ secrets.VSCE_TOKEN }} --packagePath ./build/*.vsix + + - name: Update version in package.json + run: | + cd extension + npm version patch + - name: Commit changes run: | git config --local user.email "action@github.com" @@ -116,18 +202,32 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }} - - name: Upload .vsix artifact - uses: actions/upload-artifact@v2 + # Download binaries and upload to S3 + + - name: Download Linux build + uses: actions/download-artifact@v2 with: - name: vsix-artifact - path: extension/build/* + name: Linux Build + path: exe/linux + + - name: Download macOS build + uses: actions/download-artifact@v2 + with: + name: macOS Build + path: exe/mac + + - name: Download Windows build + uses: actions/download-artifact@v2 + with: + name: Windows Build + path: exe/windows - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-west-2 + aws-region: us-west-1 - name: Upload binaries to S3 uses: jakejarvis/s3-sync-action@master diff --git a/.vscode/launch.json b/.vscode/launch.json index bbe1fd2e..12cfaef8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,18 +7,12 @@ { "name": "Server + Extension (VSCode)", "stopAll": true, - "configurations": [ - "Server", - "Extension (VSCode)" - ] + "configurations": ["Server", "Extension (VSCode)"] }, { "name": "Server + Tests (VSCode)", "stopAll": true, - "configurations": [ - "Server", - "Tests (VSCode)" - ] + "configurations": ["Server", "Tests (VSCode)"] } ], "configurations": [ @@ -27,12 +21,9 @@ "type": "python", "request": "launch", "module": "continuedev.src.continuedev.server.main", - "args": [ - "--port", - "8001" - ], + "args": ["--port", "8001"], "justMyCode": false, - "subProcess": false, + "subProcess": false // Does it need a build task? // What about a watch task? - type errors? }, @@ -45,11 +36,9 @@ // Pass a directory to manually test in "${workspaceFolder}/extension/manual-testing-sandbox", "${workspaceFolder}/extension/manual-testing-sandbox/example.ts", - "--extensionDevelopmentPath=${workspaceFolder}/extension", - ], - "outFiles": [ - "${workspaceFolder}/extension/out/**/*.js" + "--extensionDevelopmentPath=${workspaceFolder}/extension" ], + "outFiles": ["${workspaceFolder}/extension/out/**/*.js"], "preLaunchTask": "vscode-extension:build", "env": { "CONTINUE_SERVER_URL": "http://localhost:8001" @@ -77,10 +66,10 @@ "internalConsoleOptions": "openOnSessionStart", "preLaunchTask": "vscode-extension:tests:build", "env": { - "CONTINUE_SERVER_URL": "http://localhost:8001", + "CONTINUE_SERVER_URL": "http://localhost:65432", // Avoid timing out when stopping on breakpoints during debugging in VSCode - "MOCHA_TIMEOUT": "0", - }, + "MOCHA_TIMEOUT": "0" + } } ] -}
\ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c15edf0d..1a29668b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,6 +7,8 @@ "dependsOn": [ // To detect compile errors "vscode-extension:tsc", + // To build the react-app that is used in the extension + "vscode-extension:continue-ui:build", // To bundle the code the same way we do for publishing "vscode-extension:esbuild" ], @@ -50,6 +52,20 @@ "clear": true, }, }, + // Build the react-app. It gets bundled into the extension as a file resource and has a seprate build step + { + "label": "vscode-extension:continue-ui:build", + "type": "npm", + "script": "build", + "path": "extension/react-app", + "problemMatcher": [ + "$tsc" + ], + "presentation": { + "revealProblems": "onProblem", + "clear": true, + }, + }, // // Compile and bundle tests { diff --git a/continuedev/pyproject.toml b/continuedev/pyproject.toml index 49b3c5ed..90ff0572 100644 --- a/continuedev/pyproject.toml +++ b/continuedev/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" python = "^3.8.1" fastapi = "^0.95.1" typer = "^0.7.0" -openai = "^0.27.8" +openai = "^0.27.5" boltons = "^23.0.0" pydantic = "^1.10.7" uvicorn = "^0.21.1" diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 256f3439..9100c34e 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -81,11 +81,7 @@ class Autopilot(ContinueBaseModel): self.continue_sdk.config.context_providers + [ HighlightedCodeContextProvider(ide=self.ide), FileContextProvider(workspace_dir=self.ide.workspace_directory) - ]) - - logger.debug("Loading index") - create_async_task(self.context_manager.load_index( - self.ide.workspace_directory)) + ], self.ide.workspace_directory) if full_state is not None: self.history = full_state.history @@ -188,6 +184,9 @@ class Autopilot(ContinueBaseModel): await self._run_singular_step(step) async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]): + if "code" not in self.context_manager.context_providers: + return + # Add to context manager await self.context_manager.context_providers["code"].handle_highlighted_code( range_in_files) @@ -212,10 +211,16 @@ class Autopilot(ContinueBaseModel): await self.update_subscribers() async def toggle_adding_highlighted_code(self): + if "code" not in self.context_manager.context_providers: + return + self.context_manager.context_providers["code"].adding_highlighted_code = not self.context_manager.context_providers["code"].adding_highlighted_code await self.update_subscribers() async def set_editing_at_ids(self, ids: List[str]): + if "code" not in self.context_manager.context_providers: + return + await self.context_manager.context_providers["code"].set_editing_at_ids(ids) await self.update_subscribers() diff --git a/continuedev/src/continuedev/core/context.py b/continuedev/src/continuedev/core/context.py index b1f68b50..db1c770a 100644 --- a/continuedev/src/continuedev/core/context.py +++ b/continuedev/src/continuedev/core/context.py @@ -8,9 +8,10 @@ from pydantic import BaseModel from .main import ChatMessage, ContextItem, ContextItemDescription, ContextItemId -from ..server.meilisearch_server import check_meilisearch_running +from ..server.meilisearch_server import poll_meilisearch_running from ..libs.util.logging import logger from ..libs.util.telemetry import posthog_logger +from ..libs.util.create_async_task import create_async_task SEARCH_INDEX_NAME = "continue_context_items" @@ -140,31 +141,32 @@ class ContextManager: self.context_providers = {} self.provider_titles = set() - async def start(self, context_providers: List[ContextProvider]): + async def start(self, context_providers: List[ContextProvider], workspace_directory: str): """ Starts the context manager. """ + # Use only non-meilisearch-dependent providers until it is loaded self.context_providers = { - prov.title: prov for prov in context_providers} + title: provider for title, provider in self.context_providers.items() if title == "code" + } self.provider_titles = { provider.title for provider in context_providers} - async with Client('http://localhost:7700') as search_client: - meilisearch_running = True + # Start MeiliSearch in the background without blocking + async def start_meilisearch(context_providers): try: - - health = await search_client.health() - if not health.status == "available": - meilisearch_running = False - except: - meilisearch_running = False - - if not meilisearch_running: + await asyncio.wait_for(poll_meilisearch_running(), timeout=20) + self.context_providers = { + prov.title: prov for prov in context_providers} + logger.debug("Loading Meilisearch index...") + await self.load_index(workspace_directory) + logger.debug("Loaded Meilisearch index") + except asyncio.TimeoutError: + logger.warning("MeiliSearch did not start within 5 seconds") logger.warning( "MeiliSearch not running, avoiding any dependent context providers") - self.context_providers = { - title: provider for title, provider in self.context_providers.items() if title == "code" - } + + create_async_task(start_meilisearch(context_providers)) async def load_index(self, workspace_dir: str): for _, provider in self.context_providers.items(): @@ -176,14 +178,24 @@ class ContextManager: "id": item.description.id.to_string(), "name": item.description.name, "description": item.description.description, - "content": item.content + "content": item.content, + "workspace_dir": workspace_dir, } for item in context_items ] if len(documents) > 0: try: async with Client('http://localhost:7700') as search_client: - await asyncio.wait_for(search_client.index(SEARCH_INDEX_NAME).add_documents(documents), timeout=5) + # First, create the index if it doesn't exist + await search_client.create_index(SEARCH_INDEX_NAME) + # The index is currently shared by all workspaces + globalSearchIndex = await search_client.get_index(SEARCH_INDEX_NAME) + await asyncio.wait_for(asyncio.gather( + # Ensure that the index has the correct filterable attributes + globalSearchIndex.update_filterable_attributes( + ["workspace_dir"]), + globalSearchIndex.add_documents(documents) + ), timeout=5) except Exception as e: logger.debug(f"Error loading meilisearch index: {e}") diff --git a/continuedev/src/continuedev/libs/llm/ggml.py b/continuedev/src/continuedev/libs/llm/ggml.py index 2f131354..25a61e63 100644 --- a/continuedev/src/continuedev/libs/llm/ggml.py +++ b/continuedev/src/continuedev/libs/llm/ggml.py @@ -82,7 +82,10 @@ class GGML(LLM): chunks = json_chunk.split("\n") for chunk in chunks: if chunk.strip() != "": - yield json.loads(chunk[6:])["choices"][0]["delta"] + yield { + "role": "assistant", + "content": json.loads(chunk[6:])["choices"][0]["delta"] + } except: raise Exception(str(line[0])) diff --git a/continuedev/src/continuedev/libs/llm/replicate.py b/continuedev/src/continuedev/libs/llm/replicate.py index 235fd906..0dd359e7 100644 --- a/continuedev/src/continuedev/libs/llm/replicate.py +++ b/continuedev/src/continuedev/libs/llm/replicate.py @@ -25,7 +25,7 @@ class ReplicateLLM(LLM): @property def default_args(self): - return {**DEFAULT_ARGS, "model": self.name, "max_tokens": 1024} + return {**DEFAULT_ARGS, "model": self.model, "max_tokens": 1024} def count_tokens(self, text: str): return count_tokens(self.name, text) diff --git a/continuedev/src/continuedev/libs/llm/together.py b/continuedev/src/continuedev/libs/llm/together.py new file mode 100644 index 00000000..c3f171c9 --- /dev/null +++ b/continuedev/src/continuedev/libs/llm/together.py @@ -0,0 +1,122 @@ +import json +from typing import Any, Coroutine, Dict, Generator, List, Union + +import aiohttp +from ...core.main import ChatMessage +from ..llm import LLM +from ..util.count_tokens import compile_chat_messages, DEFAULT_ARGS, count_tokens + + +class TogetherLLM(LLM): + # this is model-specific + api_key: str + model: str = "togethercomputer/RedPajama-INCITE-7B-Instruct" + max_context_length: int = 2048 + base_url: str = "https://api.together.xyz" + verify_ssl: bool = True + + _client_session: aiohttp.ClientSession = None + + async def start(self, **kwargs): + self._client_session = aiohttp.ClientSession( + connector=aiohttp.TCPConnector(verify_ssl=self.verify_ssl)) + + async def stop(self): + await self._client_session.close() + + @property + def name(self): + return self.model + + @property + def context_length(self): + return self.max_context_length + + @property + def default_args(self): + return {**DEFAULT_ARGS, "model": self.model, "max_tokens": 1024} + + def count_tokens(self, text: str): + return count_tokens(self.name, text) + + def convert_to_prompt(self, chat_messages: List[ChatMessage]) -> str: + system_message = None + if chat_messages[0]["role"] == "system": + system_message = chat_messages.pop(0)["content"] + + prompt = "\n" + if system_message: + prompt += f"<human>: Hi!\n<bot>: {system_message}\n" + for message in chat_messages: + prompt += f'<{"human" if message["role"] == "user" else "bot"}>: {message["content"]}\n' + + prompt += "<bot>:" + return prompt + + async def stream_complete(self, prompt, with_history: List[ChatMessage] = None, **kwargs) -> Generator[Union[Any, List, Dict], None, None]: + args = self.default_args.copy() + args.update(kwargs) + args["stream_tokens"] = True + + args = {**self.default_args, **kwargs} + messages = compile_chat_messages( + self.name, with_history, self.context_length, args["max_tokens"], prompt, functions=args.get("functions", None), system_message=self.system_message) + + async with self._client_session.post(f"{self.base_url}/inference", json={ + "prompt": self.convert_to_prompt(messages), + **args + }, headers={ + "Authorization": f"Bearer {self.api_key}" + }) as resp: + async for line in resp.content.iter_any(): + if line: + try: + yield line.decode("utf-8") + except: + raise Exception(str(line)) + + async def stream_chat(self, messages: List[ChatMessage] = None, **kwargs) -> Generator[Union[Any, List, Dict], None, None]: + args = {**self.default_args, **kwargs} + messages = compile_chat_messages( + self.name, messages, self.context_length, args["max_tokens"], None, functions=args.get("functions", None), system_message=self.system_message) + args["stream_tokens"] = True + + async with self._client_session.post(f"{self.base_url}/inference", json={ + "prompt": self.convert_to_prompt(messages), + **args + }, headers={ + "Authorization": f"Bearer {self.api_key}" + }) as resp: + async for line in resp.content.iter_chunks(): + if line[1]: + try: + json_chunk = line[0].decode("utf-8") + if json_chunk.startswith(": ping - ") or json_chunk.startswith("data: [DONE]"): + continue + chunks = json_chunk.split("\n") + for chunk in chunks: + if chunk.strip() != "": + yield { + "role": "assistant", + "content": json.loads(chunk[6:])["choices"][0]["text"] + } + except: + raise Exception(str(line[0])) + + async def complete(self, prompt: str, with_history: List[ChatMessage] = None, **kwargs) -> Coroutine[Any, Any, str]: + args = {**self.default_args, **kwargs} + + messages = compile_chat_messages(args["model"], with_history, self.context_length, + args["max_tokens"], prompt, functions=None, system_message=self.system_message) + async with self._client_session.post(f"{self.base_url}/inference", json={ + "prompt": self.convert_to_prompt(messages), + **args + }, headers={ + "Authorization": f"Bearer {self.api_key}" + }) as resp: + try: + text = await resp.text() + j = json.loads(text) + return j["output"]["choices"][0]["text"] + except: + raise Exception(await resp.text()) diff --git a/continuedev/src/continuedev/plugins/context_providers/file.py b/continuedev/src/continuedev/plugins/context_providers/file.py index 31aa5423..b40092af 100644 --- a/continuedev/src/continuedev/plugins/context_providers/file.py +++ b/continuedev/src/continuedev/plugins/context_providers/file.py @@ -54,33 +54,37 @@ class FileContextProvider(ContextProvider): list(filter(lambda d: f"**/{d}", DEFAULT_IGNORE_DIRS)) async def provide_context_items(self, workspace_dir: str) -> List[ContextItem]: - filepaths = [] + absolute_filepaths: List[str] = [] for root, dir_names, file_names in os.walk(workspace_dir): dir_names[:] = [d for d in dir_names if not any( fnmatch(d, pattern) for pattern in self.ignore_patterns)] for file_name in file_names: - filepaths.append(os.path.join(root, file_name)) + absolute_filepaths.append(os.path.join(root, file_name)) - if len(filepaths) > 1000: + if len(absolute_filepaths) > 1000: break - if len(filepaths) > 1000: + if len(absolute_filepaths) > 1000: break items = [] - for file in filepaths: - content = get_file_contents(file) + for absolute_filepath in absolute_filepaths: + content = get_file_contents(absolute_filepath) if content is None: continue # no pun intended + + relative_to_workspace = os.path.relpath(absolute_filepath, workspace_dir) items.append(ContextItem( content=content[:min(2000, len(content))], description=ContextItemDescription( - name=os.path.basename(file), - description=file, + name=os.path.basename(absolute_filepath), + # We should add the full path to the ContextItem + # It warrants a data modeling discussion and has no immediate use case + description=relative_to_workspace, id=ContextItemId( provider_title=self.title, - item_id=remove_meilisearch_disallowed_chars(file) + item_id=remove_meilisearch_disallowed_chars(absolute_filepath) ) ) )) diff --git a/continuedev/src/continuedev/plugins/steps/help.py b/continuedev/src/continuedev/plugins/steps/help.py index ec670999..82f885d6 100644 --- a/continuedev/src/continuedev/plugins/steps/help.py +++ b/continuedev/src/continuedev/plugins/steps/help.py @@ -39,6 +39,7 @@ class HelpStep(Step): if question.strip() == "": self.description = help else: + self.description = "The following output is generated by a language model, which may hallucinate. Type just '/help'to see a fixed answer. You can also learn more by reading [the docs](https://continue.dev/docs).\n\n" prompt = dedent(f""" Information: @@ -48,7 +49,7 @@ class HelpStep(Step): Please us the information below to provide a succinct answer to the following question: {question} - Do not cite any slash commands other than those you've been told about, which are: /edit and /feedback.""") + Do not cite any slash commands other than those you've been told about, which are: /edit and /feedback. Never refer or link to any URL.""") self.chat_context.append(ChatMessage( role="user", diff --git a/continuedev/src/continuedev/server/meilisearch_server.py b/continuedev/src/continuedev/server/meilisearch_server.py index 7f460afc..f47c08ca 100644 --- a/continuedev/src/continuedev/server/meilisearch_server.py +++ b/continuedev/src/continuedev/server/meilisearch_server.py @@ -1,3 +1,4 @@ +import asyncio import os import shutil import subprocess @@ -58,15 +59,26 @@ async def check_meilisearch_running() -> bool: async with Client('http://localhost:7700') as client: try: resp = await client.health() - if resp["status"] != "available": + if resp.status != "available": return False return True - except: + except Exception as e: + logger.debug(e) return False except Exception: return False +async def poll_meilisearch_running(frequency: int = 0.1) -> bool: + """ + Polls MeiliSearch to see if it is running. + """ + while True: + if await check_meilisearch_running(): + return True + await asyncio.sleep(frequency) + + async def start_meilisearch(): """ Starts the MeiliSearch server, wait for it. diff --git a/extension/.vscodeignore b/extension/.vscodeignore index d3326fdc..51d66585 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -24,4 +24,6 @@ react-app/src scripts/data scripts/env -scripts/.continue_env_installed
\ No newline at end of file +scripts/.continue_env_installed + +server/exe/**
\ No newline at end of file diff --git a/extension/manual-testing-sandbox/nested-folder/helloNested.py b/extension/manual-testing-sandbox/nested-folder/helloNested.py new file mode 100644 index 00000000..195ad40c --- /dev/null +++ b/extension/manual-testing-sandbox/nested-folder/helloNested.py @@ -0,0 +1,2 @@ +def main(): + print("Hello Nested!") diff --git a/extension/package-lock.json b/extension/package-lock.json index 6f7d32b2..be88abdd 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.296", + "version": "0.0.301", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.296", + "version": "0.0.301", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index 93b6e944..91b5606b 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,6 +1,7 @@ { "name": "continue", "icon": "media/terminal-continue.png", + "version": "0.0.301", "repository": { "type": "git", "url": "https://github.com/continuedev/continue" @@ -14,7 +15,6 @@ "displayName": "Continue", "pricing": "Free", "description": "The open-source coding autopilot", - "version": "0.0.296", "publisher": "Continue", "engines": { "vscode": "^1.67.0" @@ -194,7 +194,7 @@ "lint": "eslint src --ext ts", "build-test": "tsc && node esbuild.test.mjs", "test": "npm run build-test && node ./out/test-runner/runTestOnVSCodeHost.js", - "package": "npm install && npm run typegen && cd react-app && npm install && npm run build && cd .. && mkdir -p ./build && vsce package --out ./build" + "package": "node scripts/package.js" }, "devDependencies": { "@nestjs/common": "^8.4.7", diff --git a/extension/react-app/package.json b/extension/react-app/package.json index b4762990..23cdf9bb 100644 --- a/extension/react-app/package.json +++ b/extension/react-app/package.json @@ -1,11 +1,10 @@ { "name": "react-app", "private": true, - "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build --sourcemap", + "build": "tsc && vite build --sourcemap 'inline'", "preview": "vite preview" }, "dependencies": { @@ -39,4 +38,4 @@ "typescript": "^4.9.3", "vite": "^4.1.0" } -} +}
\ No newline at end of file diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index 879373a0..05b322ff 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -11,6 +11,7 @@ import { setSessionId, setVscMediaUrl, setDataSwitchOn, + setWorkspacePaths, } from "./redux/slices/configSlice"; import { setHighlightedCode } from "./redux/slices/miscSlice"; import { postVscMessage } from "./vscode"; @@ -56,6 +57,7 @@ function App() { dispatch(setSessionId(event.data.sessionId)); dispatch(setVscMediaUrl(event.data.vscMediaUrl)); dispatch(setDataSwitchOn(event.data.dataSwitchOn)); + dispatch(setWorkspacePaths(event.data.workspacePaths)); break; case "highlightedCode": dispatch(setHighlightedCode(event.data.rangeInFile)); diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 472e1b14..4e564000 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -24,7 +24,8 @@ import { setBottomMessage, setBottomMessageCloseTimeout, } from "../redux/slices/uiStateSlice"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; const SEARCH_INDEX_NAME = "continue_context_items"; @@ -136,6 +137,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" }); const client = useContext(GUIClientContext); const dispatch = useDispatch(); + const workspacePaths = useSelector((state: RootStore) => state.config.workspacePaths); const [history, setHistory] = React.useState<string[]>([]); // The position of the current command you are typing now, so the one that will be appended to history once you press enter @@ -181,10 +183,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // Get search results and return setCurrentlyInContextQuery(true); const providerAndQuery = segs[segs.length - 1] || ""; - const [provider, query] = providerAndQuery.split(" "); + // Only return context items from the current workspace - the index is currently shared between all sessions + const workspaceFilter = + workspacePaths && workspacePaths.length > 0 + ? `workspace_dir IN [ ${workspacePaths.map((path) => `"${path}"`).join(", ")} ]` + : undefined; searchClient .index(SEARCH_INDEX_NAME) - .search(providerAndQuery) + .search(providerAndQuery, { + filter: workspaceFilter, + }) .then((res) => { setItems( res.hits.map((hit) => { @@ -410,7 +418,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // Prevent Downshift's default 'Enter' behavior. (event.nativeEvent as any).preventDownshiftDefault = true; - if (props.onEnter) props.onEnter(event); + if (props.onEnter) {props.onEnter(event);} setCurrentlyInContextQuery(false); } else if (event.key === "Tab" && items.length > 0) { downshiftProps.setInputValue(items[0].name); @@ -423,7 +431,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ) { (event.nativeEvent as any).preventDownshiftDefault = true; } else if (event.key === "ArrowUp") { - if (positionInHistory == 0) return; + if (positionInHistory == 0) {return;} else if ( positionInHistory == history.length && (history.length === 0 || @@ -479,7 +487,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { selected={downshiftProps.selectedItem === item} > <span> - {item.name}:{" "} + {item.name} + {" "} <span style={{ color: lightGray }}>{item.description}</span> </span> </Li> diff --git a/extension/react-app/src/redux/slices/configSlice.ts b/extension/react-app/src/redux/slices/configSlice.ts index 59c76066..9cf5402f 100644 --- a/extension/react-app/src/redux/slices/configSlice.ts +++ b/extension/react-app/src/redux/slices/configSlice.ts @@ -7,13 +7,13 @@ export const configSlice = createSlice({ apiUrl: "http://localhost:65432", } as RootStore["config"], reducers: { - setWorkspacePath: ( + setWorkspacePaths: ( state: RootStore["config"], - action: { type: string; payload: string } + action: { type: string; payload: string[] } ) => { return { ...state, - workspacePath: action.payload, + workspacePaths: action.payload, }; }, setApiUrl: ( @@ -57,7 +57,7 @@ export const configSlice = createSlice({ export const { setVscMachineId, setApiUrl, - setWorkspacePath, + setWorkspacePaths, setSessionId, setVscMediaUrl, setDataSwitchOn, diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts index bd60f1c7..a20476b2 100644 --- a/extension/react-app/src/redux/slices/serverStateReducer.ts +++ b/extension/react-app/src/redux/slices/serverStateReducer.ts @@ -9,9 +9,9 @@ const initialState: FullState = { name: "Welcome to Continue", hide: false, description: `- 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 - - [Customize Continue](https://continue.dev/docs/customization) (e.g. use your own API key) by typing '/config'.`, +- Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue +- Use \`/help\` to ask questions about how to use Continue +- [Customize Continue](https://continue.dev/docs/customization) (e.g. use your own API key) by typing '/config'.`, system_message: null, chat_context: [], manage_own_chat_context: false, diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts index a9a45ec1..7959a067 100644 --- a/extension/react-app/src/redux/store.ts +++ b/extension/react-app/src/redux/store.ts @@ -14,7 +14,7 @@ export interface ChatMessage { export interface RootStore { config: { - workspacePath: string | undefined; + workspacePaths: string[] | undefined; apiUrl: string; vscMachineId: string | undefined; sessionId: string | undefined; diff --git a/extension/scripts/package.js b/extension/scripts/package.js new file mode 100644 index 00000000..59da9181 --- /dev/null +++ b/extension/scripts/package.js @@ -0,0 +1,42 @@ +const { exec } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +exec("npm install", (error) => { + if (error) throw error; + console.log("npm install completed"); + + exec("npm run typegen", (error) => { + if (error) throw error; + console.log("npm run typegen completed"); + + process.chdir("react-app"); + + exec("npm install", (error) => { + if (error) throw error; + console.log("npm install in react-app completed"); + + exec("npm run build", (error) => { + if (error) throw error; + if (!fs.existsSync(path.join("dist", "assets", "index.js"))) { + throw new Error("react-app build did not produce index.js"); + } + if (!fs.existsSync(path.join("dist", "assets", "index.css"))) { + throw new Error("react-app build did not produce index.css"); + } + console.log("npm run build in react-app completed"); + + process.chdir(".."); + + if (!fs.existsSync("build")) { + fs.mkdirSync("build"); + } + + exec("vsce package --out ./build patch", (error) => { + if (error) throw error; + console.log("vsce package completed"); + }); + }); + }); + }); +}); diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts index f0e41ca9..748a5984 100644 --- a/extension/src/activation/environmentSetup.ts +++ b/extension/src/activation/environmentSetup.ts @@ -240,7 +240,7 @@ export async function startContinuePythonServer() { windowsHide: true, }); child.stdout.on("data", (data: any) => { - console.log(`stdout: ${data}`); + // console.log(`stdout: ${data}`); }); child.stderr.on("data", (data: any) => { console.log(`stderr: ${data}`); diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index d89093ca..5b9e285d 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -12,7 +12,7 @@ import { rejectSuggestionCommand, } from "./suggestions"; import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; -import * as fs from 'fs'; +import * as fs from "fs"; import { WebsocketMessenger } from "./util/messenger"; import { diffManager } from "./diffs"; const os = require("os"); @@ -30,13 +30,12 @@ class IdeProtocolClient { private _lastReloadTime: number = 16; private _reconnectionTimeouts: NodeJS.Timeout[] = []; - private _sessionId: string | null = null; + sessionId: string | null = null; private _serverUrl: string; private _newWebsocketMessenger() { const requestUrl = - this._serverUrl + - (this._sessionId ? `?session_id=${this._sessionId}` : ""); + this._serverUrl + (this.sessionId ? `?session_id=${this.sessionId}` : ""); const messenger = new WebsocketMessenger(requestUrl); this.messenger = messenger; @@ -383,7 +382,9 @@ class IdeProtocolClient { async getUserSecret(key: string) { // Check if secret already exists in VS Code settings (global) let secret = vscode.workspace.getConfiguration("continue").get(key); - if (typeof secret !== "undefined" && secret !== null) {return secret;} + if (typeof secret !== "undefined" && secret !== null) { + return secret; + } // If not, ask user for secret secret = await vscode.window.showInputBox({ @@ -420,7 +421,7 @@ class IdeProtocolClient { console.log("Getting session ID"); const resp = await this.messenger?.sendAndReceive("getSessionId", {}); console.log("New Continue session with ID: ", resp.sessionId); - this._sessionId = resp.sessionId; + this.sessionId = resp.sessionId; return resp.sessionId; } diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts index 61ff455a..dbec45ea 100644 --- a/extension/src/debugPanel.ts +++ b/extension/src/debugPanel.ts @@ -67,7 +67,7 @@ class WebsocketConnection { export let debugPanelWebview: vscode.Webview | undefined; export function setupDebugPanel( panel: vscode.WebviewPanel | vscode.WebviewView, - sessionIdPromise: Promise<string> | string + sessionIdPromise: Promise<string> ): string { debugPanelWebview = panel.webview; panel.onDidDispose(() => { @@ -180,16 +180,14 @@ export function setupDebugPanel( panel.webview.onDidReceiveMessage(async (data) => { switch (data.type) { case "onLoad": { - let sessionId: string; - if (typeof sessionIdPromise === "string") { - sessionId = sessionIdPromise; - } else { - sessionId = await sessionIdPromise; - } + const sessionId = await sessionIdPromise; panel.webview.postMessage({ type: "onLoad", vscMachineId: vscode.env.machineId, apiUrl: getContinueServerUrl(), + workspacePaths: vscode.workspace.workspaceFolders?.map( + (folder) => folder.uri.fsPath + ), sessionId, vscMediaUrl, dataSwitchOn: vscode.workspace @@ -323,9 +321,9 @@ export class ContinueGUIWebviewViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = "continue.continueGUIView"; - private readonly sessionIdPromise: Promise<string> | string; + private readonly sessionIdPromise: Promise<string>; - constructor(sessionIdPromise: Promise<string> | string) { + constructor(sessionIdPromise: Promise<string>) { this.sessionIdPromise = sessionIdPromise; } diff --git a/extension/src/test-runner/runTestOnVSCodeHost.ts b/extension/src/test-runner/runTestOnVSCodeHost.ts index 2a542ffc..21267c2d 100644 --- a/extension/src/test-runner/runTestOnVSCodeHost.ts +++ b/extension/src/test-runner/runTestOnVSCodeHost.ts @@ -11,7 +11,10 @@ async function main() { // The path to test runner // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(extensionDevelopmentPath, "out/test-runner/mochaRunner"); + const extensionTestsPath = path.resolve( + extensionDevelopmentPath, + "out/test-runner/mochaRunner" + ); // Download VS Code, unzip it and run the integration test await runTests({ extensionDevelopmentPath, extensionTestsPath }); diff --git a/extension/src/test-suite/environmentSetup.test.ts b/extension/src/test-suite/environmentSetup.test.ts index 9a478522..d0406340 100644 --- a/extension/src/test-suite/environmentSetup.test.ts +++ b/extension/src/test-suite/environmentSetup.test.ts @@ -2,18 +2,27 @@ import { test, describe } from "mocha"; import * as assert from "assert"; import { getContinueServerUrl } from "../bridge"; -import { startContinuePythonServer } from "../activation/environmentSetup"; +import { ideProtocolClient } from "../activation/activate"; import fetch from "node-fetch"; +import fkill from "fkill"; describe("Can start python server", () => { test("Can start python server in under 10 seconds", async function () { - this.timeout(17_000); - await startContinuePythonServer(); + const allowedTime = 25_000; + this.timeout(allowedTime + 10_000); + try { + fkill(65432, { force: true, silent: true }); + console.log("Killed existing server"); + } catch (e) { + console.log("No existing server: ", e); + } - await new Promise((resolve) => setTimeout(resolve, 15_000)); + // If successful, the server is started by the extension while we wait + await new Promise((resolve) => setTimeout(resolve, allowedTime)); // Check if server is running const serverUrl = getContinueServerUrl(); + console.log("Server URL: ", serverUrl); const response = await fetch(`${serverUrl}/health`); assert.equal(response.status, 200); }); |