diff options
-rw-r--r-- | .vscode/tasks.json | 16 | ||||
-rw-r--r-- | continuedev/pyproject.toml | 2 | ||||
-rw-r--r-- | continuedev/src/continuedev/core/autopilot.py | 6 | ||||
-rw-r--r-- | continuedev/src/continuedev/core/context.py | 48 | ||||
-rw-r--r-- | continuedev/src/continuedev/plugins/context_providers/file.py | 22 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/meilisearch_server.py | 16 | ||||
-rw-r--r-- | extension/manual-testing-sandbox/nested-folder/helloNested.py | 2 | ||||
-rw-r--r-- | extension/react-app/package.json | 4 | ||||
-rw-r--r-- | extension/react-app/src/App.tsx | 2 | ||||
-rw-r--r-- | extension/react-app/src/components/ComboBox.tsx | 18 | ||||
-rw-r--r-- | extension/react-app/src/redux/slices/configSlice.ts | 8 | ||||
-rw-r--r-- | extension/react-app/src/redux/store.ts | 2 | ||||
-rw-r--r-- | extension/src/debugPanel.ts | 16 |
13 files changed, 106 insertions, 56 deletions
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..71971ddd 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 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/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/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/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/react-app/package.json b/extension/react-app/package.json index b4762990..38beb742 100644 --- a/extension/react-app/package.json +++ b/extension/react-app/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build --sourcemap", + "build": "tsc && vite build --sourcemap 'inline'", "preview": "vite preview" }, "dependencies": { @@ -39,4 +39,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..c75f9ee6 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 || 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/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/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; } |