diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-09-28 01:02:52 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-28 01:02:52 -0700 |
commit | 95363a5b52f3bf73531ac76b00178fa79ca97661 (patch) | |
tree | 9b9c1614556f1f0d21f363e6a9fe950069affb5d | |
parent | d4acf4bb11dbd7d3d6210e2949d21143d721e81e (diff) | |
download | sncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.tar.gz sncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.tar.bz2 sncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.zip |
Past input (#513)
* feat: :construction: use ComboBox in place of UserInputContainer
* feat: :construction: adding context to previous inputs steps
* feat: :sparkles: preview context items on click
* feat: :construction: more work on context items ui
* style: :construction: working out the details of ctx item buttons
* feat: :sparkles: getting the final details
* fix: :bug: fix height of ctx items bar
* fix: :bug: last couple of details
* fix: :bug: pass model param through to hf inference api
* fix: :loud_sound: better logging for timeout
* feat: :sparkles: option to set the meilisearch url
* fix: :bug: fix height of past inputs
41 files changed, 1433 insertions, 551 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index 674c23a4..88b9537a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,7 +37,11 @@ "type": "python", "request": "launch", "module": "continuedev.src.continuedev.__main__", - "args": ["--port", "8001"], + "args": [ + "--port", + "8001" + // "--meilisearch-url", "http://localhost:7701" + ], "justMyCode": false, "subProcess": false // Does it need a build task? diff --git a/continuedev/src/continuedev/__main__.py b/continuedev/src/continuedev/__main__.py index 1974d87c..caaba117 100644 --- a/continuedev/src/continuedev/__main__.py +++ b/continuedev/src/continuedev/__main__.py @@ -12,6 +12,9 @@ app = typer.Typer() def main( port: int = typer.Option(65432, help="server port"), host: str = typer.Option("127.0.0.1", help="server host"), + meilisearch_url: Optional[str] = typer.Option( + None, help="The URL of the MeiliSearch server if running manually" + ), config: Optional[str] = typer.Option( None, help="The path to the configuration file" ), @@ -20,7 +23,7 @@ def main( if headless: run(config) else: - run_server(port=port, host=host) + run_server(port=port, host=host, meilisearch_url=meilisearch_url) if __name__ == "__main__": diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 0155e755..9ebf288b 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -37,7 +37,7 @@ from ..plugins.steps.core.core import ( ) from ..plugins.steps.on_traceback import DefaultOnTracebackStep from ..server.ide_protocol import AbstractIdeProtocolServer -from ..server.meilisearch_server import stop_meilisearch +from ..server.meilisearch_server import get_meilisearch_url, stop_meilisearch from .config import ContinueConfig from .context import ContextManager from .main import ( @@ -179,6 +179,7 @@ class Autopilot(ContinueBaseModel): config=self.continue_sdk.config, saved_context_groups=self._saved_context_groups, context_providers=self.context_manager.get_provider_descriptions(), + meilisearch_url=get_meilisearch_url(), ) self.full_state = full_state return full_state @@ -306,7 +307,8 @@ class Autopilot(ContinueBaseModel): await self.update_subscribers() async def edit_step_at_index(self, user_input: str, index: int): - step_to_rerun = self.history.timeline[index].step.copy() + node_to_rerun = self.history.timeline[index].copy() + step_to_rerun = node_to_rerun.step step_to_rerun.user_input = user_input step_to_rerun.description = user_input @@ -318,13 +320,29 @@ class Autopilot(ContinueBaseModel): node_to_delete.deleted = True self.history.current_index = index - 1 + + # Set the context to the context used by that step + await self.context_manager.clear_context() + for context_item in node_to_rerun.context_used: + await self.context_manager.manually_add_context_item(context_item) + await self.update_subscribers() # Rerun from the current step await self.run_from_step(step_to_rerun) - async def delete_context_with_ids(self, ids: List[str]): - await self.context_manager.delete_context_with_ids(ids) + async def delete_context_with_ids( + self, ids: List[str], index: Optional[int] = None + ): + if index is None: + await self.context_manager.delete_context_with_ids(ids) + else: + self.history.timeline[index].context_used = list( + filter( + lambda item: item.description.id.to_string() not in ids, + self.history.timeline[index].context_used, + ) + ) await self.update_subscribers() async def toggle_adding_highlighted_code(self): @@ -380,7 +398,12 @@ class Autopilot(ContinueBaseModel): # Update history - do this first so we get top-first tree ordering index_of_history_node = self.history.add_node( - HistoryNode(step=step, observation=None, depth=self._step_depth) + HistoryNode( + step=step, + observation=None, + depth=self._step_depth, + context_used=await self.context_manager.get_selected_items(), + ) ) # Call all subscribed callbacks @@ -600,7 +623,7 @@ class Autopilot(ContinueBaseModel): async def accept_user_input(self, user_input: str): self._main_user_input_queue.append(user_input) - await self.update_subscribers() + # await self.update_subscribers() if len(self._main_user_input_queue) > 1: return @@ -609,7 +632,7 @@ class Autopilot(ContinueBaseModel): # Just run the step that takes user input, and # then up to the policy to decide how to deal with it. self._main_user_input_queue.pop(0) - await self.update_subscribers() + # await self.update_subscribers() await self.run_from_step(UserInputStep(user_input=user_input)) while len(self._main_user_input_queue) > 0: @@ -635,6 +658,16 @@ class Autopilot(ContinueBaseModel): await self.context_manager.select_context_item(id, query) await self.update_subscribers() + async def select_context_item_at_index(self, id: str, query: str, index: int): + # TODO: This is different from how it works for the main input + # Ideally still tracked through the ContextProviders + # so they can watch for duplicates + context_item = await self.context_manager.get_context_item(id, query) + if context_item is None: + return + self.history.timeline[index].context_used.append(context_item) + await self.update_subscribers() + async def set_config_attr(self, key_path: List[str], value: redbaron.RedBaron): edit_config_property(key_path, value) await self.update_subscribers() diff --git a/continuedev/src/continuedev/core/context.py b/continuedev/src/continuedev/core/context.py index f2658602..d374dd02 100644 --- a/continuedev/src/continuedev/core/context.py +++ b/continuedev/src/continuedev/core/context.py @@ -10,7 +10,11 @@ from ..libs.util.create_async_task import create_async_task from ..libs.util.devdata import dev_data_logger from ..libs.util.logging import logger from ..libs.util.telemetry import posthog_logger -from ..server.meilisearch_server import poll_meilisearch_running, restart_meilisearch +from ..server.meilisearch_server import ( + get_meilisearch_url, + poll_meilisearch_running, + restart_meilisearch, +) from .main import ( ChatMessage, ContextItem, @@ -127,7 +131,7 @@ class ContextProvider(BaseModel): Default implementation uses the search index to get the item. """ - async with Client("http://localhost:7700") as search_client: + async with Client(get_meilisearch_url()) as search_client: try: result = await search_client.index(SEARCH_INDEX_NAME).get_document( id.to_string() @@ -295,7 +299,7 @@ class ContextManager: } for item in context_items ] - async with Client("http://localhost:7700") as search_client: + async with Client(get_meilisearch_url()) as search_client: async def add_docs(): index = await search_client.get_index(SEARCH_INDEX_NAME) @@ -313,7 +317,7 @@ class ContextManager: """ Deletes the documents in the search index. """ - async with Client("http://localhost:7700") as search_client: + async with Client(get_meilisearch_url()) as search_client: await asyncio.wait_for( search_client.index(SEARCH_INDEX_NAME).delete_documents(ids), timeout=20, @@ -321,7 +325,7 @@ class ContextManager: async def load_index(self, workspace_dir: str, should_retry: bool = True): try: - async with Client("http://localhost:7700") as search_client: + async with Client(get_meilisearch_url()) as search_client: # First, create the index if it doesn't exist # The index is currently shared by all workspaces await search_client.create_index(SEARCH_INDEX_NAME) @@ -422,6 +426,18 @@ class ContextManager: ) await self.context_providers[id.provider_title].add_context_item(id, query) + async def get_context_item(self, id: str, query: str) -> ContextItem: + """ + Returns the ContextItem with the given id. + """ + id: ContextItemId = ContextItemId.from_string(id) + if id.provider_title not in self.provider_titles: + raise ValueError( + f"Context provider with title {id.provider_title} not found" + ) + + return await self.context_providers[id.provider_title].get_item(id, query) + async def delete_context_with_ids(self, ids: List[str]): """ Deletes the ContextItems with the given IDs, lets ContextProviders recalculate. diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py index cf41aab9..617a5aaa 100644 --- a/continuedev/src/continuedev/core/main.py +++ b/continuedev/src/continuedev/core/main.py @@ -108,6 +108,7 @@ class HistoryNode(ContinueBaseModel): deleted: bool = False active: bool = True logs: List[str] = [] + context_used: List["ContextItem"] = [] def to_chat_messages(self) -> List[ChatMessage]: if self.step.description is None or self.step.manage_own_chat_context: @@ -312,6 +313,7 @@ class FullState(ContinueBaseModel): config: ContinueConfig saved_context_groups: Dict[str, List[ContextItem]] = {} context_providers: List[ContextProviderDescription] = [] + meilisearch_url: Optional[str] = None class ContinueSDK: diff --git a/continuedev/src/continuedev/libs/constants/default_config.py b/continuedev/src/continuedev/libs/constants/default_config.py index a1b2de2c..92913001 100644 --- a/continuedev/src/continuedev/libs/constants/default_config.py +++ b/continuedev/src/continuedev/libs/constants/default_config.py @@ -31,24 +31,24 @@ config = ContinueConfig( custom_commands=[ CustomCommand( name="test", - description="Write unit tests for the highlighted code", + description="Write unit tests for highlighted code", prompt="Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated. Give the tests just as chat output, don't edit any file.", ) ], slash_commands=[ SlashCommand( name="edit", - description="Edit code in the current file or the highlighted code", + description="Edit highlighted code", step=EditHighlightedCodeStep, ), SlashCommand( name="config", - description="Customize Continue - slash commands, LLMs, system message, etc.", + description="Customize Continue", step=OpenConfigStep, ), SlashCommand( name="comment", - description="Write comments for the current file or highlighted code", + description="Write comments for the highlighted code", step=CommentCodeStep, ), SlashCommand( @@ -58,7 +58,7 @@ config = ContinueConfig( ), SlashCommand( name="share", - description="Download and share the session transcript", + description="Download and share this session", step=ShareSessionStep, ), SlashCommand( diff --git a/continuedev/src/continuedev/libs/llm/hf_inference_api.py b/continuedev/src/continuedev/libs/llm/hf_inference_api.py index a7771018..ab1482e8 100644 --- a/continuedev/src/continuedev/libs/llm/hf_inference_api.py +++ b/continuedev/src/continuedev/libs/llm/hf_inference_api.py @@ -57,16 +57,15 @@ class HuggingFaceInferenceAPI(LLM): if "stop" in args: args["stop_sequences"] = args["stop"] del args["stop"] - if "model" in args: - del args["model"] + return args async def _stream_complete(self, prompt, options): - self.collect_args(options) + args = self.collect_args(options) client = InferenceClient(self.endpoint_url, token=self.hf_token) - stream = client.text_generation(prompt, stream=True, details=True) + stream = client.text_generation(prompt, stream=True, details=True, **args) for r in stream: # skip special tokens diff --git a/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py b/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py index df82b1ab..bd31531e 100644 --- a/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py +++ b/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py @@ -257,8 +257,17 @@ class HighlightedCodeContextProvider(ContextProvider): self._disambiguate_highlighted_ranges() async def set_editing_at_ids(self, ids: List[str]): + # Don't do anything if there are no valid ids here + count = 0 for hr in self.highlighted_ranges: - hr.item.editing = hr.item.description.id.to_string() in ids + if hr.item.description.id.item_id in ids: + count += 1 + + if count == 0: + return + + for hr in self.highlighted_ranges: + hr.item.editing = hr.item.description.id.item_id in ids async def add_context_item( self, id: ContextItemId, query: str, prev: List[ContextItem] = None diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index 9d2ea47a..10f6974f 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -104,7 +104,7 @@ class GUIProtocolServer: elif message_type == "delete_at_index": self.on_delete_at_index(data["index"]) elif message_type == "delete_context_with_ids": - self.on_delete_context_with_ids(data["ids"]) + self.on_delete_context_with_ids(data["ids"], data.get("index", None)) elif message_type == "toggle_adding_highlighted_code": self.on_toggle_adding_highlighted_code() elif message_type == "set_editing_at_ids": @@ -112,9 +112,11 @@ class GUIProtocolServer: elif message_type == "show_logs_at_index": self.on_show_logs_at_index(data["index"]) elif message_type == "show_context_virtual_file": - self.show_context_virtual_file() + self.show_context_virtual_file(data.get("index", None)) elif message_type == "select_context_item": self.select_context_item(data["id"], data["query"]) + elif message_type == "select_context_item_at_index": + self.select_context_item_at_index(data["id"], data["query"], data["index"]) elif message_type == "load_session": self.load_session(data.get("session_id", None)) elif message_type == "edit_step_at_index": @@ -171,9 +173,9 @@ class GUIProtocolServer: self.on_error, ) - def on_delete_context_with_ids(self, ids: List[str]): + def on_delete_context_with_ids(self, ids: List[str], index: Optional[int] = None): create_async_task( - self.session.autopilot.delete_context_with_ids(ids), self.on_error + self.session.autopilot.delete_context_with_ids(ids, index), self.on_error ) def on_toggle_adding_highlighted_code(self): @@ -188,7 +190,7 @@ class GUIProtocolServer: def on_show_logs_at_index(self, index: int): name = "Continue Context" logs = "\n\n############################################\n\n".join( - ["This is the prompt sent to the LLM during this step"] + ["This is the prompt that was sent to the LLM during this step"] + self.session.autopilot.continue_sdk.history.timeline[index].logs ) create_async_task( @@ -196,12 +198,20 @@ class GUIProtocolServer: ) posthog_logger.capture_event("show_logs_at_index", {}) - def show_context_virtual_file(self): + def show_context_virtual_file(self, index: Optional[int] = None): async def async_stuff(): - msgs = await self.session.autopilot.continue_sdk.get_chat_context() + if index is None: + context_items = ( + await self.session.autopilot.context_manager.get_selected_items() + ) + elif index < len(self.session.autopilot.continue_sdk.history.timeline): + context_items = self.session.autopilot.continue_sdk.history.timeline[ + index + ].context_used + ctx = "\n\n-----------------------------------\n\n".join( - ["This is the exact context that will be passed to the LLM"] - + list(map(lambda x: x.content, msgs)) + ["These are the context items that will be passed to the LLM"] + + list(map(lambda x: x.content, context_items)) ) await self.session.autopilot.ide.showVirtualFile( "Continue - Selected Context", ctx @@ -218,6 +228,13 @@ class GUIProtocolServer: self.session.autopilot.select_context_item(id, query), self.on_error ) + def select_context_item_at_index(self, id: str, query: str, index: int): + """Called when user selects an item from the dropdown for prev UserInputStep""" + create_async_task( + self.session.autopilot.select_context_item_at_index(id, query, index), + self.on_error, + ) + def load_session(self, session_id: Optional[str] = None): async def load_and_tell_to_reconnect(): new_session_id = await session_manager.load_session( diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index d4f0690b..32bd0f0c 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -12,6 +12,7 @@ from pydantic import BaseModel from starlette.websockets import WebSocketDisconnect, WebSocketState from uvicorn.main import Server +from ..core.main import ContinueCustomException from ..libs.util.create_async_task import create_async_task from ..libs.util.devdata import dev_data_logger from ..libs.util.logging import logger @@ -39,7 +40,6 @@ from ..models.filesystem_edit import ( from ..plugins.steps.core.core import DisplayErrorStep from .gui import session_manager from .ide_protocol import AbstractIdeProtocolServer -from .meilisearch_server import start_meilisearch from .session_manager import SessionManager nest_asyncio.apply() @@ -201,21 +201,24 @@ class IdeProtocolServer(AbstractIdeProtocolServer): except RuntimeError as e: logger.warning(f"Error sending IDE message, websocket probably closed: {e}") - async def _receive_json(self, message_type: str, timeout: int = 20) -> Any: + async def _receive_json( + self, message_type: str, timeout: int = 20, message=None + ) -> Any: try: return await asyncio.wait_for( self.sub_queue.get(message_type), timeout=timeout ) except asyncio.TimeoutError: - raise Exception( - f"IDE Protocol _receive_json timed out after 20 seconds: {message_type}" + raise ContinueCustomException( + title=f"IDE Protocol _receive_json timed out after 20 seconds: {message_type}", + message=f"IDE Protocol _receive_json timed out after 20 seconds. The message sent was: {message or ''}", ) async def _send_and_receive_json( self, data: Any, resp_model: Type[T], message_type: str ) -> T: await self._send_json(message_type, data) - resp = await self._receive_json(message_type) + resp = await self._receive_json(message_type, message=data) return resp_model.parse_obj(resp) async def handle_json(self, message_type: str, data: Any): @@ -597,17 +600,6 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str = None): logger.debug(f"Accepted websocket connection from {websocket.client}") await websocket.send_json({"messageType": "connected", "data": {}}) - # Start meilisearch - try: - - async def on_err(e): - logger.debug(f"Failed to start MeiliSearch: {e}") - - create_async_task(start_meilisearch(), on_err) - except Exception as e: - logger.debug("Failed to start MeiliSearch") - logger.debug(e) - # Message handler def handle_msg(msg): message = json.loads(msg) diff --git a/continuedev/src/continuedev/server/main.py b/continuedev/src/continuedev/server/main.py index bbae2bb2..aa6c8944 100644 --- a/continuedev/src/continuedev/server/main.py +++ b/continuedev/src/continuedev/server/main.py @@ -1,6 +1,8 @@ import argparse import asyncio import atexit +from contextlib import asynccontextmanager +from typing import Optional import uvicorn from fastapi import FastAPI @@ -9,10 +11,21 @@ from fastapi.middleware.cors import CORSMiddleware from ..libs.util.logging import logger from .gui import router as gui_router from .ide import router as ide_router +from .meilisearch_server import start_meilisearch, stop_meilisearch from .session_manager import router as sessions_router from .session_manager import session_manager -app = FastAPI() +meilisearch_url_global = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await start_meilisearch(url=meilisearch_url_global) + yield + stop_meilisearch() + + +app = FastAPI(lifespan=lifespan) app.include_router(ide_router) app.include_router(gui_router) @@ -34,7 +47,13 @@ def health(): return {"status": "ok"} -def run_server(port: int = 65432, host: str = "127.0.0.1"): +def run_server( + port: int = 65432, host: str = "127.0.0.1", meilisearch_url: Optional[str] = None +): + global meilisearch_url_global + + meilisearch_url_global = meilisearch_url + config = uvicorn.Config(app, host=host, port=port) server = uvicorn.Server(config) server.run() diff --git a/continuedev/src/continuedev/server/meilisearch_server.py b/continuedev/src/continuedev/server/meilisearch_server.py index 5e6cdd53..8929b69d 100644 --- a/continuedev/src/continuedev/server/meilisearch_server.py +++ b/continuedev/src/continuedev/server/meilisearch_server.py @@ -2,9 +2,11 @@ import asyncio import os import shutil import subprocess +from typing import Optional import aiofiles import aiohttp +import psutil from meilisearch_python_async import Client from ..libs.util.logging import logger @@ -89,13 +91,22 @@ async def ensure_meilisearch_installed() -> bool: return True +meilisearch_process = None +DEFAULT_MEILISEARCH_URL = "http://localhost:7700" +meilisearch_url = DEFAULT_MEILISEARCH_URL + + +def get_meilisearch_url(): + return meilisearch_url + + async def check_meilisearch_running() -> bool: """ Checks if MeiliSearch is running. """ try: - async with Client("http://localhost:7700") as client: + async with Client(meilisearch_url) as client: try: resp = await client.health() if resp.status != "available": @@ -117,14 +128,16 @@ async def poll_meilisearch_running(frequency: int = 0.1) -> bool: await asyncio.sleep(frequency) -meilisearch_process = None - - -async def start_meilisearch(): +async def start_meilisearch(url: Optional[str] = None): """ Starts the MeiliSearch server, wait for it. """ - global meilisearch_process + global meilisearch_process, meilisearch_url + + if url is not None: + logger.debug("Using MeiliSearch at URL: " + url) + meilisearch_url = url + return serverPath = getServerFolderPath() @@ -157,9 +170,6 @@ def stop_meilisearch(): meilisearch_process = None -import psutil - - def kill_proc(port): for proc in psutil.process_iter(): try: @@ -180,4 +190,4 @@ def kill_proc(port): async def restart_meilisearch(): stop_meilisearch() kill_proc(7700) - await start_meilisearch() + await start_meilisearch(url=meilisearch_url) diff --git a/extension/package.json b/extension/package.json index 3792146d..4cf39677 100644 --- a/extension/package.json +++ b/extension/package.json @@ -51,7 +51,7 @@ "continue.serverUrl": { "type": "string", "default": "http://localhost:65432", - "description": "The URL of the Continue server. Only change this if you are running the server manually. If you want to use an LLM hosted at a custom URL, please see https://continue.dev/docs/customization#change-the-default-llm. All other configuration is done in `~/.continue/config.py`, which you can access by using the '/config' slash command." + "description": "The URL of the Continue server if you are running Continue manually. NOTE: This is NOT the URL of the LLM server. If you want to use an LLM hosted at a custom URL, please see https://continue.dev/docs/customization#change-the-default-llm and complete configuration in `~/.continue/config.py`, which you can access by using the '/config' slash command." }, "continue.manuallyRunningServer": { "type": "boolean", diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 934b7337..6c99a650 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -7,7 +7,7 @@ import React, { useState, } from "react"; import { useCombobox } from "downshift"; -import styled from "styled-components"; +import styled, { keyframes } from "styled-components"; import { buttonColor, defaultBorderRadius, @@ -21,8 +21,10 @@ import HeaderButtonWithText from "./HeaderButtonWithText"; import { ArrowLeftIcon, ArrowRightIcon, - MagnifyingGlassIcon, + ArrowUpLeftIcon, + StopCircleIcon, TrashIcon, + XMarkIcon, } from "@heroicons/react/24/outline"; import { postVscMessage } from "../vscode"; import { GUIClientContext } from "../App"; @@ -31,12 +33,58 @@ import { setBottomMessage } from "../redux/slices/uiStateSlice"; import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import ContinueButton from "./ContinueButton"; -import { getFontSize } from "../util"; +import { + getFontSize, + getMarkdownLanguageTagForFile, + getMetaKeyLabel, +} from "../util"; +import { ContextItem } from "../../../schema/FullState"; +import StyledMarkdownPreview from "./StyledMarkdownPreview"; const SEARCH_INDEX_NAME = "continue_context_items"; // #region styled components +const gradient = keyframes` + 0% { + background-position: 0px 0; + } + 100% { + background-position: 100em 0; + } +`; + +const GradientBorder = styled.div<{ + borderRadius?: string; + borderColor?: string; + isFirst: boolean; + isLast: boolean; + loading: boolean; +}>` + border-radius: ${(props) => props.borderRadius || "0"}; + padding: 1px; + background: ${(props) => + props.borderColor + ? props.borderColor + : `repeating-linear-gradient( + 101.79deg, + #1BBE84 0%, + #331BBE 16%, + #BE1B55 33%, + #A6BE1B 55%, + #BE1B55 67%, + #331BBE 85%, + #1BBE84 99% + )`}; + animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite; + background-size: 200% 200%; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + margin-top: 8px; +`; + const HiddenHeaderButtonWithText = styled.button` opacity: 0; background-color: transparent; @@ -75,7 +123,7 @@ const MainTextInput = styled.textarea<{ font-size: ${(props) => props.fontSize || mainInputFontSize}px; font-family: inherit; border-radius: ${defaultBorderRadius}; - margin: 8px auto; + margin: 0; height: auto; width: 100%; background-color: ${secondaryDark}; @@ -98,6 +146,15 @@ const MainTextInput = styled.textarea<{ } `; +const DeleteButtonDiv = styled.div` + position: absolute; + top: 14px; + right: 12px; + background-color: ${secondaryDark}; + border-radius: ${defaultBorderRadius}; + z-index: 100; +`; + const DynamicQueryTitleDiv = styled.div` position: absolute; right: 0px; @@ -119,12 +176,14 @@ const Ul = styled.ul<{ ulHeightPixels: number; inputBoxHeight?: string; fontSize?: number; + isMainInput: boolean; }>` ${(props) => props.showAbove ? `transform: translateY(-${props.ulHeightPixels + 8}px);` : `transform: translateY(${ - 5 * (props.fontSize || mainInputFontSize) - 2 + (props.isMainInput ? 5 : 4) * (props.fontSize || mainInputFontSize) - + (props.isMainInput ? 2 : 4) }px);`} position: absolute; background: ${vscBackground}; @@ -137,11 +196,11 @@ const Ul = styled.ul<{ ${({ hidden }) => hidden && "display: none;"} border-radius: ${defaultBorderRadius}; outline: 0.5px solid ${lightGray}; - z-index: 2; -ms-overflow-style: none; font-size: ${(props) => props.fontSize || mainInputFontSize}px; scrollbar-width: none; /* Firefox */ + z-index: 500; /* Hide scrollbar for Chrome, Safari and Opera */ &::-webkit-scrollbar { @@ -165,6 +224,7 @@ const Li = styled.li<{ ${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"} /* border-top: 1px solid gray; */ cursor: pointer; + z-index: 500; `; // #endregion @@ -176,14 +236,39 @@ interface ComboBoxItem { content?: string; } interface ComboBoxProps { - onInputValueChange: (inputValue: string) => void; + onInputValueChange?: (inputValue: string) => void; disabled?: boolean; - onEnter: (e?: React.KeyboardEvent<HTMLInputElement>) => void; - onToggleAddContext: () => void; + onEnter?: (e?: React.KeyboardEvent<HTMLInputElement>, value?: string) => void; + onToggleAddContext?: () => void; + + isMainInput: boolean; + value?: string; + active?: boolean; + groupIndices?: number[]; + onToggle?: (arg0: boolean) => void; + onToggleAll?: (arg0: boolean) => void; + isToggleOpen?: boolean; + index?: number; + onDelete?: () => void; } const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { - const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" }); + const meilisearchUrl = useSelector( + (state: RootStore) => + state.serverState.meilisearch_url || "http://127.0.0.1:7700" + ); + + const [searchClient, setSearchClient] = useState<MeiliSearch | undefined>( + undefined + ); + + useEffect(() => { + const client = new MeiliSearch({ + host: meilisearchUrl, + }); + setSearchClient(client); + }, [meilisearchUrl]); + const client = useContext(GUIClientContext); const dispatch = useDispatch(); const workspacePaths = useSelector( @@ -197,6 +282,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const inputRef = React.useRef<HTMLInputElement>(null); + useEffect(() => { + if (!inputRef.current) return; + if (inputRef.current.scrollHeight > inputRef.current.clientHeight) { + inputRef.current.style.height = "auto"; + inputRef.current.style.height = inputRef.current.scrollHeight + "px"; + } + }, [inputRef.current, props.value]); + // Whether the current input follows an '@' and should be treated as context query const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false); const [nestedContextProvider, setNestedContextProvider] = useState< @@ -206,9 +299,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { any | undefined >(undefined); - const sessionId = useSelector( - (state: RootStore) => state.serverState.session_info?.session_id - ); const availableSlashCommands = useSelector( (state: RootStore) => state.serverState.slash_commands ).map((cmd) => { @@ -217,15 +307,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { description: cmd.description, }; }); - const selectedContextItems = useSelector( - (state: RootStore) => state.serverState.selected_context_items - ); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); + const selectedContextItems = useSelector((state: RootStore) => { + if (props.index) { + return state.serverState.history.timeline[props.index].context_used || []; + } else { + return state.serverState.selected_context_items; } - }, [sessionId, inputRef.current]); + }); + const timeline = useSelector( + (state: RootStore) => state.serverState.history.timeline + ); useEffect(() => { if (!currentlyInContextQuery) { @@ -287,7 +378,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { setInQueryForContextProvider(undefined); } - props.onInputValueChange(inputValue); + props.onInputValueChange?.(inputValue); // Handle context selection if (inputValue.endsWith("@") || currentlyInContextQuery) { @@ -365,7 +456,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { .join(", ")} ] AND provider_name = '${provider}'` : undefined; try { - const res = await searchClient.index(SEARCH_INDEX_NAME).search(query, { + const res = await searchClient?.index(SEARCH_INDEX_NAME).search(query, { filter: workspaceFilter, }); return ( @@ -410,13 +501,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { useImperativeHandle(ref, () => downshiftProps, [downshiftProps]); const contextItemsDivRef = React.useRef<HTMLDivElement>(null); - const handleTabPressed = () => { + const handleTabPressed = useCallback(() => { + setShowContextItemsIfNotMain(true); // Set the focus to the next item in the context items div if (!contextItemsDivRef.current) { return; } - const focusableItems = - contextItemsDivRef.current.querySelectorAll(".pill-button"); + const focusableItems = contextItemsDivRef.current.querySelectorAll( + `.pill-button-${props.index || "main"}` + ); const focusableItemsArray = Array.from(focusableItems); const focusedItemIndex = focusableItemsArray.findIndex( (item) => item === document.activeElement @@ -433,22 +526,30 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const firstItem = focusableItemsArray[0]; (firstItem as any)?.focus(); } - }; + }, [props.index]); useEffect(() => { - if (typeof window !== "undefined") { + if (inputRef.current) { const listener = (e: any) => { if (e.key === "Tab") { e.preventDefault(); handleTabPressed(); } }; - window.addEventListener("keydown", listener); + inputRef.current.addEventListener("keydown", listener); return () => { - window.removeEventListener("keydown", listener); + inputRef.current?.removeEventListener("keydown", listener); }; } - }, []); + }, [inputRef.current]); + + useEffect(() => { + if (props.value) { + downshiftProps.setInputValue(props.value); + } + }, [props.value, downshiftProps.setInputValue]); + + const [isHovered, setIsHovered] = useState(false); useLayoutEffect(() => { if (!ulRef.current) { @@ -458,7 +559,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { }, [items, downshiftProps.setHighlightedIndex, ulRef.current]); const [metaKeyPressed, setMetaKeyPressed] = useState(false); - const [focused, setFocused] = useState(false); + const [inputFocused, setInputFocused] = useState(false); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Meta") { @@ -479,10 +580,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { }, []); useEffect(() => { - if (!inputRef.current) { + if (!inputRef.current || !props.isMainInput) { return; } - inputRef.current.focus(); + if (props.isMainInput) { + inputRef.current.focus(); + } const handler = (event: any) => { if (event.data.type === "focusContinueInput") { inputRef.current!.focus(); @@ -498,7 +601,20 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { return () => { window.removeEventListener("message", handler); }; - }, [inputRef.current]); + }, [inputRef.current, props.isMainInput]); + + const deleteButtonDivRef = React.useRef<HTMLDivElement>(null); + + const selectContextItem = useCallback( + (id: string, query: string) => { + if (props.isMainInput) { + client?.selectContextItem(id, query); + } else if (props.index) { + client?.selectContextItemAtIndex(id, query, props.index); + } + }, + [client, props.index] + ); const selectContextItemFromDropdown = useCallback( (event: any) => { @@ -511,7 +627,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { if (!newProvider) { if (nestedContextProvider && newItem.id) { // Tell server the context item was selected - client?.selectContextItem(newItem.id, ""); + selectContextItem(newItem.id, ""); // Clear the input downshiftProps.setInputValue(""); @@ -542,7 +658,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const query = segs[segs.length - 1]; // Tell server the context item was selected - client?.selectContextItem(newItem.id, query); + selectContextItem(newItem.id, query); if (downshiftProps.inputValue.includes("@")) { const selectedNestedContextProvider = contextProviders.find( (provider) => provider.title === newItem.id @@ -582,221 +698,428 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { contextProviders, nestedContextProvider, downshiftProps.inputValue, + selectContextItem, ] ); const [isComposing, setIsComposing] = useState(false); + const [previewingContextItem, setPreviewingContextItem] = useState< + ContextItem | undefined + >(undefined); + + const [focusedContextItem, setFocusedContextItem] = useState< + ContextItem | undefined + >(undefined); + + const topRef = React.useRef<HTMLDivElement>(null); + + const [showContextItemsIfNotMain, setShowContextItemsIfNotMain] = + useState(false); + + useEffect(() => { + if (!inputFocused) { + setShowContextItemsIfNotMain(false); + } + }, [inputFocused]); + return ( - <> - <div - className="px-2 flex gap-2 items-center flex-wrap mt-2" - ref={contextItemsDivRef} - > - <HiddenHeaderButtonWithText - className={selectedContextItems.length > 0 ? "pill-button" : ""} - onClick={() => { - client?.deleteContextWithIds( - selectedContextItems.map((item) => item.description.id) - ); - inputRef.current?.focus(); - }} - onKeyDown={(e: any) => { - if (e.key === "Backspace") { + <div ref={topRef}> + {props.isMainInput || + (selectedContextItems.length > 0 && showContextItemsIfNotMain) ? ( + <div + className="px-2 flex gap-2 items-center flex-wrap" + ref={contextItemsDivRef} + style={{ backgroundColor: vscBackground }} + > + <HiddenHeaderButtonWithText + className={ + selectedContextItems.length > 0 + ? `pill-button-${props.index || "main"}` + : "" + } + onClick={() => { client?.deleteContextWithIds( - selectedContextItems.map((item) => item.description.id) + selectedContextItems.map((item) => item.description.id), + props.index ); inputRef.current?.focus(); - } - }} - > - <TrashIcon width="1.4em" height="1.4em" /> - </HiddenHeaderButtonWithText> - {selectedContextItems.map((item, idx) => { - return ( - <PillButton - areMultipleItems={selectedContextItems.length > 1} - key={`${item.description.id.item_id}${idx}`} - item={item} - editing={ - item.editing && - (inputRef.current as any)?.value?.startsWith("/edit") - } - editingAny={(inputRef.current as any)?.value?.startsWith("/edit")} - index={idx} - onDelete={() => { - client?.deleteContextWithIds([item.description.id]); + }} + onKeyDown={(e: any) => { + if (e.key === "Backspace") { + client?.deleteContextWithIds( + selectedContextItems.map((item) => item.description.id), + props.index + ); inputRef.current?.focus(); - }} - /> - ); - })} + setPreviewingContextItem(undefined); + setFocusedContextItem(undefined); + } + }} + > + <TrashIcon width="1.4em" height="1.4em" /> + </HiddenHeaderButtonWithText> + {(props.isMainInput + ? selectedContextItems + : timeline[props.index!].context_used || [] + ).map((item, idx) => { + return ( + <PillButton + areMultipleItems={selectedContextItems.length > 1} + key={`${item.description.id.item_id}${idx}`} + item={item} + editing={ + item.editing && + (inputRef.current as any)?.value?.startsWith("/edit") + } + editingAny={(inputRef.current as any)?.value?.startsWith( + "/edit" + )} + stepIndex={props.index} + index={idx} + onDelete={() => { + client?.deleteContextWithIds( + [item.description.id], + props.index + ); + inputRef.current?.focus(); + if ( + (item.description.id.item_id === + focusedContextItem?.description.id.item_id && + focusedContextItem?.description.id.provider_name === + item.description.id.provider_name) || + (item.description.id.item_id === + previewingContextItem?.description.id.item_id && + previewingContextItem?.description.id.provider_name === + item.description.id.provider_name) + ) { + setPreviewingContextItem(undefined); + setFocusedContextItem(undefined); + } + }} + onClick={(e) => { + if ( + item.description.id.item_id === + focusedContextItem?.description.id.item_id && + focusedContextItem?.description.id.provider_name === + item.description.id.provider_name + ) { + setFocusedContextItem(undefined); + } else { + setFocusedContextItem(item); + } + }} + onBlur={() => { + setFocusedContextItem(undefined); + }} + toggleViewContent={() => { + setPreviewingContextItem((prev) => { + if (!prev) return item; + if ( + prev.description.id.item_id === + item.description.id.item_id && + prev.description.id.provider_name === + item.description.id.provider_name + ) { + return undefined; + } else { + return item; + } + }); + }} + previewing={ + item.description.id.item_id === + previewingContextItem?.description.id.item_id && + previewingContextItem?.description.id.provider_name === + item.description.id.provider_name + } + focusing={ + item.description.id.item_id === + focusedContextItem?.description.id.item_id && + focusedContextItem?.description.id.provider_name === + item.description.id.provider_name + } + /> + ); + })} - {selectedContextItems.length > 0 && ( + {/* {selectedContextItems.length > 0 && ( <HeaderButtonWithText onClick={() => { - client?.showContextVirtualFile(); + client?.showContextVirtualFile(props.index); }} text="View Current Context" > <MagnifyingGlassIcon width="1.4em" height="1.4em" /> </HeaderButtonWithText> - )} - </div> + )} */} + </div> + ) : ( + selectedContextItems.length > 0 && ( + <div + onClick={() => { + inputRef.current?.focus(); + setShowContextItemsIfNotMain(true); + }} + style={{ + color: lightGray, + fontSize: "10px", + backgroundColor: vscBackground, + paddingLeft: "12px", + cursor: "default", + paddingTop: getFontSize(), + }} + > + {props.active ? "Using" : "Used"} {selectedContextItems.length}{" "} + context item + {selectedContextItems.length === 1 ? "" : "s"} + </div> + ) + )} + {previewingContextItem && ( + <pre className="m-0"> + <StyledMarkdownPreview + fontSize={getFontSize()} + source={`\`\`\`${getMarkdownLanguageTagForFile( + previewingContextItem.description.description + )}\n${previewingContextItem.content}\n\`\`\``} + wrapperElement={{ + "data-color-mode": "dark", + }} + maxHeight={200} + /> + </pre> + )} <div className="flex px-2 relative" + style={{ + backgroundColor: vscBackground, + }} ref={divRef} - hidden={!downshiftProps.isOpen} > - <MainTextInput - inQueryForDynamicProvider={ - typeof inQueryForContextProvider !== "undefined" - } - fontSize={getFontSize()} - disabled={props.disabled} - placeholder={`Ask a question, '/' for slash commands, '@' to add context`} - {...getInputProps({ - onCompositionStart: () => setIsComposing(true), - onCompositionEnd: () => setIsComposing(false), - onChange: (e) => { - const target = e.target as HTMLTextAreaElement; - // Update the height of the textarea to match the content, up to a max of 200px. - target.style.height = "auto"; - target.style.height = `${Math.min( - target.scrollHeight, - 300 - ).toString()}px`; - - // setShowContextDropdown(target.value.endsWith("@")); - }, - onFocus: (e) => { - setFocused(true); - dispatch(setBottomMessage(undefined)); - }, - onKeyDown: (event) => { - dispatch(setBottomMessage(undefined)); - if (event.key === "Enter" && event.shiftKey) { - // Prevent Downshift's default 'Enter' behavior. - (event.nativeEvent as any).preventDownshiftDefault = true; - setCurrentlyInContextQuery(false); - } else if ( - event.key === "Enter" && - (!downshiftProps.isOpen || items.length === 0) && - !isComposing + <GradientBorder + loading={props.active || false} + isFirst={false} + isLast={false} + borderColor={props.active ? undefined : vscBackground} + borderRadius={defaultBorderRadius} + > + <MainTextInput + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={(e) => { + console.log("left"); + if ( + e.relatedTarget === deleteButtonDivRef.current || + deleteButtonDivRef.current?.contains(e.relatedTarget as Node) ) { - const value = downshiftProps.inputValue; - if (inQueryForContextProvider) { - const segs = value.split("@"); - client?.selectContextItem( - inQueryForContextProvider.title, - segs[segs.length - 1] - ); - setCurrentlyInContextQuery(false); - downshiftProps.setInputValue(""); + return; + } + setIsHovered(false); + }} + rows={props.isMainInput ? undefined : 1} + inQueryForDynamicProvider={ + typeof inQueryForContextProvider !== "undefined" + } + fontSize={getFontSize()} + disabled={props.disabled} + placeholder={`Ask a question, '/' for slash commands, '@' to add context`} + {...getInputProps({ + onCompositionStart: () => setIsComposing(true), + onCompositionEnd: () => setIsComposing(false), + onChange: (e) => { + const target = e.target as HTMLTextAreaElement; + // Update the height of the textarea to match the content, up to a max of 200px. + target.style.height = "auto"; + target.style.height = `${Math.min( + target.scrollHeight, + 300 + ).toString()}px`; + + // setShowContextDropdown(target.value.endsWith("@")); + }, + onFocus: (e) => { + setInputFocused(true); + dispatch(setBottomMessage(undefined)); + }, + onBlur: (e) => { + if (topRef.current?.contains(e.relatedTarget as Node)) { return; - } else { - if (value !== "") { - setPositionInHistory(history.length + 1); - setHistory([...history, value]); - } + } + setInputFocused(false); + }, + onKeyDown: (event) => { + dispatch(setBottomMessage(undefined)); + if (event.key === "Enter" && event.shiftKey) { // Prevent Downshift's default 'Enter' behavior. (event.nativeEvent as any).preventDownshiftDefault = true; - - if (props.onEnter) { - props.onEnter(event); + setCurrentlyInContextQuery(false); + } else if ( + event.key === "Enter" && + (!downshiftProps.isOpen || items.length === 0) && + !isComposing + ) { + const value = downshiftProps.inputValue; + if (inQueryForContextProvider) { + const segs = value.split("@"); + selectContextItem( + inQueryForContextProvider.title, + segs[segs.length - 1] + ); + setCurrentlyInContextQuery(false); + downshiftProps.setInputValue(""); + return; + } else { + if (value !== "") { + setPositionInHistory(history.length + 1); + setHistory([...history, value]); + } + // Prevent Downshift's default 'Enter' behavior. + (event.nativeEvent as any).preventDownshiftDefault = true; + + if (props.onEnter) { + props.onEnter(event, value); + } } - } - setCurrentlyInContextQuery(false); - } else if (event.key === "Enter" && currentlyInContextQuery) { - // Handle "Enter" for Context Providers - selectContextItemFromDropdown(event); - } else if ( - event.key === "Tab" && - downshiftProps.isOpen && - items.length > 0 && - items[downshiftProps.highlightedIndex]?.name.startsWith("/") - ) { - downshiftProps.setInputValue(items[0].name); - event.preventDefault(); - } else if (event.key === "Tab") { - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if ( - (event.key === "ArrowUp" || event.key === "ArrowDown") && - items.length > 0 - ) { - return; - } else if (event.key === "ArrowUp") { - // Only go back in history if selectionStart is 0 - // (i.e. the cursor is at the beginning of the input) - if ( - positionInHistory == 0 || - event.currentTarget.selectionStart !== 0 + setCurrentlyInContextQuery(false); + } else if (event.key === "Enter" && currentlyInContextQuery) { + // Handle "Enter" for Context Providers + selectContextItemFromDropdown(event); + } else if ( + event.key === "Tab" && + downshiftProps.isOpen && + items.length > 0 && + items[downshiftProps.highlightedIndex]?.name.startsWith("/") ) { + downshiftProps.setInputValue(items[0].name); + event.preventDefault(); + } else if (event.key === "Tab") { (event.nativeEvent as any).preventDownshiftDefault = true; - return; } else if ( - positionInHistory == history.length && - (history.length === 0 || - history[history.length - 1] !== event.currentTarget.value) + (event.key === "ArrowUp" || event.key === "ArrowDown") && + items.length > 0 ) { - setHistory([...history, event.currentTarget.value]); - } - downshiftProps.setInputValue(history[positionInHistory - 1]); - setPositionInHistory((prev) => prev - 1); - setCurrentlyInContextQuery(false); - } else if (event.key === "ArrowDown") { - if ( - positionInHistory === history.length || - event.currentTarget.selectionStart !== - event.currentTarget.value.length - ) { - (event.nativeEvent as any).preventDownshiftDefault = true; return; - } + } else if (event.key === "ArrowUp") { + // Only go back in history if selectionStart is 0 + // (i.e. the cursor is at the beginning of the input) + if ( + positionInHistory == 0 || + event.currentTarget.selectionStart !== 0 + ) { + (event.nativeEvent as any).preventDownshiftDefault = true; + return; + } else if ( + positionInHistory == history.length && + (history.length === 0 || + history[history.length - 1] !== event.currentTarget.value) + ) { + setHistory([...history, event.currentTarget.value]); + } + downshiftProps.setInputValue(history[positionInHistory - 1]); + setPositionInHistory((prev) => prev - 1); + setCurrentlyInContextQuery(false); + } else if (event.key === "ArrowDown") { + if ( + positionInHistory === history.length || + event.currentTarget.selectionStart !== + event.currentTarget.value.length + ) { + (event.nativeEvent as any).preventDownshiftDefault = true; + return; + } - if (positionInHistory < history.length) { - downshiftProps.setInputValue(history[positionInHistory + 1]); - } - setPositionInHistory((prev) => - Math.min(prev + 1, history.length) - ); - setCurrentlyInContextQuery(false); - } else if (event.key === "Escape") { - if (nestedContextProvider) { - goBackToContextProviders(); - (event.nativeEvent as any).preventDownshiftDefault = true; - return; - } else if (inQueryForContextProvider) { - goBackToContextProviders(); - (event.nativeEvent as any).preventDownshiftDefault = true; - return; - } + if (positionInHistory < history.length) { + downshiftProps.setInputValue( + history[positionInHistory + 1] + ); + } + setPositionInHistory((prev) => + Math.min(prev + 1, history.length) + ); + setCurrentlyInContextQuery(false); + } else if (event.key === "Escape") { + if (nestedContextProvider) { + goBackToContextProviders(); + (event.nativeEvent as any).preventDownshiftDefault = true; + return; + } else if (inQueryForContextProvider) { + goBackToContextProviders(); + (event.nativeEvent as any).preventDownshiftDefault = true; + return; + } - setCurrentlyInContextQuery(false); - if (downshiftProps.isOpen && items.length > 0) { - downshiftProps.closeMenu(); + setCurrentlyInContextQuery(false); + if (downshiftProps.isOpen && items.length > 0) { + downshiftProps.closeMenu(); + (event.nativeEvent as any).preventDownshiftDefault = true; + } else { + (event.nativeEvent as any).preventDownshiftDefault = true; + // Remove focus from the input + inputRef.current?.blur(); + // Move cursor back over to the editor + postVscMessage("focusEditor", {}); + } + } + // Home and end keys + else if (event.key === "Home") { (event.nativeEvent as any).preventDownshiftDefault = true; - } else { + } else if (event.key === "End") { (event.nativeEvent as any).preventDownshiftDefault = true; - // Remove focus from the input - inputRef.current?.blur(); - // Move cursor back over to the editor - postVscMessage("focusEditor", {}); } - } - // Home and end keys - else if (event.key === "Home") { - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if (event.key === "End") { - (event.nativeEvent as any).preventDownshiftDefault = true; - } - }, - onClick: () => { - dispatch(setBottomMessage(undefined)); - }, - ref: inputRef, - })} - /> + }, + onClick: () => { + dispatch(setBottomMessage(undefined)); + }, + ref: inputRef, + })} + /> + {props.isMainInput || ( + <DeleteButtonDiv ref={deleteButtonDivRef}> + {isHovered && ( + <div className="flex"> + <> + {timeline + .filter( + (h, i: number) => + props.groupIndices?.includes(i) && h.logs + ) + .some((h) => h.logs!.length > 0) && ( + <HeaderButtonWithText + onClick={(e) => { + e.stopPropagation(); + if (props.groupIndices) + client?.showLogsAtIndex(props.groupIndices[1]); + }} + text="Inspect Prompt" + > + <ArrowUpLeftIcon width="1.3em" height="1.3em" /> + </HeaderButtonWithText> + )} + <HeaderButtonWithText + onClick={(e) => { + e.stopPropagation(); + if (props.active && props.groupIndices) { + client?.deleteAtIndex(props.groupIndices[1]); + } else { + props.onDelete?.(); + } + }} + text={ + props.active ? `Stop (${getMetaKeyLabel()}⌫)` : "Delete" + } + > + {props.active ? ( + <StopCircleIcon width="1.4em" height="1.4em" /> + ) : ( + <XMarkIcon width="1.4em" height="1.4em" /> + )} + </HeaderButtonWithText> + </> + </div> + )} + </DeleteButtonDiv> + )} + </GradientBorder> {inQueryForContextProvider && ( <DynamicQueryTitleDiv> Enter {inQueryForContextProvider.display_title} Query @@ -807,6 +1130,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { {...downshiftProps.getMenuProps({ ref: ulRef, })} + isMainInput={props.isMainInput} showAbove={showAbove()} ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} hidden={ @@ -832,8 +1156,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { width="1.4em" height="1.4em" className="cursor-pointer" - onClick={() => { + onClick={(e) => { goBackToContextProviders(); + inputRef.current?.focus(); }} /> {nestedContextProvider.display_title} -{" "} @@ -888,18 +1213,23 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { </div> {selectedContextItems.length === 0 && (downshiftProps.inputValue?.startsWith("/edit") || - (focused && + (inputFocused && metaKeyPressed && downshiftProps.inputValue?.length > 0)) && ( - <div className="text-trueGray-400 pr-4 text-xs text-right"> + <div + className="text-trueGray-400 pr-4 text-xs text-right" + style={{ backgroundColor: vscBackground }} + > Inserting at cursor </div> )} - <ContinueButton - disabled={!(inputRef.current as any)?.value} - onClick={() => props.onEnter(undefined)} - /> - </> + {props.isMainInput && ( + <ContinueButton + disabled={!(inputRef.current as any)?.value} + onClick={() => props.onEnter?.(undefined)} + /> + )} + </div> ); }); diff --git a/extension/react-app/src/components/ErrorStepContainer.tsx b/extension/react-app/src/components/ErrorStepContainer.tsx index e8ab7950..666780c5 100644 --- a/extension/react-app/src/components/ErrorStepContainer.tsx +++ b/extension/react-app/src/components/ErrorStepContainer.tsx @@ -14,6 +14,7 @@ const Div = styled.div` background-color: #ff000011; border-radius: ${defaultBorderRadius}; border: 1px solid #cc0000; + margin: 8px; `; interface ErrorStepContainerProps { @@ -28,8 +29,8 @@ function ErrorStepContainer(props: ErrorStepContainerProps) { <div style={{ position: "absolute", - right: "4px", - top: "4px", + right: "12px", + top: "12px", display: "flex", }} > diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx index 84e6118c..431d0455 100644 --- a/extension/react-app/src/components/HeaderButtonWithText.tsx +++ b/extension/react-app/src/components/HeaderButtonWithText.tsx @@ -11,6 +11,7 @@ interface HeaderButtonWithTextProps { active?: boolean; className?: string; onKeyDown?: (e: any) => void; + tabIndex?: number; } const HeaderButtonWithText = React.forwardRef< @@ -39,6 +40,7 @@ const HeaderButtonWithText = React.forwardRef< onKeyDown={props.onKeyDown} className={props.className} ref={ref} + tabIndex={props.tabIndex} > {props.children} </HeaderButton> diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index fb685a82..063572b5 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -1,23 +1,23 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import styled from "styled-components"; import { StyledTooltip, defaultBorderRadius, lightGray, secondaryDark, - vscBackground, vscForeground, } from "."; import { TrashIcon, PaintBrushIcon, ExclamationTriangleIcon, + EyeIcon, } from "@heroicons/react/24/outline"; import { GUIClientContext } from "../App"; import { useDispatch } from "react-redux"; -import { setBottomMessage } from "../redux/slices/uiStateSlice"; import { ContextItem } from "../../../schema/FullState"; import { getFontSize } from "../util"; +import HeaderButtonWithText from "./HeaderButtonWithText"; const Button = styled.button<{ fontSize?: number }>` border: none; @@ -80,7 +80,13 @@ interface PillButtonProps { editingAny: boolean; index: number; areMultipleItems?: boolean; - onDelete?: () => void; + onDelete?: (index?: number) => void; + onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; + stepIndex?: number; + previewing?: boolean; + toggleViewContent?: () => void; + onBlur?: () => void; + focusing?: boolean; } interface StyledButtonProps { @@ -88,6 +94,14 @@ interface StyledButtonProps { editing?: boolean; } +const Container = styled.div<{ previewing?: boolean }>` + border-radius: ${defaultBorderRadius}; + background-color: ${secondaryDark}; + display: flex; + align-items: center; + justify-content: center; +`; + const StyledButton = styled(Button)<StyledButtonProps>` position: relative; border-color: ${(props) => props.borderColor || "transparent"}; @@ -96,12 +110,34 @@ const StyledButton = styled(Button)<StyledButtonProps>` &:focus { outline: none; - border-color: ${lightGray}; - border-width: 1px; - border-style: solid; + /* border-color: ${lightGray}; */ + text-decoration: underline; + } +`; + +const HoverableInsidePillButton = styled(HeaderButtonWithText)<{ + color: string; +}>` + &:hover { + background-color: ${(props) => props.color}; } `; +const ClickableInsidePillButton = styled(HeaderButtonWithText)<{ + color: string; + selected: boolean; +}>` + ${(props) => + props.selected && + ` + background-color: ${props.color}; + + &:hover { + background-color: ${props.color}; + } + `} +`; + const PillButton = (props: PillButtonProps) => { const [isHovered, setIsHovered] = useState(false); const client = useContext(GUIClientContext); @@ -116,122 +152,125 @@ const PillButton = (props: PillButtonProps) => { } }, [props.editing, props.item]); - const dispatch = useDispatch(); + const pillContainerRef = useRef<HTMLDivElement>(null); + const buttonRef = useRef<HTMLButtonElement>(null); return ( <div style={{ position: "relative" }}> - <StyledButton - fontSize={getFontSize()} - borderColor={props.editing ? (warning ? "red" : undefined) : undefined} - onMouseEnter={() => { - setIsHovered(true); - if (props.onHover) { - props.onHover(true); + <Container previewing={props.previewing} ref={pillContainerRef}> + <StyledButton + fontSize={getFontSize()} + borderColor={ + props.editing ? (warning ? "red" : undefined) : undefined } - }} - onMouseLeave={() => { - setIsHovered(false); - if (props.onHover) { - props.onHover(false); - } - }} - className="pill-button" - onKeyDown={(e) => { - if (e.key === "Backspace") { - props.onDelete?.(); - } - }} - > - {isHovered && ( - <GridDiv - style={{ - gridTemplateColumns: - props.item.editable && - props.areMultipleItems && - props.editingAny - ? "1fr 1fr" - : "1fr", - backgroundColor: vscBackground, - }} - > - {props.editingAny && - props.item.editable && - props.areMultipleItems && ( - <ButtonDiv - data-tooltip-id={`edit-${props.index}`} - backgroundColor={"#8800aa55"} - onClick={() => { - client?.setEditingAtIds([ - props.item.description.id.item_id, - ]); - }} - > - <PaintBrushIcon style={{ margin: "auto" }} width="1.6em" /> - </ButtonDiv> - )} - - <StyledTooltip id={`pin-${props.index}`}> - Edit this range - </StyledTooltip> - <ButtonDiv - data-tooltip-id={`delete-${props.index}`} - backgroundColor={"#cc000055"} + ref={buttonRef} + onMouseEnter={() => { + setIsHovered(true); + if (props.onHover) { + props.onHover(true); + } + }} + onMouseLeave={() => { + setIsHovered(false); + if (props.onHover) { + props.onHover(false); + } + }} + className={`pill-button-${props.stepIndex || "main"}`} + onKeyDown={(e) => { + if (e.key === "Backspace") { + props.onDelete?.(props.stepIndex); + } else if (e.key === "v") { + props.toggleViewContent?.(); + } else if (e.key === "e") { + client?.setEditingAtIds([props.item.description.id.item_id]); + } + }} + onClick={(e) => { + props.onClick?.(e); + }} + onBlur={(e) => { + if (!pillContainerRef.current?.contains(e.relatedTarget as any)) { + props.onBlur?.(); + } else { + e.preventDefault(); + buttonRef.current?.focus(); + } + }} + > + <span className={isHovered ? "underline" : ""}> + {props.item.description.name} + </span> + </StyledButton> + {((props.focusing && props.item.editable && props.editingAny) || + props.editing) && ( + <> + <ClickableInsidePillButton + data-tooltip-id={`circle-div-${props.item.description.name}`} + text={ + props.editing ? "Editing this range" : "Edit this range (e)" + } onClick={() => { - client?.deleteContextWithIds([props.item.description.id]); - dispatch(setBottomMessage(undefined)); + if (!props.editing) { + client?.setEditingAtIds([props.item.description.id.item_id]); + } }} + tabIndex={-1} + color="#f0f4" + selected={props.editing} > - <TrashIcon style={{ margin: "auto" }} width="1.6em" /> - </ButtonDiv> - </GridDiv> + <PaintBrushIcon width="1.4em" height="1.4em" /> + </ClickableInsidePillButton> + <StyledTooltip id={`circle-div-${props.item.description.name}`}> + Editing this range + </StyledTooltip> + </> + )} + {(props.focusing || props.previewing) && ( + <ClickableInsidePillButton + text="View (v)" + onClick={() => props.toggleViewContent?.()} + tabIndex={-1} + color="#ff04" + selected={props.previewing || false} + > + <EyeIcon width="1.4em" height="1.4em" /> + </ClickableInsidePillButton> + )} + {props.focusing && ( + <HoverableInsidePillButton + text="Delete (⌫)" + onClick={() => props.onDelete?.(props.stepIndex)} + tabIndex={-1} + color="#f004" + > + <TrashIcon width="1.4em" height="1.4em" /> + </HoverableInsidePillButton> )} - {props.item.description.name} - </StyledButton> + </Container> <StyledTooltip id={`edit-${props.index}`}> {props.item.editing ? "Editing this section (with entire file as context)" : "Edit this section"} </StyledTooltip> <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip> - {props.editing && - (warning ? ( - <> - <CircleDiv - data-tooltip-id={`circle-div-${props.item.description.name}`} - className="z-10" - > - <ExclamationTriangleIcon - style={{ margin: "auto" }} - width="1.0em" - strokeWidth={2} - /> - </CircleDiv> - <StyledTooltip id={`circle-div-${props.item.description.name}`}> - {warning} - </StyledTooltip> - </> - ) : ( - <> - <CircleDiv - data-tooltip-id={`circle-div-${props.item.description.name}`} - style={{ - backgroundColor: "#8800aa55", - border: `0.5px solid ${lightGray}`, - padding: "1px", - zIndex: 1, - }} - > - <PaintBrushIcon - style={{ margin: "auto" }} - width="1.0em" - strokeWidth={2} - /> - </CircleDiv> - <StyledTooltip id={`circle-div-${props.item.description.name}`}> - Editing this range - </StyledTooltip> - </> - ))} + {props.editing && warning && ( + <> + <CircleDiv + data-tooltip-id={`circle-div-${props.item.description.name}`} + className="z-10" + > + <ExclamationTriangleIcon + style={{ margin: "auto" }} + width="1.0em" + strokeWidth={2} + /> + </CircleDiv> + <StyledTooltip id={`circle-div-${props.item.description.name}`}> + {warning} + </StyledTooltip> + </> + )} </div> ); }; diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index e7264c5d..11e80fb2 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -35,10 +35,10 @@ const ButtonsDiv = styled.div` background-color: ${vscBackground}; box-shadow: 1px 1px 10px ${vscBackground}; border-radius: ${defaultBorderRadius}; - + z-index: 100; position: absolute; - right: 0; - top: 0; + right: 8px; + top: 16px; height: 0; `; diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx index 78d4234c..f53e5289 100644 --- a/extension/react-app/src/components/StyledMarkdownPreview.tsx +++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx @@ -12,12 +12,13 @@ import { getFontSize } from "../util"; const StyledMarkdownPreview = styled(MarkdownPreview)<{ light?: boolean; fontSize?: number; + maxHeight?: number; }>` pre { background-color: ${(props) => props.light ? vscBackground : secondaryDark}; border-radius: ${defaultBorderRadius}; - border: 0.5px solid ${lightGray}; + /* border: 0.5px solid ${lightGray}; */ max-width: calc(100vw - 24px); } @@ -34,6 +35,15 @@ const StyledMarkdownPreview = styled(MarkdownPreview)<{ props.light ? vscBackground : secondaryDark}; color: ${vscForeground}; padding: 12px; + + ${(props) => { + if (props.maxHeight) { + return ` + max-height: ${props.maxHeight}px; + overflow-y: auto; + `; + } + }} } background-color: ${(props) => (props.light ? "transparent" : vscBackground)}; diff --git a/extension/react-app/src/components/Suggestions.tsx b/extension/react-app/src/components/Suggestions.tsx index ed2eb558..bdda7579 100644 --- a/extension/react-app/src/components/Suggestions.tsx +++ b/extension/react-app/src/components/Suggestions.tsx @@ -150,6 +150,8 @@ const NUM_STAGES = suggestionsStages.length; const TutorialDiv = styled.div` margin: 4px; + margin-left: 8px; + margin-right: 8px; position: relative; background-color: #ff02; border-radius: ${defaultBorderRadius}; diff --git a/extension/react-app/src/components/TimelineItem.tsx b/extension/react-app/src/components/TimelineItem.tsx index f54788eb..b51dd307 100644 --- a/extension/react-app/src/components/TimelineItem.tsx +++ b/extension/react-app/src/components/TimelineItem.tsx @@ -11,7 +11,7 @@ const CollapseButton = styled.div` align-items: center; flex-shrink: 0; flex-grow: 0; - margin-left: 5px; + margin-left: 13px; cursor: pointer; `; diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx index 11671526..99b4bbc4 100644 --- a/extension/react-app/src/components/UserInputContainer.tsx +++ b/extension/react-app/src/components/UserInputContainer.tsx @@ -35,7 +35,6 @@ import { useSelector } from "react-redux"; interface UserInputContainerProps { onDelete: () => void; children: string; - historyNode: HistoryNode; index: number; onToggle: (arg0: boolean) => void; onToggleAll: (arg0: boolean) => void; diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index 1c27527c..9d9b7c40 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -39,7 +39,7 @@ export const StyledTooltip = styled(Tooltip)` padding: 6px; padding-left: 12px; padding-right: 12px; - z-index: 100; + z-index: 1000; max-width: 80vw; `; @@ -196,6 +196,11 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>` border-radius: ${defaultBorderRadius}; cursor: ${({ disabled }) => (disabled ? "default" : "pointer")}; + &:focus { + outline: none; + border: none; + } + &:hover { background-color: ${({ inverted }) => typeof inverted === "undefined" || inverted diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts index d71186d7..998d3a6d 100644 --- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -21,7 +21,7 @@ abstract class AbstractContinueGUIClientProtocol { abstract deleteAtIndex(index: number): void; - abstract deleteContextWithIds(ids: ContextItemId[]): void; + abstract deleteContextWithIds(ids: ContextItemId[], index?: number): void; abstract setEditingAtIds(ids: string[]): void; @@ -33,6 +33,12 @@ abstract class AbstractContinueGUIClientProtocol { abstract selectContextItem(id: string, query: string): void; + abstract selectContextItemAtIndex( + id: string, + query: string, + index: number + ): void; + abstract loadSession(session_id?: string): void; abstract onReconnectAtSession(session_id: string): void; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index 8205a629..863b1031 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -101,9 +101,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { this.messenger?.send("delete_at_index", { index }); } - deleteContextWithIds(ids: ContextItemId[]) { + deleteContextWithIds(ids: ContextItemId[], index?: number) { this.messenger?.send("delete_context_with_ids", { ids: ids.map((id) => `${id.provider_title}-${id.item_id}`), + index, }); } @@ -119,14 +120,22 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { this.messenger?.send("show_logs_at_index", { index }); } - showContextVirtualFile(): void { - this.messenger?.send("show_context_virtual_file", {}); + showContextVirtualFile(index?: number): void { + this.messenger?.send("show_context_virtual_file", { index }); } selectContextItem(id: string, query: string): void { this.messenger?.send("select_context_item", { id, query }); } + selectContextItemAtIndex(id: string, query: string, index: number): void { + this.messenger?.send("select_context_item_at_index", { + id, + query, + index, + }); + } + editStepAtIndex(userInput: string, index: number): void { this.messenger?.send("edit_step_at_index", { user_input: userInput, diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index a93ca9a0..12835121 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -17,7 +17,6 @@ import { usePostHog } from "posthog-js/react"; import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { postVscMessage } from "../vscode"; -import UserInputContainer from "../components/UserInputContainer"; import { isMetaEquivalentKeyPressed } from "../util"; import { setBottomMessage, @@ -30,6 +29,7 @@ import RingLoader from "../components/RingLoader"; import { setServerState, temporarilyClearSession, + temporarilyCreateNewUserInput, temporarilyPushToUserInputQueue, } from "../redux/slices/serverStateReducer"; import TimelineItem from "../components/TimelineItem"; @@ -76,11 +76,8 @@ const TitleTextInput = styled(TextInput)` const StepsDiv = styled.div` position: relative; background-color: transparent; - padding-left: 8px; - padding-right: 8px; & > * { - z-index: 1; position: relative; } @@ -331,7 +328,7 @@ function GUI(props: GUIProps) { } client.sendMainInput(input); - dispatch(temporarilyPushToUserInputQueue(input)); + dispatch(temporarilyCreateNewUserInput(input)); // Increment localstorage counter for popup const counter = localStorage.getItem("mainTextEntryCounter"); @@ -645,10 +642,19 @@ function GUI(props: GUIProps) { <> {node.step.name === "User Input" ? ( node.step.hide || ( - <UserInputContainer - active={getStepsInUserInputGroup(index).some((i) => { - return history.timeline[i].active; - })} + <ComboBox + isMainInput={false} + value={node.step.description as string} + active={ + getStepsInUserInputGroup(index).some((i) => { + return history.timeline[i].active; + }) || history.timeline[index].active + } + onEnter={(e, value) => { + if (value) client?.editStepAtIndex(value, index); + e?.stopPropagation(); + e?.preventDefault(); + }} groupIndices={getStepsInUserInputGroup(index)} onToggle={(isOpen: boolean) => { // Collapse all steps in the section @@ -678,10 +684,7 @@ function GUI(props: GUIProps) { client?.deleteAtIndex(i); }); }} - historyNode={node} - > - {node.step.description as string} - </UserInputContainer> + /> ) ) : ( <TimelineItem @@ -761,8 +764,9 @@ function GUI(props: GUIProps) { <div ref={aboveComboBoxDivRef} /> <ComboBox + isMainInput={true} ref={mainTextInputRef} - onEnter={(e) => { + onEnter={(e, _) => { onMainTextInput(e); e?.stopPropagation(); e?.preventDefault(); diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts index 9b3a780c..1f4836cb 100644 --- a/extension/react-app/src/redux/slices/serverStateReducer.ts +++ b/extension/react-app/src/redux/slices/serverStateReducer.ts @@ -98,6 +98,21 @@ export const serverStateSlice = createSlice({ temporarilyPushToUserInputQueue: (state, action) => { state.user_input_queue = [...state.user_input_queue, action.payload]; }, + temporarilyCreateNewUserInput: (state, action) => { + state.history.timeline = [ + ...state.history.timeline, + { + step: { + description: action.payload, + name: "User Input", + hide: false, + }, + depth: 0, + active: false, + context_used: state.selected_context_items, + }, + ]; + }, temporarilyClearSession: (state, action) => { state.history.timeline = []; state.selected_context_items = []; @@ -114,5 +129,6 @@ export const { setServerState, temporarilyPushToUserInputQueue, temporarilyClearSession, + temporarilyCreateNewUserInput, } = serverStateSlice.actions; export default serverStateSlice.reducer; diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts index fd74044d..5a95be41 100644 --- a/extension/react-app/src/util/index.ts +++ b/extension/react-app/src/util/index.ts @@ -46,3 +46,57 @@ export function getFontSize(): number { const fontSize = localStorage.getItem("fontSize"); return fontSize ? parseInt(fontSize) : 13; } + +export function getMarkdownLanguageTagForFile(filepath: string): string { + const ext = filepath.split(".").pop(); + switch (ext) { + case "py": + return "python"; + case "js": + return "javascript"; + case "ts": + return "typescript"; + case "java": + return "java"; + case "go": + return "go"; + case "rb": + return "ruby"; + case "rs": + return "rust"; + case "c": + return "c"; + case "cpp": + return "cpp"; + case "cs": + return "csharp"; + case "php": + return "php"; + case "scala": + return "scala"; + case "swift": + return "swift"; + case "kt": + return "kotlin"; + case "md": + return "markdown"; + case "json": + return "json"; + case "html": + return "html"; + case "css": + return "css"; + case "sh": + return "shell"; + case "yaml": + return "yaml"; + case "toml": + return "toml"; + case "tex": + return "latex"; + case "sql": + return "sql"; + default: + return ""; + } +} diff --git a/extension/schema/ContinueConfig.d.ts b/extension/schema/ContinueConfig.d.ts index 92f6e047..64aa5c02 100644 --- a/extension/schema/ContinueConfig.d.ts +++ b/extension/schema/ContinueConfig.d.ts @@ -72,10 +72,14 @@ export type VerifySsl = boolean; */ export type CaBundlePath = string; /** + * Proxy URL to use when making the HTTP request + */ +export type Proxy = string; +/** * The API key for the LLM provider. */ export type ApiKey = string; -export type Unused = LLM[]; +export type Saved = LLM[]; /** * The temperature parameter for sampling from the LLM. Higher temperatures will result in more random output, while lower temperatures will result in more predictable output. This value ranges from 0 to 1. */ @@ -205,12 +209,10 @@ export interface FunctionCall { */ export interface Models1 { default: LLM; - small?: LLM; - medium?: LLM; - large?: LLM; + summarize?: LLM; edit?: LLM; chat?: LLM; - unused?: Unused; + saved?: Saved; sdk?: ContinueSDK; [k: string]: unknown; } @@ -224,6 +226,7 @@ export interface LLM { timeout?: Timeout; verify_ssl?: VerifySsl; ca_bundle_path?: CaBundlePath; + proxy?: Proxy; prompt_templates?: PromptTemplates; api_key?: ApiKey; [k: string]: unknown; diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts index 5d5a5444..90b8506b 100644 --- a/extension/schema/FullState.d.ts +++ b/extension/schema/FullState.d.ts @@ -23,21 +23,22 @@ export type Depth = number; export type Deleted = boolean; export type Active = boolean; export type Logs = string[]; -export type Timeline = HistoryNode[]; -export type CurrentIndex = number; -export type Active1 = boolean; -export type UserInputQueue = string[]; export type Name3 = string; export type Description1 = string; -export type SlashCommands = SlashCommandDescription[]; -export type AddingHighlightedCode = boolean; -export type Name4 = string; -export type Description2 = string; export type ProviderTitle = string; export type ItemId = string; export type Content1 = string; export type Editing = boolean; export type Editable = boolean; +export type ContextUsed = ContextItem[]; +export type Timeline = HistoryNode[]; +export type CurrentIndex = number; +export type Active1 = boolean; +export type UserInputQueue = string[]; +export type Name4 = string; +export type Description2 = string; +export type SlashCommands = SlashCommandDescription[]; +export type AddingHighlightedCode = boolean; export type SelectedContextItems = ContextItem[]; export type SessionId = string; export type Title = string; @@ -51,6 +52,7 @@ export type Description3 = string; export type Dynamic = boolean; export type RequiresQuery = boolean; export type ContextProviders = ContextProviderDescription[]; +export type MeilisearchUrl = string; /** * A full state of the program, including the history @@ -66,6 +68,7 @@ export interface FullState1 { config: ContinueConfig; saved_context_groups?: SavedContextGroups; context_providers?: ContextProviders; + meilisearch_url?: MeilisearchUrl; [k: string]: unknown; } /** @@ -86,6 +89,7 @@ export interface HistoryNode { deleted?: Deleted; active?: Active; logs?: Logs; + context_used?: ContextUsed; [k: string]: unknown; } export interface Step { @@ -114,11 +118,6 @@ export interface FunctionCall { export interface Observation { [k: string]: unknown; } -export interface SlashCommandDescription { - name: Name3; - description: Description1; - [k: string]: unknown; -} /** * A ContextItem is a single item that is stored in the ContextManager. */ @@ -135,8 +134,8 @@ export interface ContextItem { * The id can be used to retrieve the ContextItem from the ContextManager. */ export interface ContextItemDescription { - name: Name4; - description: Description2; + name: Name3; + description: Description1; id: ContextItemId; [k: string]: unknown; } @@ -148,6 +147,11 @@ export interface ContextItemId { item_id: ItemId; [k: string]: unknown; } +export interface SlashCommandDescription { + name: Name4; + description: Description2; + [k: string]: unknown; +} export interface SessionInfo { session_id: SessionId; title: Title; diff --git a/extension/schema/History.d.ts b/extension/schema/History.d.ts index b00a1505..9b7db18a 100644 --- a/extension/schema/History.d.ts +++ b/extension/schema/History.d.ts @@ -23,6 +23,14 @@ export type Depth = number; export type Deleted = boolean; export type Active = boolean; export type Logs = string[]; +export type Name3 = string; +export type Description1 = string; +export type ProviderTitle = string; +export type ItemId = string; +export type Content1 = string; +export type Editing = boolean; +export type Editable = boolean; +export type ContextUsed = ContextItem[]; export type Timeline = HistoryNode[]; export type CurrentIndex = number; @@ -44,6 +52,7 @@ export interface HistoryNode { deleted?: Deleted; active?: Active; logs?: Logs; + context_used?: ContextUsed; [k: string]: unknown; } export interface Step { @@ -72,3 +81,32 @@ export interface FunctionCall { export interface Observation { [k: string]: unknown; } +/** + * A ContextItem is a single item that is stored in the ContextManager. + */ +export interface ContextItem { + description: ContextItemDescription; + content: Content1; + editing?: Editing; + editable?: Editable; + [k: string]: unknown; +} +/** + * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'. + * + * The id can be used to retrieve the ContextItem from the ContextManager. + */ +export interface ContextItemDescription { + name: Name3; + description: Description1; + id: ContextItemId; + [k: string]: unknown; +} +/** + * A ContextItemId is a unique identifier for a ContextItem. + */ +export interface ContextItemId { + provider_title: ProviderTitle; + item_id: ItemId; + [k: string]: unknown; +} diff --git a/extension/schema/HistoryNode.d.ts b/extension/schema/HistoryNode.d.ts index 08424d75..ad4c1154 100644 --- a/extension/schema/HistoryNode.d.ts +++ b/extension/schema/HistoryNode.d.ts @@ -23,6 +23,14 @@ export type Depth = number; export type Deleted = boolean; export type Active = boolean; export type Logs = string[]; +export type Name3 = string; +export type Description1 = string; +export type ProviderTitle = string; +export type ItemId = string; +export type Content1 = string; +export type Editing = boolean; +export type Editable = boolean; +export type ContextUsed = ContextItem[]; /** * A point in history, a list of which make up History @@ -34,6 +42,7 @@ export interface HistoryNode1 { deleted?: Deleted; active?: Active; logs?: Logs; + context_used?: ContextUsed; [k: string]: unknown; } export interface Step { @@ -62,3 +71,32 @@ export interface FunctionCall { export interface Observation { [k: string]: unknown; } +/** + * A ContextItem is a single item that is stored in the ContextManager. + */ +export interface ContextItem { + description: ContextItemDescription; + content: Content1; + editing?: Editing; + editable?: Editable; + [k: string]: unknown; +} +/** + * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'. + * + * The id can be used to retrieve the ContextItem from the ContextManager. + */ +export interface ContextItemDescription { + name: Name3; + description: Description1; + id: ContextItemId; + [k: string]: unknown; +} +/** + * A ContextItemId is a unique identifier for a ContextItem. + */ +export interface ContextItemId { + provider_title: ProviderTitle; + item_id: ItemId; + [k: string]: unknown; +} diff --git a/extension/schema/LLM.d.ts b/extension/schema/LLM.d.ts index 31d38456..2c1ced29 100644 --- a/extension/schema/LLM.d.ts +++ b/extension/schema/LLM.d.ts @@ -43,6 +43,10 @@ export type VerifySsl = boolean; */ export type CaBundlePath = string; /** + * Proxy URL to use when making the HTTP request + */ +export type Proxy = string; +/** * The API key for the LLM provider. */ export type ApiKey = string; @@ -57,6 +61,7 @@ export interface LLM1 { timeout?: Timeout; verify_ssl?: VerifySsl; ca_bundle_path?: CaBundlePath; + proxy?: Proxy; prompt_templates?: PromptTemplates; api_key?: ApiKey; [k: string]: unknown; diff --git a/extension/schema/Models.d.ts b/extension/schema/Models.d.ts index 9005c08c..67d73cfc 100644 --- a/extension/schema/Models.d.ts +++ b/extension/schema/Models.d.ts @@ -43,22 +43,24 @@ export type VerifySsl = boolean; */ export type CaBundlePath = string; /** + * Proxy URL to use when making the HTTP request + */ +export type Proxy = string; +/** * The API key for the LLM provider. */ export type ApiKey = string; -export type Unused = LLM[]; +export type Saved = LLM[]; /** * Main class that holds the current model configuration */ export interface Models1 { default: LLM; - small?: LLM; - medium?: LLM; - large?: LLM; + summarize?: LLM; edit?: LLM; chat?: LLM; - unused?: Unused; + saved?: Saved; sdk?: ContinueSDK; [k: string]: unknown; } @@ -72,6 +74,7 @@ export interface LLM { timeout?: Timeout; verify_ssl?: VerifySsl; ca_bundle_path?: CaBundlePath; + proxy?: Proxy; prompt_templates?: PromptTemplates; api_key?: ApiKey; [k: string]: unknown; diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index e2c86bdf..006ac156 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -70,10 +70,11 @@ class IdeProtocolClient { }); messenger.onMessage((messageType, data, messenger) => { this.handleMessage(messageType, data, messenger).catch((err) => { + console.log("Error handling message: ", err); vscode.window .showErrorMessage( `Error handling message (${messageType}) from Continue server: ` + - err.message, + err, "View Logs" ) .then((selection) => { diff --git a/schema/json/ContinueConfig.json b/schema/json/ContinueConfig.json index 8666c420..e78bb3c9 100644 --- a/schema/json/ContinueConfig.json +++ b/schema/json/ContinueConfig.json @@ -15,7 +15,10 @@ "type": "string" } }, - "required": ["name", "arguments"] + "required": [ + "name", + "arguments" + ] }, "ChatMessage": { "title": "ChatMessage", @@ -23,7 +26,12 @@ "properties": { "role": { "title": "Role", - "enum": ["assistant", "user", "system", "function"], + "enum": [ + "assistant", + "user", + "system", + "function" + ], "type": "string" }, "content": { @@ -42,7 +50,10 @@ "$ref": "#/definitions/FunctionCall" } }, - "required": ["role", "summary"] + "required": [ + "role", + "summary" + ] }, "Step": { "title": "Step", @@ -139,6 +150,11 @@ "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string" }, + "proxy": { + "title": "Proxy", + "description": "Proxy URL to use when making the HTTP request", + "type": "string" + }, "prompt_templates": { "title": "Prompt Templates", "description": "A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the \"edit\" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.", @@ -151,7 +167,9 @@ "type": "string" } }, - "required": ["model"] + "required": [ + "model" + ] }, "src__continuedev__core__models__ContinueSDK": { "title": "ContinueSDK", @@ -166,13 +184,7 @@ "default": { "$ref": "#/definitions/LLM" }, - "small": { - "$ref": "#/definitions/LLM" - }, - "medium": { - "$ref": "#/definitions/LLM" - }, - "large": { + "summarize": { "$ref": "#/definitions/LLM" }, "edit": { @@ -181,8 +193,8 @@ "chat": { "$ref": "#/definitions/LLM" }, - "unused": { - "title": "Unused", + "saved": { + "title": "Saved", "default": [], "type": "array", "items": { @@ -193,7 +205,9 @@ "$ref": "#/definitions/src__continuedev__core__models__ContinueSDK" } }, - "required": ["default"] + "required": [ + "default" + ] }, "CustomCommand": { "title": "CustomCommand", @@ -212,7 +226,11 @@ "type": "string" } }, - "required": ["name", "prompt", "description"] + "required": [ + "name", + "prompt", + "description" + ] }, "SlashCommand": { "title": "SlashCommand", @@ -235,7 +253,11 @@ "type": "object" } }, - "required": ["name", "description", "step"] + "required": [ + "name", + "description", + "step" + ] }, "Policy": { "title": "Policy", @@ -263,7 +285,10 @@ "type": "string" } }, - "required": ["provider_title", "item_id"] + "required": [ + "provider_title", + "item_id" + ] }, "ContextItemDescription": { "title": "ContextItemDescription", @@ -282,7 +307,11 @@ "$ref": "#/definitions/ContextItemId" } }, - "required": ["name", "description", "id"] + "required": [ + "name", + "description", + "id" + ] }, "ContextItem": { "title": "ContextItem", @@ -307,7 +336,10 @@ "type": "boolean" } }, - "required": ["description", "content"] + "required": [ + "description", + "content" + ] }, "ContextProvider": { "title": "ContextProvider", @@ -359,7 +391,12 @@ } } }, - "required": ["title", "display_title", "description", "dynamic"] + "required": [ + "title", + "display_title", + "description", + "dynamic" + ] }, "src__continuedev__core__config__ContinueConfig": { "title": "ContinueConfig", @@ -403,13 +440,13 @@ "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, + "proxy": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "OpenAIFreeTrial" }, - "small": null, - "medium": { + "summarize": { "title": null, "system_message": null, "context_length": 2048, @@ -418,15 +455,15 @@ "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, + "proxy": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "OpenAIFreeTrial" }, - "large": null, "edit": null, "chat": null, - "unused": [] + "saved": [] }, "allOf": [ { @@ -516,4 +553,4 @@ } } } -} +}
\ No newline at end of file diff --git a/schema/json/FullState.json b/schema/json/FullState.json index ae52cf5d..aebe4b21 100644 --- a/schema/json/FullState.json +++ b/schema/json/FullState.json @@ -101,6 +101,76 @@ "type": "object", "properties": {} }, + "ContextItemId": { + "title": "ContextItemId", + "description": "A ContextItemId is a unique identifier for a ContextItem.", + "type": "object", + "properties": { + "provider_title": { + "title": "Provider Title", + "type": "string" + }, + "item_id": { + "title": "Item Id", + "type": "string" + } + }, + "required": [ + "provider_title", + "item_id" + ] + }, + "ContextItemDescription": { + "title": "ContextItemDescription", + "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "id": { + "$ref": "#/definitions/ContextItemId" + } + }, + "required": [ + "name", + "description", + "id" + ] + }, + "ContextItem": { + "title": "ContextItem", + "description": "A ContextItem is a single item that is stored in the ContextManager.", + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/ContextItemDescription" + }, + "content": { + "title": "Content", + "type": "string" + }, + "editing": { + "title": "Editing", + "default": false, + "type": "boolean" + }, + "editable": { + "title": "Editable", + "default": false, + "type": "boolean" + } + }, + "required": [ + "description", + "content" + ] + }, "HistoryNode": { "title": "HistoryNode", "description": "A point in history, a list of which make up History", @@ -133,6 +203,14 @@ "items": { "type": "string" } + }, + "context_used": { + "title": "Context Used", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ContextItem" + } } }, "required": [ @@ -180,76 +258,6 @@ "description" ] }, - "ContextItemId": { - "title": "ContextItemId", - "description": "A ContextItemId is a unique identifier for a ContextItem.", - "type": "object", - "properties": { - "provider_title": { - "title": "Provider Title", - "type": "string" - }, - "item_id": { - "title": "Item Id", - "type": "string" - } - }, - "required": [ - "provider_title", - "item_id" - ] - }, - "ContextItemDescription": { - "title": "ContextItemDescription", - "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.", - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string" - }, - "description": { - "title": "Description", - "type": "string" - }, - "id": { - "$ref": "#/definitions/ContextItemId" - } - }, - "required": [ - "name", - "description", - "id" - ] - }, - "ContextItem": { - "title": "ContextItem", - "description": "A ContextItem is a single item that is stored in the ContextManager.", - "type": "object", - "properties": { - "description": { - "$ref": "#/definitions/ContextItemDescription" - }, - "content": { - "title": "Content", - "type": "string" - }, - "editing": { - "title": "Editing", - "default": false, - "type": "boolean" - }, - "editable": { - "title": "Editable", - "default": false, - "type": "boolean" - } - }, - "required": [ - "description", - "content" - ] - }, "SessionInfo": { "title": "SessionInfo", "type": "object", @@ -385,6 +393,10 @@ "items": { "$ref": "#/definitions/ContextProviderDescription" } + }, + "meilisearch_url": { + "title": "Meilisearch Url", + "type": "string" } }, "required": [ diff --git a/schema/json/History.json b/schema/json/History.json index c8b8208b..9575b8c3 100644 --- a/schema/json/History.json +++ b/schema/json/History.json @@ -101,6 +101,76 @@ "type": "object", "properties": {} }, + "ContextItemId": { + "title": "ContextItemId", + "description": "A ContextItemId is a unique identifier for a ContextItem.", + "type": "object", + "properties": { + "provider_title": { + "title": "Provider Title", + "type": "string" + }, + "item_id": { + "title": "Item Id", + "type": "string" + } + }, + "required": [ + "provider_title", + "item_id" + ] + }, + "ContextItemDescription": { + "title": "ContextItemDescription", + "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "id": { + "$ref": "#/definitions/ContextItemId" + } + }, + "required": [ + "name", + "description", + "id" + ] + }, + "ContextItem": { + "title": "ContextItem", + "description": "A ContextItem is a single item that is stored in the ContextManager.", + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/ContextItemDescription" + }, + "content": { + "title": "Content", + "type": "string" + }, + "editing": { + "title": "Editing", + "default": false, + "type": "boolean" + }, + "editable": { + "title": "Editable", + "default": false, + "type": "boolean" + } + }, + "required": [ + "description", + "content" + ] + }, "HistoryNode": { "title": "HistoryNode", "description": "A point in history, a list of which make up History", @@ -133,6 +203,14 @@ "items": { "type": "string" } + }, + "context_used": { + "title": "Context Used", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ContextItem" + } } }, "required": [ diff --git a/schema/json/HistoryNode.json b/schema/json/HistoryNode.json index 3ca9e394..f9004a43 100644 --- a/schema/json/HistoryNode.json +++ b/schema/json/HistoryNode.json @@ -101,6 +101,76 @@ "type": "object", "properties": {} }, + "ContextItemId": { + "title": "ContextItemId", + "description": "A ContextItemId is a unique identifier for a ContextItem.", + "type": "object", + "properties": { + "provider_title": { + "title": "Provider Title", + "type": "string" + }, + "item_id": { + "title": "Item Id", + "type": "string" + } + }, + "required": [ + "provider_title", + "item_id" + ] + }, + "ContextItemDescription": { + "title": "ContextItemDescription", + "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "id": { + "$ref": "#/definitions/ContextItemId" + } + }, + "required": [ + "name", + "description", + "id" + ] + }, + "ContextItem": { + "title": "ContextItem", + "description": "A ContextItem is a single item that is stored in the ContextManager.", + "type": "object", + "properties": { + "description": { + "$ref": "#/definitions/ContextItemDescription" + }, + "content": { + "title": "Content", + "type": "string" + }, + "editing": { + "title": "Editing", + "default": false, + "type": "boolean" + }, + "editable": { + "title": "Editable", + "default": false, + "type": "boolean" + } + }, + "required": [ + "description", + "content" + ] + }, "src__continuedev__core__main__HistoryNode": { "title": "HistoryNode", "description": "A point in history, a list of which make up History", @@ -133,6 +203,14 @@ "items": { "type": "string" } + }, + "context_used": { + "title": "Context Used", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ContextItem" + } } }, "required": [ diff --git a/schema/json/LLM.json b/schema/json/LLM.json index acfc4dc2..b5b48d6b 100644 --- a/schema/json/LLM.json +++ b/schema/json/LLM.json @@ -56,6 +56,11 @@ "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string" }, + "proxy": { + "title": "Proxy", + "description": "Proxy URL to use when making the HTTP request", + "type": "string" + }, "prompt_templates": { "title": "Prompt Templates", "description": "A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the \"edit\" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.", diff --git a/schema/json/Models.json b/schema/json/Models.json index de2f32c5..9a7bd310 100644 --- a/schema/json/Models.json +++ b/schema/json/Models.json @@ -56,6 +56,11 @@ "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string" }, + "proxy": { + "title": "Proxy", + "description": "Proxy URL to use when making the HTTP request", + "type": "string" + }, "prompt_templates": { "title": "Prompt Templates", "description": "A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the \"edit\" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.", @@ -85,13 +90,7 @@ "default": { "$ref": "#/definitions/LLM" }, - "small": { - "$ref": "#/definitions/LLM" - }, - "medium": { - "$ref": "#/definitions/LLM" - }, - "large": { + "summarize": { "$ref": "#/definitions/LLM" }, "edit": { @@ -100,8 +99,8 @@ "chat": { "$ref": "#/definitions/LLM" }, - "unused": { - "title": "Unused", + "saved": { + "title": "Saved", "default": [], "type": "array", "items": { |