diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-09-23 13:06:00 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-23 13:06:00 -0700 |
commit | e976d60974a7837967d03807605cbf2e7b4f3f9a (patch) | |
tree | 5ecb19062abb162832530dd953e9d2801026c23c | |
parent | 470711d25b44d1a545c57bc17d40d5e1fd402216 (diff) | |
download | sncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.tar.gz sncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.tar.bz2 sncontinue-e976d60974a7837967d03807605cbf2e7b4f3f9a.zip |
UI Redesign and fixing many details (#496)
* feat: :lipstick: start of major design upgrade
* feat: :lipstick: model selection page
* feat: :lipstick: use shortcut to add highlighted code as ctx
* feat: :lipstick: better display of errors
* feat: :lipstick: ui for learning keyboard shortcuts, more details
* refactor: :construction: testing slash commands ui
* Truncate continue.log
* refactor: :construction: refactoring client_session, ui, more
* feat: :bug: layout fixes
* refactor: :lipstick: ui to enter OpenAI Key
* refactor: :truck: rename MaybeProxyOpenAI -> OpenAIFreeTrial
* starting help center
* removing old shortcut docs
* fix: :bug: fix model setting logic to avoid overwrites
* feat: :lipstick: tutorial and model descriptions
* refactor: :truck: rename unused -> saved
* refactor: :truck: rename model roles
* feat: :lipstick: edit indicator
* refactor: :lipstick: move +, folder icons
* feat: :lipstick: tab to clear all context
* fix: :bug: context providers ui fixes
* fix: :bug: fix lag when stopping step
* fix: :bug: don't override system message for models
* fix: :bug: fix continue button cursor
* feat: :lipstick: title bar
* fix: :bug: updates to code highlighting logic and more
* fix: :bug: fix renaming of summarize model role
* feat: :lipstick: help page and better session title
* feat: :lipstick: more help page / ui improvements
* feat: :lipstick: set session title
* fix: :bug: small fixes for changing sessions
* fix: :bug: perfecting the highlighting code and ctx interactions
* style: :lipstick: sticky headers for scroll, ollama warming
* fix: :bug: fix toggle bug
---------
Co-authored-by: Ty Dunn <ty@tydunn.com>
127 files changed, 3913 insertions, 1538 deletions
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index a27b0cb7..5804ce6b 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -10,6 +10,7 @@ from aiohttp import ClientPayloadError from openai import error as openai_errors from pydantic import root_validator +from ..libs.llm.prompts.chat import template_alpaca_messages from ..libs.util.create_async_task import create_async_task from ..libs.util.devdata import dev_data_logger from ..libs.util.edit_config import edit_config_property @@ -201,7 +202,9 @@ class Autopilot(ContinueBaseModel): ) or [] ) - return custom_commands + slash_commands + cmds = custom_commands + slash_commands + cmds.sort(key=lambda x: x["name"] == "edit", reverse=True) + return cmds async def clear_history(self): # Reset history @@ -273,14 +276,16 @@ class Autopilot(ContinueBaseModel): await self._run_singular_step(step) async def handle_highlighted_code( - self, range_in_files: List[RangeInFileWithContents] + self, + range_in_files: List[RangeInFileWithContents], + edit: Optional[bool] = False, ): if "code" not in self.context_manager.context_providers: return # Add to context manager await self.context_manager.context_providers["code"].handle_highlighted_code( - range_in_files + range_in_files, edit ) await self.update_subscribers() @@ -292,7 +297,9 @@ class Autopilot(ContinueBaseModel): self._retry_queue.post(str(index), None) async def delete_at_index(self, index: int): - self.history.timeline[index].step.hide = True + if not self.history.timeline[index].active: + self.history.timeline[index].step.hide = True + self.history.timeline[index].deleted = True self.history.timeline[index].active = False @@ -476,9 +483,43 @@ class Autopilot(ContinueBaseModel): create_async_task( update_description(), - on_error=lambda e: self.continue_sdk.run_step(DisplayErrorStep(e=e)), + on_error=lambda e: self.continue_sdk.run_step( + DisplayErrorStep.from_exception(e) + ), ) + # Create the session title if not done yet + if self.session_info is None or self.session_info.title is None: + visible_nodes = list( + filter(lambda node: not node.step.hide, self.history.timeline) + ) + + user_input = None + should_create_title = False + for visible_node in visible_nodes: + if isinstance(visible_node.step, UserInputStep): + if user_input is None: + user_input = visible_node.step.user_input + else: + # More than one user input, so don't create title + should_create_title = False + break + elif user_input is None: + continue + else: + # Already have user input, now have the next step + should_create_title = True + break + + # Only create the title if the step after the first input is done + if should_create_title: + create_async_task( + self.create_title(backup=user_input), + on_error=lambda e: self.continue_sdk.run_step( + DisplayErrorStep.from_exception(e) + ), + ) + return observation async def run_from_step(self, step: "Step"): @@ -523,41 +564,43 @@ class Autopilot(ContinueBaseModel): self._should_halt = False return None - async def accept_user_input(self, user_input: str): - self._main_user_input_queue.append(user_input) - await self.update_subscribers() + def set_current_session_title(self, title: str): + self.session_info = SessionInfo( + title=title, + session_id=self.ide.session_id, + date_created=str(time.time()), + workspace_directory=self.ide.workspace_directory, + ) - # Use the first input to create title for session info, and make the session saveable - if self.session_info is None: + async def create_title(self, backup: str = None): + # Use the first input and first response to create title for session info, and make the session saveable + if self.session_info is not None and self.session_info.title is not None: + return - async def create_title(): - if ( - self.session_info is not None - and self.session_info.title is not None - ): - return + if self.continue_sdk.config.disable_summaries: + if backup is not None: + title = backup + else: + title = "New Session" + else: + chat_history = list( + map(lambda x: x.dict(), await self.continue_sdk.get_chat_context()) + ) + chat_history_str = template_alpaca_messages(chat_history) + title = await self.continue_sdk.models.summarize.complete( + f"{chat_history_str}\n\nGive a short title to describe the above chat session. Do not put quotes around the title. Do not use more than 6 words. The title is: ", + max_tokens=20, + log=False, + ) + title = remove_quotes_and_escapes(title) - if self.continue_sdk.config.disable_summaries: - title = user_input - else: - title = await self.continue_sdk.models.medium.complete( - f'Give a short title to describe the current chat session. Do not put quotes around the title. The first message was: "{user_input}". Do not use more than 10 words. The title is: ', - max_tokens=20, - ) - title = remove_quotes_and_escapes(title) - - self.session_info = SessionInfo( - title=title, - session_id=self.ide.session_id, - date_created=str(time.time()), - workspace_directory=self.ide.workspace_directory, - ) - dev_data_logger.capture("new_session", self.session_info.dict()) + self.set_current_session_title(title) + await self.update_subscribers() + dev_data_logger.capture("new_session", self.session_info.dict()) - create_async_task( - create_title(), - on_error=lambda e: self.continue_sdk.run_step(DisplayErrorStep(e=e)), - ) + async def accept_user_input(self, user_input: str): + self._main_user_input_queue.append(user_input) + await self.update_subscribers() if len(self._main_user_input_queue) > 1: return @@ -579,6 +622,15 @@ class Autopilot(ContinueBaseModel): await self.reverse_to_index(index) await self.run_from_step(UserInputStep(user_input=user_input)) + async def reject_diff(self, step_index: int): + # Hide the edit step and the UserInputStep before it + self.history.timeline[step_index].step.hide = True + for i in range(step_index - 1, -1, -1): + if isinstance(self.history.timeline[i].step, UserInputStep): + self.history.timeline[i].step.hide = True + break + await self.update_subscribers() + async def select_context_item(self, id: str, query: str): await self.context_manager.select_context_item(id, query) await self.update_subscribers() diff --git a/continuedev/src/continuedev/core/config.py b/continuedev/src/continuedev/core/config.py index d431c704..2bbb42cc 100644 --- a/continuedev/src/continuedev/core/config.py +++ b/continuedev/src/continuedev/core/config.py @@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Type from pydantic import BaseModel, Field, validator -from ..libs.llm.maybe_proxy_openai import MaybeProxyOpenAI +from ..libs.llm.openai_free_trial import OpenAIFreeTrial from .context import ContextProvider from .main import Policy, Step from .models import Models @@ -48,8 +48,8 @@ class ContinueConfig(BaseModel): ) models: Models = Field( Models( - default=MaybeProxyOpenAI(model="gpt-4"), - medium=MaybeProxyOpenAI(model="gpt-3.5-turbo"), + default=OpenAIFreeTrial(model="gpt-4"), + summarize=OpenAIFreeTrial(model="gpt-3.5-turbo"), ), description="Configuration for the models used by Continue. Read more about how to configure models in the documentation.", ) diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py index 63a3e6a9..cf41aab9 100644 --- a/continuedev/src/continuedev/core/main.py +++ b/continuedev/src/continuedev/core/main.py @@ -337,6 +337,12 @@ class Step(ContinueBaseModel): hide: bool = False description: Union[str, None] = None + class_name: str = "Step" + + @validator("class_name", pre=True, always=True) + def class_name_is_class_name(cls, class_name): + return cls.__name__ + system_message: Union[str, None] = None chat_context: List[ChatMessage] = [] manage_own_chat_context: bool = False diff --git a/continuedev/src/continuedev/core/models.py b/continuedev/src/continuedev/core/models.py index f24c81ca..2396a0db 100644 --- a/continuedev/src/continuedev/core/models.py +++ b/continuedev/src/continuedev/core/models.py @@ -5,13 +5,14 @@ from pydantic import BaseModel from ..libs.llm import LLM from ..libs.llm.anthropic import AnthropicLLM from ..libs.llm.ggml import GGML +from ..libs.llm.hf_inference_api import HuggingFaceInferenceAPI +from ..libs.llm.hf_tgi import HuggingFaceTGI from ..libs.llm.llamacpp import LlamaCpp -from ..libs.llm.maybe_proxy_openai import MaybeProxyOpenAI from ..libs.llm.ollama import Ollama from ..libs.llm.openai import OpenAI +from ..libs.llm.openai_free_trial import OpenAIFreeTrial from ..libs.llm.replicate import ReplicateLLM from ..libs.llm.together import TogetherLLM -from ..libs.llm.hf_inference_api import HuggingFaceInferenceAPI class ContinueSDK(BaseModel): @@ -20,9 +21,7 @@ class ContinueSDK(BaseModel): ALL_MODEL_ROLES = [ "default", - "small", - "medium", - "large", + "summarize", "edit", "chat", ] @@ -31,7 +30,7 @@ MODEL_CLASSES = { cls.__name__: cls for cls in [ OpenAI, - MaybeProxyOpenAI, + OpenAIFreeTrial, GGML, TogetherLLM, AnthropicLLM, @@ -39,12 +38,13 @@ MODEL_CLASSES = { Ollama, LlamaCpp, HuggingFaceInferenceAPI, + HuggingFaceTGI, ] } MODEL_MODULE_NAMES = { "OpenAI": "openai", - "MaybeProxyOpenAI": "maybe_proxy_openai", + "OpenAIFreeTrial": "openai_free_trial", "GGML": "ggml", "TogetherLLM": "together", "AnthropicLLM": "anthropic", @@ -52,6 +52,7 @@ MODEL_MODULE_NAMES = { "Ollama": "ollama", "LlamaCpp": "llamacpp", "HuggingFaceInferenceAPI": "hf_inference_api", + "HuggingFaceTGI": "hf_tgi", } @@ -59,13 +60,11 @@ class Models(BaseModel): """Main class that holds the current model configuration""" default: LLM - small: Optional[LLM] = None - medium: Optional[LLM] = None - large: Optional[LLM] = None + summarize: Optional[LLM] = None edit: Optional[LLM] = None chat: Optional[LLM] = None - unused: List[LLM] = [] + saved: List[LLM] = [] # TODO namespace these away to not confuse readers, # or split Models into ModelsConfig, which gets turned into Models @@ -89,7 +88,8 @@ class Models(BaseModel): def set_system_message(self, msg: str): for model in self.all_models: - model.system_message = msg + if model.system_message is None: + model.system_message = msg async def start(self, sdk: "ContinueSDK"): """Start each of the LLMs, or fall back to default""" diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py index 12fce1c6..64fd784c 100644 --- a/continuedev/src/continuedev/core/sdk.py +++ b/continuedev/src/continuedev/core/sdk.py @@ -104,7 +104,7 @@ class ContinueSDK(AbstractContinueSDK): ) await sdk.lsp.start() except Exception as e: - logger.warning(f"Failed to start LSP client: {e}", exc_info=True) + logger.warning(f"Failed to start LSP client: {e}", exc_info=False) sdk.lsp = None create_async_task( diff --git a/continuedev/src/continuedev/libs/constants/default_config.py b/continuedev/src/continuedev/libs/constants/default_config.py index d93dffcd..a1b2de2c 100644 --- a/continuedev/src/continuedev/libs/constants/default_config.py +++ b/continuedev/src/continuedev/libs/constants/default_config.py @@ -8,16 +8,14 @@ See https://continue.dev/docs/customization to for documentation of the availabl from continuedev.src.continuedev.core.models import Models from continuedev.src.continuedev.core.config import CustomCommand, SlashCommand, ContinueConfig from continuedev.src.continuedev.plugins.context_providers.github import GitHubIssuesContextProvider -from continuedev.src.continuedev.libs.llm.maybe_proxy_openai import MaybeProxyOpenAI +from continuedev.src.continuedev.libs.llm.openai_free_trial import OpenAIFreeTrial from continuedev.src.continuedev.plugins.steps.open_config import OpenConfigStep from continuedev.src.continuedev.plugins.steps.clear_history import ClearHistoryStep -from continuedev.src.continuedev.plugins.steps.feedback import FeedbackStep from continuedev.src.continuedev.plugins.steps.comment_code import CommentCodeStep from continuedev.src.continuedev.plugins.steps.share_session import ShareSessionStep from continuedev.src.continuedev.plugins.steps.main import EditHighlightedCodeStep from continuedev.src.continuedev.plugins.steps.cmd import GenerateShellCommandStep -from continuedev.src.continuedev.plugins.context_providers.search import SearchContextProvider from continuedev.src.continuedev.plugins.context_providers.diff import DiffContextProvider from continuedev.src.continuedev.plugins.context_providers.url import URLContextProvider from continuedev.src.continuedev.plugins.context_providers.terminal import TerminalContextProvider @@ -25,8 +23,8 @@ from continuedev.src.continuedev.plugins.context_providers.terminal import Termi config = ContinueConfig( allow_anonymous_telemetry=True, models=Models( - default=MaybeProxyOpenAI(api_key="", model="gpt-4"), - medium=MaybeProxyOpenAI(api_key="", model="gpt-3.5-turbo") + default=OpenAIFreeTrial(api_key="", model="gpt-4"), + summarize=OpenAIFreeTrial(api_key="", model="gpt-3.5-turbo") ), system_message=None, temperature=0.5, @@ -54,11 +52,6 @@ config = ContinueConfig( step=CommentCodeStep, ), SlashCommand( - name="feedback", - description="Send feedback to improve Continue", - step=FeedbackStep, - ), - SlashCommand( name="clear", description="Clear step history", step=ClearHistoryStep, @@ -79,7 +72,6 @@ config = ContinueConfig( # repo_name="<your github username or organization>/<your repo name>", # auth_token="<your github auth token>" # ), - SearchContextProvider(), DiffContextProvider(), URLContextProvider( preset_urls = [ diff --git a/continuedev/src/continuedev/libs/llm/__init__.py b/continuedev/src/continuedev/libs/llm/__init__.py index b2eecab6..28f614c7 100644 --- a/continuedev/src/continuedev/libs/llm/__init__.py +++ b/continuedev/src/continuedev/libs/llm/__init__.py @@ -1,5 +1,8 @@ +import ssl from typing import Any, Callable, Coroutine, Dict, Generator, List, Optional, Union +import aiohttp +import certifi from pydantic import Field, validator from ...core.main import ChatMessage @@ -83,6 +86,10 @@ class LLM(ContinueBaseModel): None, description="Path to a custom CA bundle to use when making the HTTP request", ) + proxy: Optional[str] = Field( + None, + description="Proxy URL to use when making the HTTP request", + ) prompt_templates: dict = Field( {}, 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.', @@ -134,6 +141,10 @@ class LLM(ContinueBaseModel): "verify_ssl": { "description": "Whether to verify SSL certificates for requests." }, + "ca_bundle_path": { + "description": "Path to a custom CA bundle to use when making the HTTP request" + }, + "proxy": {"description": "Proxy URL to use when making the HTTP request"}, } def dict(self, **kwargs): @@ -155,6 +166,22 @@ class LLM(ContinueBaseModel): """Stop the connection to the LLM.""" pass + def create_client_session(self): + if self.verify_ssl is False: + return aiohttp.ClientSession( + connector=aiohttp.TCPConnector(verify_ssl=False), + timeout=aiohttp.ClientTimeout(total=self.timeout), + ) + else: + ca_bundle_path = ( + certifi.where() if self.ca_bundle_path is None else self.ca_bundle_path + ) + ssl_context = ssl.create_default_context(cafile=ca_bundle_path) + return aiohttp.ClientSession( + connector=aiohttp.TCPConnector(ssl_context=ssl_context), + timeout=aiohttp.ClientTimeout(total=self.timeout), + ) + def collect_args(self, options: CompletionOptions) -> Dict[str, Any]: """Collect the arguments for the LLM.""" args = {**DEFAULT_ARGS.copy(), "model": self.model} @@ -199,6 +226,7 @@ class LLM(ContinueBaseModel): stop: Optional[List[str]] = None, max_tokens: Optional[int] = None, functions: Optional[List[Any]] = None, + log: bool = True, ) -> Generator[Union[Any, List, Dict], None, None]: """Yield completion response, either streamed or not.""" options = CompletionOptions( @@ -220,14 +248,17 @@ class LLM(ContinueBaseModel): if not raw: prompt = self.template_prompt_like_messages(prompt) - self.write_log(f"Prompt: \n\n{prompt}") + if log: + self.write_log(prompt) completion = "" async for chunk in self._stream_complete(prompt=prompt, options=options): yield chunk completion += chunk - self.write_log(f"Completion: \n\n{completion}") + # if log: + # self.write_log(f"Completion: \n\n{completion}") + dev_data_logger.capture( "tokens_generated", {"model": self.model, "tokens": self.count_tokens(completion)}, @@ -246,6 +277,7 @@ class LLM(ContinueBaseModel): stop: Optional[List[str]] = None, max_tokens: Optional[int] = None, functions: Optional[List[Any]] = None, + log: bool = True, ) -> str: """Yield completion response, either streamed or not.""" options = CompletionOptions( @@ -267,11 +299,14 @@ class LLM(ContinueBaseModel): if not raw: prompt = self.template_prompt_like_messages(prompt) - self.write_log(f"Prompt: \n\n{prompt}") + if log: + self.write_log(prompt) completion = await self._complete(prompt=prompt, options=options) - self.write_log(f"Completion: \n\n{completion}") + # if log: + # self.write_log(f"Completion: \n\n{completion}") + dev_data_logger.capture( "tokens_generated", {"model": self.model, "tokens": self.count_tokens(completion)}, @@ -291,6 +326,7 @@ class LLM(ContinueBaseModel): stop: Optional[List[str]] = None, max_tokens: Optional[int] = None, functions: Optional[List[Any]] = None, + log: bool = True, ) -> Generator[Union[Any, List, Dict], None, None]: """Yield completion response, either streamed or not.""" options = CompletionOptions( @@ -313,7 +349,8 @@ class LLM(ContinueBaseModel): else: prompt = format_chat_messages(messages) - self.write_log(f"Prompt: \n\n{prompt}") + if log: + self.write_log(prompt) completion = "" @@ -328,7 +365,9 @@ class LLM(ContinueBaseModel): yield {"role": "assistant", "content": chunk} completion += chunk - self.write_log(f"Completion: \n\n{completion}") + # if log: + # self.write_log(f"Completion: \n\n{completion}") + dev_data_logger.capture( "tokens_generated", {"model": self.model, "tokens": self.count_tokens(completion)}, diff --git a/continuedev/src/continuedev/libs/llm/ggml.py b/continuedev/src/continuedev/libs/llm/ggml.py index 2fd123bd..27a55dfe 100644 --- a/continuedev/src/continuedev/libs/llm/ggml.py +++ b/continuedev/src/continuedev/libs/llm/ggml.py @@ -1,8 +1,6 @@ import json -import ssl from typing import Any, Callable, Coroutine, Dict, List, Optional -import aiohttp from pydantic import Field from ...core.main import ChatMessage @@ -38,10 +36,6 @@ class GGML(LLM): "http://localhost:8000", description="URL of the OpenAI-compatible server where the model is being served", ) - proxy: Optional[str] = Field( - None, - description="Proxy URL to use when making the HTTP request", - ) model: str = Field( "ggml", description="The name of the model to use (optional for the GGML class)" ) @@ -57,20 +51,6 @@ class GGML(LLM): class Config: arbitrary_types_allowed = True - def create_client_session(self): - if self.ca_bundle_path is None: - ssl_context = ssl.create_default_context(cafile=self.ca_bundle_path) - tcp_connector = aiohttp.TCPConnector( - verify_ssl=self.verify_ssl, ssl=ssl_context - ) - else: - tcp_connector = aiohttp.TCPConnector(verify_ssl=self.verify_ssl) - - return aiohttp.ClientSession( - connector=tcp_connector, - timeout=aiohttp.ClientTimeout(total=self.timeout), - ) - def get_headers(self): headers = { "Content-Type": "application/json", diff --git a/continuedev/src/continuedev/libs/llm/hf_tgi.py b/continuedev/src/continuedev/libs/llm/hf_tgi.py index a3672fe2..27d71cb4 100644 --- a/continuedev/src/continuedev/libs/llm/hf_tgi.py +++ b/continuedev/src/continuedev/libs/llm/hf_tgi.py @@ -1,7 +1,6 @@ import json from typing import Any, Callable, List -import aiohttp from pydantic import Field from ...core.main import ChatMessage @@ -36,14 +35,12 @@ class HuggingFaceTGI(LLM): async def _stream_complete(self, prompt, options): args = self.collect_args(options) - async with aiohttp.ClientSession( - connector=aiohttp.TCPConnector(verify_ssl=self.verify_ssl), - timeout=aiohttp.ClientTimeout(total=self.timeout), - ) as client_session: + async with self.create_client_session() as client_session: async with client_session.post( f"{self.server_url}/generate_stream", json={"inputs": prompt, "parameters": args}, headers={"Content-Type": "application/json"}, + proxy=self.proxy, ) as resp: async for line in resp.content.iter_any(): if line: diff --git a/continuedev/src/continuedev/libs/llm/llamacpp.py b/continuedev/src/continuedev/libs/llm/llamacpp.py index c795bd15..0b4c9fb0 100644 --- a/continuedev/src/continuedev/libs/llm/llamacpp.py +++ b/continuedev/src/continuedev/libs/llm/llamacpp.py @@ -1,7 +1,6 @@ import json from typing import Any, Callable, Dict -import aiohttp from pydantic import Field from ..llm import LLM @@ -70,14 +69,12 @@ class LlamaCpp(LLM): headers = {"Content-Type": "application/json"} async def server_generator(): - async with aiohttp.ClientSession( - connector=aiohttp.TCPConnector(verify_ssl=self.verify_ssl), - timeout=aiohttp.ClientTimeout(total=self.timeout), - ) as client_session: + async with self.create_client_session() as client_session: async with client_session.post( f"{self.server_url}/completion", json={"prompt": prompt, "stream": True, **args}, headers=headers, + proxy=self.proxy, ) as resp: async for line in resp.content: content = line.decode("utf-8") diff --git a/continuedev/src/continuedev/libs/llm/ollama.py b/continuedev/src/continuedev/libs/llm/ollama.py index b699398b..19d48a2f 100644 --- a/continuedev/src/continuedev/libs/llm/ollama.py +++ b/continuedev/src/continuedev/libs/llm/ollama.py @@ -5,6 +5,7 @@ import aiohttp from pydantic import Field from ..llm import LLM +from ..util.logging import logger from .prompts.chat import llama2_template_messages from .prompts.edit import simplified_edit_prompt @@ -43,9 +44,19 @@ class Ollama(LLM): async def start(self, **kwargs): await super().start(**kwargs) - self._client_session = aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=self.timeout) - ) + self._client_session = self.create_client_session() + try: + async with self._client_session.post( + f"{self.server_url}/api/generate", + proxy=self.proxy, + json={ + "prompt": "", + "model": self.model, + }, + ) as _: + pass + except Exception as e: + logger.warning(f"Error pre-loading Ollama model: {e}") async def stop(self): await self._client_session.close() @@ -59,6 +70,7 @@ class Ollama(LLM): "system": self.system_message, "options": {"temperature": options.temperature}, }, + proxy=self.proxy, ) as resp: async for line in resp.content.iter_any(): if line: diff --git a/continuedev/src/continuedev/libs/llm/maybe_proxy_openai.py b/continuedev/src/continuedev/libs/llm/openai_free_trial.py index 3fdcb42e..367f2bbd 100644 --- a/continuedev/src/continuedev/libs/llm/maybe_proxy_openai.py +++ b/continuedev/src/continuedev/libs/llm/openai_free_trial.py @@ -6,9 +6,9 @@ from .openai import OpenAI from .proxy_server import ProxyServer -class MaybeProxyOpenAI(LLM): +class OpenAIFreeTrial(LLM): """ - With the `MaybeProxyOpenAI` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. + With the `OpenAIFreeTrial` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. Once you are using Continue regularly though, you will need to add an OpenAI API key that has access to GPT-4 by following these steps: @@ -21,13 +21,13 @@ class MaybeProxyOpenAI(LLM): config = ContinueConfig( ... models=Models( - default=MaybeProxyOpenAI(model="gpt-4", api_key=API_KEY), - medium=MaybeProxyOpenAI(model="gpt-3.5-turbo", api_key=API_KEY) + default=OpenAIFreeTrial(model="gpt-4", api_key=API_KEY), + summarize=OpenAIFreeTrial(model="gpt-3.5-turbo", api_key=API_KEY) ) ) ``` - The `MaybeProxyOpenAI` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. + The `OpenAIFreeTrial` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. These classes support any models available through the OpenAI API, assuming your API key has access, including "gpt-4", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", and "gpt-4-32k". """ diff --git a/continuedev/src/continuedev/libs/llm/prompts/chat.py b/continuedev/src/continuedev/libs/llm/prompts/chat.py index 03230499..0bf8635b 100644 --- a/continuedev/src/continuedev/libs/llm/prompts/chat.py +++ b/continuedev/src/continuedev/libs/llm/prompts/chat.py @@ -28,8 +28,8 @@ def template_alpaca_messages(msgs: List[Dict[str, str]]) -> str: prompt += f"{msgs[0]['content']}\n" msgs.pop(0) - prompt += "### Instruction:\n" for msg in msgs: + prompt += "### Instruction:\n" if msg["role"] == "user" else "### Response:\n" prompt += f"{msg['content']}\n" prompt += "### Response:\n" diff --git a/continuedev/src/continuedev/libs/llm/proxy_server.py b/continuedev/src/continuedev/libs/llm/proxy_server.py index 294c1713..d741fee4 100644 --- a/continuedev/src/continuedev/libs/llm/proxy_server.py +++ b/continuedev/src/continuedev/libs/llm/proxy_server.py @@ -1,10 +1,8 @@ import json -import ssl import traceback from typing import List import aiohttp -import certifi from ...core.main import ChatMessage from ..llm import LLM @@ -32,20 +30,7 @@ class ProxyServer(LLM): **kwargs, ): await super().start(**kwargs) - if self.verify_ssl is False: - self._client_session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(verify_ssl=False), - timeout=aiohttp.ClientTimeout(total=self.timeout), - ) - else: - ca_bundle_path = ( - certifi.where() if self.ca_bundle_path is None else self.ca_bundle_path - ) - ssl_context = ssl.create_default_context(cafile=ca_bundle_path) - self._client_session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(ssl_context=ssl_context), - timeout=aiohttp.ClientTimeout(total=self.timeout), - ) + self._client_session = self.create_client_session() self.context_length = MAX_TOKENS_FOR_MODEL[self.model] @@ -62,6 +47,7 @@ class ProxyServer(LLM): f"{SERVER_URL}/complete", json={"messages": [{"role": "user", "content": prompt}], **args}, headers=self.get_headers(), + proxy=self.proxy, ) as resp: resp_text = await resp.text() if resp.status != 200: @@ -75,6 +61,7 @@ class ProxyServer(LLM): f"{SERVER_URL}/stream_chat", json={"messages": messages, **args}, headers=self.get_headers(), + proxy=self.proxy, ) as resp: if resp.status != 200: raise Exception(await resp.text()) @@ -110,6 +97,7 @@ class ProxyServer(LLM): f"{SERVER_URL}/stream_complete", json={"messages": [{"role": "user", "content": prompt}], **args}, headers=self.get_headers(), + proxy=self.proxy, ) as resp: if resp.status != 200: raise Exception(await resp.text()) diff --git a/continuedev/src/continuedev/libs/llm/together.py b/continuedev/src/continuedev/libs/llm/together.py index 257f9a8f..b679351c 100644 --- a/continuedev/src/continuedev/libs/llm/together.py +++ b/continuedev/src/continuedev/libs/llm/together.py @@ -1,5 +1,5 @@ import json -from typing import Callable, Optional +from typing import Callable import aiohttp from pydantic import Field @@ -68,6 +68,7 @@ class TogetherLLM(LLM): **args, }, headers={"Authorization": f"Bearer {self.api_key}"}, + proxy=self.proxy, ) as resp: async for line in resp.content.iter_chunks(): if line[1]: @@ -99,6 +100,7 @@ class TogetherLLM(LLM): f"{self.base_url}/inference", json={"prompt": prompt, **args}, headers={"Authorization": f"Bearer {self.api_key}"}, + proxy=self.proxy, ) as resp: text = await resp.text() j = json.loads(text) diff --git a/continuedev/src/continuedev/libs/util/logging.py b/continuedev/src/continuedev/libs/util/logging.py index 4a550168..b4799abb 100644 --- a/continuedev/src/continuedev/libs/util/logging.py +++ b/continuedev/src/continuedev/libs/util/logging.py @@ -1,13 +1,31 @@ import logging +import os from .paths import getLogFilePath +logfile_path = getLogFilePath() + +try: + # Truncate the logs that are more than a day old + if os.path.exists(logfile_path): + tail = None + with open(logfile_path, "rb") as f: + f.seek(-32 * 1024, os.SEEK_END) + tail = f.read().decode("utf-8") + + if tail is not None: + with open(logfile_path, "w") as f: + f.write(tail) + +except Exception as e: + print("Error truncating log file: {}".format(e)) + # Create a logger logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # Create a file handler -file_handler = logging.FileHandler(getLogFilePath()) +file_handler = logging.FileHandler(logfile_path) file_handler.setLevel(logging.DEBUG) # Create a console handler diff --git a/continuedev/src/continuedev/libs/util/paths.py b/continuedev/src/continuedev/libs/util/paths.py index e8bbd4ba..9d4eccd6 100644 --- a/continuedev/src/continuedev/libs/util/paths.py +++ b/continuedev/src/continuedev/libs/util/paths.py @@ -1,4 +1,5 @@ import os +from typing import Optional from ..constants.default_config import default_config from ..constants.main import ( @@ -73,6 +74,22 @@ def getSessionsListFilePath(): return path +def migrateConfigFile(existing: str) -> Optional[str]: + if existing.strip() == "": + return default_config + + migrated = ( + existing.replace("MaybeProxyOpenAI", "OpenAIFreeTrial") + .replace("maybe_proxy_openai", "openai_free_trial") + .replace("unused=", "saved=") + .replace("medium=", "summarize=") + ) + if migrated != existing: + return migrated + + return None + + def getConfigFilePath() -> str: path = os.path.join(getGlobalFolderPath(), "config.py") os.makedirs(os.path.dirname(path), exist_ok=True) @@ -81,12 +98,15 @@ def getConfigFilePath() -> str: with open(path, "w") as f: f.write(default_config) else: + # Make any necessary migrations with open(path, "r") as f: existing_content = f.read() - if existing_content.strip() == "": + migrated = migrateConfigFile(existing_content) + + if migrated is not None: with open(path, "w") as f: - f.write(default_config) + f.write(migrated) return path diff --git a/continuedev/src/continuedev/models/main.py b/continuedev/src/continuedev/models/main.py index 34c557e0..5519d718 100644 --- a/continuedev/src/continuedev/models/main.py +++ b/continuedev/src/continuedev/models/main.py @@ -116,6 +116,12 @@ class Range(BaseModel): def contains(self, position: Position) -> bool: return self.start <= position and position <= self.end + def merge_with(self, other: "Range") -> "Range": + return Range( + start=min(self.start, other.start).copy(), + end=max(self.end, other.end).copy(), + ) + @staticmethod def from_indices(string: str, start_index: int, end_index: int) -> "Range": return Range( diff --git a/continuedev/src/continuedev/models/reference/test.py b/continuedev/src/continuedev/models/reference/test.py index 1cebfc36..87f01ede 100644 --- a/continuedev/src/continuedev/models/reference/test.py +++ b/continuedev/src/continuedev/models/reference/test.py @@ -14,7 +14,7 @@ LLM_MODULES = [ ("together", "TogetherLLM"), ("hf_inference_api", "HuggingFaceInferenceAPI"), ("hf_tgi", "HuggingFaceTGI"), - ("maybe_proxy_openai", "MaybeProxyOpenAI"), + ("openai_free_trial", "OpenAIFreeTrial"), ("queued", "QueuedLLM"), ] @@ -101,7 +101,7 @@ for module_name, module_title in LLM_MODULES: markdown_docs = docs_from_schema( schema, f"libs/llm/{module_name}.py", inherited=ctx_properties ) - with open(f"docs/docs/reference/Models/{module_name}.md", "w") as f: + with open(f"docs/docs/reference/Models/{module_title.lower()}.md", "w") as f: f.write(markdown_docs) config_module = importlib.import_module("continuedev.src.continuedev.core.config") @@ -130,7 +130,9 @@ for module_name, module_title in CONTEXT_PROVIDER_MODULES: ], inherited=ctx_properties, ) - with open(f"docs/docs/reference/Context Providers/{module_name}.md", "w") as f: + with open( + f"docs/docs/reference/Context Providers/{module_title.lower()}.md", "w" + ) as f: f.write(markdown_docs) # sdk_module = importlib.import_module("continuedev.src.continuedev.core.sdk") diff --git a/continuedev/src/continuedev/plugins/context_providers/diff.py b/continuedev/src/continuedev/plugins/context_providers/diff.py index 157cbc33..05da3547 100644 --- a/continuedev/src/continuedev/plugins/context_providers/diff.py +++ b/continuedev/src/continuedev/plugins/context_providers/diff.py @@ -4,7 +4,12 @@ from typing import List from pydantic import Field from ...core.context import ContextProvider -from ...core.main import ContextItem, ContextItemDescription, ContextItemId +from ...core.main import ( + ContextItem, + ContextItemDescription, + ContextItemId, + ContinueCustomException, +) class DiffContextProvider(ContextProvider): @@ -44,9 +49,24 @@ class DiffContextProvider(ContextProvider): if not id.provider_title == self.title: raise Exception("Invalid provider title for item") - diff = subprocess.check_output(["git", "diff"], cwd=self.workspace_dir).decode( - "utf-8" + result = subprocess.run( + ["git", "diff"], cwd=self.workspace_dir, capture_output=True, text=True ) + diff = result.stdout + error = result.stderr + if error.strip() != "": + if error.startswith("warning: Not a git repository"): + raise ContinueCustomException( + title="Not a git repository", + message="The @diff context provider only works in git repositories.", + ) + raise ContinueCustomException( + title="Error running git diff", + message=f"Error running git diff:\n\n{error}", + ) + + if diff.strip() == "": + diff = "No changes" ctx_item = self.BASE_CONTEXT_ITEM.copy() ctx_item.content = diff diff --git a/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py b/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py index 0610a8c3..df82b1ab 100644 --- a/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py +++ b/continuedev/src/continuedev/plugins/context_providers/highlighted_code.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from pydantic import BaseModel @@ -35,7 +35,8 @@ class HighlightedCodeContextProvider(ContextProvider): ide: Any # IdeProtocolServer highlighted_ranges: List[HighlightedRangeContextItem] = [] - adding_highlighted_code: bool = False + adding_highlighted_code: bool = True + # Controls whether you can have more than one highlighted range. Now always True. should_get_fallback_context_item: bool = True last_added_fallback: bool = False @@ -177,7 +178,9 @@ class HighlightedCodeContextProvider(ContextProvider): ) async def handle_highlighted_code( - self, range_in_files: List[RangeInFileWithContents] + self, + range_in_files: List[RangeInFileWithContents], + edit: Optional[bool] = False, ): self.should_get_fallback_context_item = True self.last_added_fallback = False @@ -208,47 +211,47 @@ class HighlightedCodeContextProvider(ContextProvider): self.highlighted_ranges = [ HighlightedRangeContextItem( rif=range_in_files[0], - item=self._rif_to_context_item(range_in_files[0], 0, True), + item=self._rif_to_context_item(range_in_files[0], 0, edit), ) ] return - # If current range overlaps with any others, delete them and only keep the new range - new_ranges = [] - for i, hr in enumerate(self.highlighted_ranges): - found_overlap = False - for new_rif in range_in_files: - if hr.rif.filepath == new_rif.filepath and hr.rif.range.overlaps_with( - new_rif.range - ): - found_overlap = True - break + # If editing, make sure none of the other ranges are editing + if edit: + for hr in self.highlighted_ranges: + hr.item.editing = False - # Also don't allow multiple ranges in same file with same content. This is useless to the model, and avoids - # the bug where cmd+f causes repeated highlights + # If new range overlaps with any existing, keep the existing but merged + new_ranges = [] + for i, new_hr in enumerate(range_in_files): + found_overlap_with = None + for existing_rif in self.highlighted_ranges: if ( - hr.rif.filepath == new_rif.filepath - and hr.rif.contents == new_rif.contents + new_hr.filepath == existing_rif.rif.filepath + and new_hr.range.overlaps_with(existing_rif.rif.range) ): - found_overlap = True + existing_rif.rif.range = existing_rif.rif.range.merge_with( + new_hr.range + ) + found_overlap_with = existing_rif break - if not found_overlap: + if found_overlap_with is None: new_ranges.append( HighlightedRangeContextItem( - rif=hr.rif, - item=self._rif_to_context_item(hr.rif, len(new_ranges), False), + rif=new_hr, + item=self._rif_to_context_item( + new_hr, len(self.highlighted_ranges) + i, edit + ), ) ) + elif edit: + # Want to update the range so it's only the newly selected portion + found_overlap_with.rif.range = new_hr.range + found_overlap_with.item.editing = True - self.highlighted_ranges = new_ranges + [ - HighlightedRangeContextItem( - rif=rif, - item=self._rif_to_context_item(rif, len(new_ranges) + idx, False), - ) - for idx, rif in enumerate(range_in_files) - ] + self.highlighted_ranges = self.highlighted_ranges + new_ranges self._make_sure_is_editing_range() self._disambiguate_highlighted_ranges() diff --git a/continuedev/src/continuedev/plugins/policies/default.py b/continuedev/src/continuedev/plugins/policies/default.py index ea3541e3..574d2a1c 100644 --- a/continuedev/src/continuedev/plugins/policies/default.py +++ b/continuedev/src/continuedev/plugins/policies/default.py @@ -1,13 +1,9 @@ -import os -from textwrap import dedent from typing import Type, Union from ...core.config import ContinueConfig from ...core.main import History, Policy, Step from ...core.observation import UserInputObservation -from ...libs.util.paths import getServerFolderPath from ..steps.chat import SimpleChatStep -from ..steps.core.core import MessageStep from ..steps.custom_command import CustomCommandStep from ..steps.main import EditHighlightedCodeStep from ..steps.steps_on_startup import StepsOnStartupStep @@ -59,24 +55,7 @@ class DefaultPolicy(Policy): def next(self, config: ContinueConfig, history: History) -> Step: # At the very start, run initial Steps specified in the config if history.get_current() is None: - shown_welcome_file = os.path.join(getServerFolderPath(), ".shown_welcome") - if os.path.exists(shown_welcome_file): - return StepsOnStartupStep() - - with open(shown_welcome_file, "w") as f: - f.write("") - return ( - MessageStep( - name="Welcome to Continue", - message=dedent( - """\ - - Highlight code section and ask a question or use `/edit` - - Use `cmd+m` (Mac) / `ctrl+m` (Windows) to open Continue - - [Customize Continue](https://continue.dev/docs/customization) by typing '/config' (e.g. use your own API key) """ - ), - ) - >> StepsOnStartupStep() - ) + return StepsOnStartupStep() observation = history.get_current().observation if observation is not None and isinstance(observation, UserInputObservation): diff --git a/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py index 43a2b800..9a5ca2bb 100644 --- a/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py +++ b/continuedev/src/continuedev/plugins/recipes/CreatePipelineRecipe/steps.py @@ -30,7 +30,7 @@ class SetupPipelineStep(Step): sdk.context.set("api_description", self.api_description) source_name = ( - await sdk.models.medium.complete( + await sdk.models.summarize.complete( f"Write a snake_case name for the data source described by {self.api_description}: " ) ).strip() @@ -115,7 +115,7 @@ class ValidatePipelineStep(Step): if "Traceback" in output or "SyntaxError" in output: output = "Traceback" + output.split("Traceback")[-1] file_content = await sdk.ide.readFile(os.path.join(workspace_dir, filename)) - suggestion = await sdk.models.medium.complete( + suggestion = await sdk.models.summarize.complete( dedent( f"""\ ```python @@ -131,7 +131,7 @@ class ValidatePipelineStep(Step): ) ) - api_documentation_url = await sdk.models.medium.complete( + api_documentation_url = await sdk.models.summarize.complete( dedent( f"""\ The API I am trying to call is the '{sdk.context.get('api_description')}'. I tried calling it in the @resource function like this: @@ -216,7 +216,7 @@ class RunQueryStep(Step): ) if "Traceback" in output or "SyntaxError" in output: - suggestion = await sdk.models.medium.complete( + suggestion = await sdk.models.summarize.complete( dedent( f"""\ ```python diff --git a/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py index e2712746..63edabc6 100644 --- a/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py +++ b/continuedev/src/continuedev/plugins/recipes/WritePytestsRecipe/main.py @@ -45,7 +45,7 @@ class WritePytestsRecipe(Step): Here is a complete set of pytest unit tests:""" ) - tests = await sdk.models.medium.complete(prompt) + tests = await sdk.models.summarize.complete(prompt) await sdk.apply_filesystem_edit(AddFile(filepath=path, content=tests)) diff --git a/continuedev/src/continuedev/plugins/steps/README.md b/continuedev/src/continuedev/plugins/steps/README.md index 3f2f804c..a8cae90b 100644 --- a/continuedev/src/continuedev/plugins/steps/README.md +++ b/continuedev/src/continuedev/plugins/steps/README.md @@ -33,7 +33,7 @@ If you'd like to override the default description of your step, which is just th - Return a static string - Store state in a class attribute (prepend with a double underscore, which signifies (through Pydantic) that this is not a parameter for the Step, just internal state) during the run method, and then grab this in the describe method. -- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.medium.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`. +- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.summarize.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`. Here's an example: diff --git a/continuedev/src/continuedev/plugins/steps/chat.py b/continuedev/src/continuedev/plugins/steps/chat.py index b00bf85b..179882bb 100644 --- a/continuedev/src/continuedev/plugins/steps/chat.py +++ b/continuedev/src/continuedev/plugins/steps/chat.py @@ -11,8 +11,8 @@ from pydantic import Field from ...core.main import ChatMessage, FunctionCall, Models, Step, step_to_json_schema from ...core.sdk import ContinueSDK -from ...libs.llm.maybe_proxy_openai import MaybeProxyOpenAI from ...libs.llm.openai import OpenAI +from ...libs.llm.openai_free_trial import OpenAIFreeTrial from ...libs.util.devdata import dev_data_logger from ...libs.util.strings import remove_quotes_and_escapes from ...libs.util.telemetry import posthog_logger @@ -41,7 +41,7 @@ class SimpleChatStep(Step): async def run(self, sdk: ContinueSDK): # Check if proxy server API key if ( - isinstance(sdk.models.default, MaybeProxyOpenAI) + isinstance(sdk.models.default, OpenAIFreeTrial) and ( sdk.models.default.api_key is None or sdk.models.default.api_key.strip() == "" @@ -70,8 +70,8 @@ class SimpleChatStep(Step): config=ContinueConfig( ... models=Models( - default=MaybeProxyOpenAI(api_key="<API_KEY>", model="gpt-4"), - medium=MaybeProxyOpenAI(api_key="<API_KEY>", model="gpt-3.5-turbo") + default=OpenAIFreeTrial(api_key="<API_KEY>", model="gpt-4"), + summarize=OpenAIFreeTrial(api_key="<API_KEY>", model="gpt-3.5-turbo") ) ) ``` @@ -129,9 +129,10 @@ class SimpleChatStep(Step): await sdk.update_ui() self.name = add_ellipsis( remove_quotes_and_escapes( - await sdk.models.medium.complete( + await sdk.models.summarize.complete( f'"{self.description}"\n\nPlease write a short title summarizing the message quoted above. Use no more than 10 words:', max_tokens=20, + log=False, ) ), 200, diff --git a/continuedev/src/continuedev/plugins/steps/chroma.py b/continuedev/src/continuedev/plugins/steps/chroma.py index 25633942..39b0741f 100644 --- a/continuedev/src/continuedev/plugins/steps/chroma.py +++ b/continuedev/src/continuedev/plugins/steps/chroma.py @@ -58,7 +58,7 @@ class AnswerQuestionChroma(Step): Here is the answer:""" ) - answer = await sdk.models.medium.complete(prompt) + answer = await sdk.models.summarize.complete(prompt) # Make paths relative to the workspace directory answer = answer.replace(await sdk.ide.getWorkspaceDirectory(), "") diff --git a/continuedev/src/continuedev/plugins/steps/core/core.py b/continuedev/src/continuedev/plugins/steps/core/core.py index 61de6578..ad2e88e2 100644 --- a/continuedev/src/continuedev/plugins/steps/core/core.py +++ b/continuedev/src/continuedev/plugins/steps/core/core.py @@ -1,16 +1,13 @@ # These steps are depended upon by ContinueSDK import difflib import subprocess -import traceback from textwrap import dedent -from typing import Any, Coroutine, List, Optional, Union - -from pydantic import validator +from typing import Coroutine, List, Optional, Union from ....core.main import ChatMessage, ContinueCustomException, Step from ....core.observation import Observation, TextObservation, UserInputObservation from ....libs.llm import LLM -from ....libs.llm.maybe_proxy_openai import MaybeProxyOpenAI +from ....libs.llm.openai_free_trial import OpenAIFreeTrial from ....libs.util.count_tokens import DEFAULT_MAX_TOKENS from ....libs.util.devdata import dev_data_logger from ....libs.util.strings import ( @@ -57,21 +54,25 @@ class MessageStep(Step): class DisplayErrorStep(Step): name: str = "Error in the Continue server" - e: Any + + title: str = "Error in the Continue server" + message: str = "There was an error in the Continue server." + + @staticmethod + def from_exception(e: Exception) -> "DisplayErrorStep": + if isinstance(e, ContinueCustomException): + return DisplayErrorStep(title=e.title, message=e.message, name=e.title) + + return DisplayErrorStep(message=str(e)) class Config: arbitrary_types_allowed = True - @validator("e", pre=True, always=True) - def validate_e(cls, v): - if isinstance(v, Exception): - return "\n".join(traceback.format_exception(v)) - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return self.e + return self.message async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - raise ContinueCustomException(message=self.e, title=self.name) + raise ContinueCustomException(message=self.message, title=self.title) class FileSystemEditStep(ReversibleStep): @@ -109,7 +110,7 @@ class ShellCommandsStep(Step): return f"Error when running shell commands:\n```\n{self._err_text}\n```" cmds_str = "\n".join(self.cmds) - return await models.medium.complete( + return await models.summarize.complete( f"{cmds_str}\n\nSummarize what was done in these shell commands, using markdown bullet points:" ) @@ -180,10 +181,10 @@ class DefaultModelEditCodeStep(Step): _new_contents: str = "" _prompt_and_completion: str = "" - summary_prompt: str = "Please give brief a description of the changes made above using markdown bullet points. Be concise:" + summary_prompt: str = "Please briefly explain the changes made to the code above. Give no more than 2-3 sentences, and use markdown bullet points:" async def describe(self, models: Models) -> Coroutine[str, None, None]: - name = await models.medium.complete( + name = await models.summarize.complete( f"Write a very short title to describe this requested change (no quotes): '{self.user_input}'. This is the title:" ) self.name = remove_quotes_and_escapes(name) @@ -231,7 +232,7 @@ class DefaultModelEditCodeStep(Step): # If using 3.5 and overflows, upgrade to 3.5.16k if model_to_use.model == "gpt-3.5-turbo": if total_tokens > model_to_use.context_length: - model_to_use = MaybeProxyOpenAI(model="gpt-3.5-turbo-0613") + model_to_use = OpenAIFreeTrial(model="gpt-3.5-turbo-0613") await sdk.start_model(model_to_use) # Remove tokens from the end first, and then the start to clear space @@ -829,7 +830,7 @@ Please output the code to be inserted at the cursor in order to fulfill the user else: self.name = "Generating summary" self.description = "" - async for chunk in sdk.models.medium.stream_complete( + async for chunk in sdk.models.summarize.stream_complete( dedent( f"""\ Diff summary: "{self.user_input}" diff --git a/continuedev/src/continuedev/plugins/steps/main.py b/continuedev/src/continuedev/plugins/steps/main.py index 43299d00..241afe31 100644 --- a/continuedev/src/continuedev/plugins/steps/main.py +++ b/continuedev/src/continuedev/plugins/steps/main.py @@ -105,7 +105,7 @@ class FasterEditHighlightedCodeStep(Step): for rif in range_in_files: rif_dict[rif.filepath] = rif.contents - completion = await sdk.models.medium.complete(prompt) + completion = await sdk.models.summarize.complete(prompt) # Temporarily doing this to generate description. self._prompt = prompt @@ -180,7 +180,7 @@ class StarCoderEditHighlightedCodeStep(Step): _prompt_and_completion: str = "" async def describe(self, models: Models) -> Coroutine[str, None, None]: - return await models.medium.complete( + return await models.summarize.complete( f"{self._prompt_and_completion}\n\nPlease give brief a description of the changes made above using markdown bullet points:" ) diff --git a/continuedev/src/continuedev/plugins/steps/on_traceback.py b/continuedev/src/continuedev/plugins/steps/on_traceback.py index 3a96a8c7..86894818 100644 --- a/continuedev/src/continuedev/plugins/steps/on_traceback.py +++ b/continuedev/src/continuedev/plugins/steps/on_traceback.py @@ -2,7 +2,7 @@ import os from textwrap import dedent from typing import Dict, List, Optional, Tuple -from ...core.main import ChatMessage, Step +from ...core.main import ChatMessage, ContinueCustomException, Step from ...core.sdk import ContinueSDK from ...libs.util.filter_files import should_filter_path from ...libs.util.traceback.traceback_parsers import ( @@ -51,6 +51,12 @@ class DefaultOnTracebackStep(Step): # And this function is where you can get arbitrarily fancy about adding context async def run(self, sdk: ContinueSDK): + if self.output.strip() == "": + raise ContinueCustomException( + title="No terminal open", + message="You must have a terminal open in order to automatically debug with Continue.", + ) + if get_python_traceback(self.output) is not None and sdk.lsp is not None: await sdk.run_step(SolvePythonTracebackStep(output=self.output)) return diff --git a/continuedev/src/continuedev/plugins/steps/react.py b/continuedev/src/continuedev/plugins/steps/react.py index a2612731..1b9bc265 100644 --- a/continuedev/src/continuedev/plugins/steps/react.py +++ b/continuedev/src/continuedev/plugins/steps/react.py @@ -29,7 +29,7 @@ class NLDecisionStep(Step): Select the step which should be taken next to satisfy the user input. Say only the name of the selected step. You must choose one:""" ) - resp = (await sdk.models.medium.complete(prompt)).lower() + resp = (await sdk.models.summarize.complete(prompt)).lower() step_to_run = None for step in self.steps: diff --git a/continuedev/src/continuedev/plugins/steps/search_directory.py b/continuedev/src/continuedev/plugins/steps/search_directory.py index 7ca8a2be..83516719 100644 --- a/continuedev/src/continuedev/plugins/steps/search_directory.py +++ b/continuedev/src/continuedev/plugins/steps/search_directory.py @@ -46,7 +46,7 @@ class WriteRegexPatternStep(Step): async def run(self, sdk: ContinueSDK): # Ask the user for a regex pattern - pattern = await sdk.models.medium.complete( + pattern = await sdk.models.summarize.complete( dedent( f"""\ This is the user request: diff --git a/continuedev/src/continuedev/plugins/steps/setup_model.py b/continuedev/src/continuedev/plugins/steps/setup_model.py index 7fa34907..83e616b0 100644 --- a/continuedev/src/continuedev/plugins/steps/setup_model.py +++ b/continuedev/src/continuedev/plugins/steps/setup_model.py @@ -6,14 +6,14 @@ from ...models.main import Range MODEL_CLASS_TO_MESSAGE = { "OpenAI": "Obtain your OpenAI API key from [here](https://platform.openai.com/account/api-keys) and paste it into the `api_key` field at config.models.default.api_key in `config.py`. Then reload the VS Code window for changes to take effect.", - "MaybeProxyOpenAI": "To get started with OpenAI models, obtain your OpenAI API key from [here](https://platform.openai.com/account/api-keys) and paste it into the `api_key` field at config.models.default.api_key in `config.py`. Then reload the VS Code window for changes to take effect.", + "OpenAIFreeTrial": "To get started with OpenAI models, obtain your OpenAI API key from [here](https://platform.openai.com/account/api-keys) and paste it into the `api_key` field at config.models.default.api_key in `config.py`. Then reload the VS Code window for changes to take effect.", "AnthropicLLM": "To get started with Anthropic, you first need to sign up for the beta [here](https://claude.ai/login) to obtain an API key. Once you have the key, paste it into the `api_key` field at config.models.default.api_key in `config.py`. Then reload the VS Code window for changes to take effect.", "ReplicateLLM": "To get started with Replicate, sign up to obtain an API key [here](https://replicate.ai/), then paste it into the `api_key` field at config.models.default.api_key in `config.py`.", "Ollama": "To get started with Ollama, download the Mac app from [ollama.ai](https://ollama.ai/). Once it is downloaded, be sure to pull at least one model and use its name in the model field in config.py (e.g. `model='codellama'`).", "GGML": "GGML models can be run locally using the `llama-cpp-python` library. To learn how to set up a local llama-cpp-python server, read [here](https://github.com/continuedev/ggml-server-example). Once it is started on port 8000, you're all set!", "TogetherLLM": "To get started using models from Together, first obtain your Together API key from [here](https://together.ai). Paste it into the `api_key` field at config.models.default.api_key in `config.py`. Then, on their models page, press 'start' on the model of your choice and make sure the `model=` parameter in the config file for the `TogetherLLM` class reflects the name of this model. Finally, reload the VS Code window for changes to take effect.", "LlamaCpp": "To get started with this model, clone the [`llama.cpp` repo](https://github.com/ggerganov/llama.cpp) and follow the instructions to set up the server [here](https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md#build). Any of the parameters described in the README can be passed to the `llama_cpp_args` field in the `LlamaCpp` class in `config.py`.", - "HuggingFaceInferenceAPI": "To get started with the HuggingFace Inference API, first deploy a model and obtain your API key from [here](https://huggingface.co/inference-api). Paste it into the `hf_token` field at config.models.default.hf_token in `config.py`. Finally, reload the VS Code window for changes to take effect." + "HuggingFaceInferenceAPI": "To get started with the HuggingFace Inference API, first deploy a model and obtain your API key from [here](https://huggingface.co/inference-api). Paste it into the `hf_token` field at config.models.default.hf_token in `config.py`. Finally, reload the VS Code window for changes to take effect.", } @@ -29,7 +29,7 @@ class SetupModelStep(Step): config_contents = await sdk.ide.readFile(getConfigFilePath()) start = config_contents.find("default=") + len("default=") - end = config_contents.find("unused=") - 1 + end = config_contents.find("saved=") - 1 range = Range.from_indices(config_contents, start, end) range.end.line -= 1 await sdk.ide.highlightCode( diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index 770065ac..9d2ea47a 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -82,7 +82,9 @@ class GUIProtocolServer: return resp_model.parse_obj(resp) def on_error(self, e: Exception): - return self.session.autopilot.continue_sdk.run_step(DisplayErrorStep(e=e)) + return self.session.autopilot.continue_sdk.run_step( + DisplayErrorStep.from_exception(e) + ) def handle_json(self, message_type: str, data: Any): if message_type == "main_input": @@ -97,6 +99,8 @@ class GUIProtocolServer: self.on_retry_at_index(data["index"]) elif message_type == "clear_history": self.on_clear_history() + elif message_type == "set_current_session_title": + self.set_current_session_title(data["title"]) elif message_type == "delete_at_index": self.on_delete_at_index(data["index"]) elif message_type == "delete_context_with_ids": @@ -107,6 +111,8 @@ class GUIProtocolServer: self.on_set_editing_at_ids(data["ids"]) 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() elif message_type == "select_context_item": self.select_context_item(data["id"], data["query"]) elif message_type == "load_session": @@ -180,11 +186,9 @@ class GUIProtocolServer: create_async_task(self.session.autopilot.set_editing_at_ids(ids), self.on_error) def on_show_logs_at_index(self, index: int): - name = "continue_logs.txt" + name = "Continue Context" logs = "\n\n############################################\n\n".join( - [ - "This is a log of the prompt/completion pairs sent/received from the LLM during this step" - ] + ["This is the prompt sent to the LLM during this step"] + self.session.autopilot.continue_sdk.history.timeline[index].logs ) create_async_task( @@ -192,6 +196,22 @@ class GUIProtocolServer: ) posthog_logger.capture_event("show_logs_at_index", {}) + def show_context_virtual_file(self): + async def async_stuff(): + msgs = await self.session.autopilot.continue_sdk.get_chat_context() + 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)) + ) + await self.session.autopilot.ide.showVirtualFile( + "Continue - Selected Context", ctx + ) + + create_async_task( + async_stuff(), + self.on_error, + ) + def select_context_item(self, id: str, query: str): """Called when user selects an item from the dropdown""" create_async_task( @@ -211,6 +231,9 @@ class GUIProtocolServer: posthog_logger.capture_event("load_session", {"session_id": session_id}) + def set_current_session_title(self, title: str): + self.session.autopilot.set_current_session_title(title) + def set_system_message(self, message: str): self.session.autopilot.continue_sdk.config.system_message = message self.session.autopilot.continue_sdk.models.set_system_message(message) @@ -239,14 +262,14 @@ class GUIProtocolServer: # Set models in SDK temp = models.default - models.default = models.unused[index] - models.unused[index] = temp + models.default = models.saved[index] + models.saved[index] = temp await self.session.autopilot.continue_sdk.start_model(models.default) # Set models in config.py JOINER = ",\n\t\t" models_args = { - "unused": f"[{JOINER.join([display_llm_class(llm) for llm in models.unused])}]", + "saved": f"[{JOINER.join([display_llm_class(llm) for llm in models.saved])}]", ("default" if role == "*" else role): display_llm_class(models.default), } @@ -265,48 +288,59 @@ class GUIProtocolServer: def add_model_for_role(self, role: str, model_class: str, model: Any): models = self.session.autopilot.continue_sdk.config.models - unused_models = models.unused if role == "*": async def async_stuff(): - for role in ALL_MODEL_ROLES: - models.__setattr__(role, None) - - # Set and start the default model if didn't already exist from unused - models.default = MODEL_CLASSES[model_class](**model) - await self.session.autopilot.continue_sdk.run_step( - SetupModelStep(model_class=model_class) + # Remove all previous models in roles and place in saved + saved_models = models.saved + existing_saved_models = set( + [display_llm_class(llm) for llm in saved_models] ) - - await self.session.autopilot.continue_sdk.start_model(models.default) - - models_args = {} - for role in ALL_MODEL_ROLES: val = models.__getattribute__(role) - if val is None: - continue # no pun intended + if ( + val is not None + and display_llm_class(val) not in existing_saved_models + ): + saved_models.append(val) + existing_saved_models.add(display_llm_class(val)) + models.__setattr__(role, None) - models_args[role] = display_llm_class(val, True) + # Set and start the new default model + new_model = MODEL_CLASSES[model_class](**model) + models.default = new_model + await self.session.autopilot.continue_sdk.start_model(models.default) + # Construct and set the new models object JOINER = ",\n\t\t" - models_args[ - "unused" - ] = f"[{JOINER.join([display_llm_class(llm) for llm in unused_models])}]" + saved_model_strings = set( + [display_llm_class(llm) for llm in saved_models] + ) + models_args = { + "default": display_llm_class(models.default, True), + "saved": f"[{JOINER.join(saved_model_strings)}]", + } await self.session.autopilot.set_config_attr( ["models"], create_obj_node("Models", models_args), ) + # Add the requisite import to config.py add_config_import( f"from continuedev.src.continuedev.libs.llm.{MODEL_MODULE_NAMES[model_class]} import {model_class}" ) + # Set all roles (in-memory) to the new default model for role in ALL_MODEL_ROLES: if role != "default": models.__setattr__(role, models.default) + # Display setup help + await self.session.autopilot.continue_sdk.run_step( + SetupModelStep(model_class=model_class) + ) + create_async_task(async_stuff(), self.on_error) else: # TODO diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index 7396b1db..d4f0690b 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -4,7 +4,7 @@ import json import os import traceback import uuid -from typing import Any, Callable, Coroutine, List, Type, TypeVar, Union +from typing import Any, Callable, Coroutine, List, Optional, Type, TypeVar, Union import nest_asyncio from fastapi import APIRouter, WebSocket @@ -232,7 +232,8 @@ class IdeProtocolServer(AbstractIdeProtocolServer): self.onFileEdits(fileEdits) elif message_type == "highlightedCodePush": self.onHighlightedCodeUpdate( - [RangeInFileWithContents(**rif) for rif in data["highlightedCode"]] + [RangeInFileWithContents(**rif) for rif in data["highlightedCode"]], + edit=data.get("edit", None), ) elif message_type == "commandOutput": output = data["output"] @@ -243,7 +244,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): elif message_type == "acceptRejectSuggestion": self.onAcceptRejectSuggestion(data["accepted"]) elif message_type == "acceptRejectDiff": - self.onAcceptRejectDiff(data["accepted"]) + self.onAcceptRejectDiff(data["accepted"], data["stepIndex"]) elif message_type == "mainUserInput": self.onMainUserInput(data["input"]) elif message_type == "deleteAtIndex": @@ -349,10 +350,17 @@ class IdeProtocolServer(AbstractIdeProtocolServer): posthog_logger.capture_event("accept_reject_suggestion", {"accepted": accepted}) dev_data_logger.capture("accept_reject_suggestion", {"accepted": accepted}) - def onAcceptRejectDiff(self, accepted: bool): + def onAcceptRejectDiff(self, accepted: bool, step_index: int): posthog_logger.capture_event("accept_reject_diff", {"accepted": accepted}) dev_data_logger.capture("accept_reject_diff", {"accepted": accepted}) + if not accepted: + if autopilot := self.__get_autopilot(): + create_async_task( + autopilot.reject_diff(step_index), + self.on_error, + ) + def onFileSystemUpdate(self, update: FileSystemEdit): # Access to Autopilot (so SessionManager) pass @@ -387,10 +395,14 @@ class IdeProtocolServer(AbstractIdeProtocolServer): if autopilot := self.__get_autopilot(): create_async_task(autopilot.handle_debug_terminal(content), self.on_error) - def onHighlightedCodeUpdate(self, range_in_files: List[RangeInFileWithContents]): + def onHighlightedCodeUpdate( + self, + range_in_files: List[RangeInFileWithContents], + edit: Optional[bool] = False, + ): if autopilot := self.__get_autopilot(): create_async_task( - autopilot.handle_highlighted_code(range_in_files), self.on_error + autopilot.handle_highlighted_code(range_in_files, edit), self.on_error ) ## Subscriptions ## @@ -456,7 +468,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer): resp = await self._send_and_receive_json( {"commands": commands}, TerminalContentsResponse, "getTerminalContents" ) - return resp.contents + return resp.contents.strip() async def getHighlightedCode(self) -> List[RangeInFile]: resp = await self._send_and_receive_json( @@ -640,7 +652,7 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str = None): if session_id is not None and session_id in session_manager.sessions: await session_manager.sessions[session_id].autopilot.continue_sdk.run_step( - DisplayErrorStep(e=e) + DisplayErrorStep.from_exception(e) ) elif ideProtocolServer is not None: await ideProtocolServer.showMessage(f"Error in Continue server: {err_msg}") diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 34030047..015da767 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, Callable, List, Union +from typing import Any, Callable, List, Optional, Union from fastapi import WebSocket @@ -104,7 +104,11 @@ class AbstractIdeProtocolServer(ABC): """Run a command""" @abstractmethod - def onHighlightedCodeUpdate(self, range_in_files: List[RangeInFileWithContents]): + def onHighlightedCodeUpdate( + self, + range_in_files: List[RangeInFileWithContents], + edit: Optional[bool] = False, + ): """Called when highlighted code is updated""" @abstractmethod diff --git a/continuedev/src/continuedev/server/meilisearch_server.py b/continuedev/src/continuedev/server/meilisearch_server.py index 40d46b18..5e6cdd53 100644 --- a/continuedev/src/continuedev/server/meilisearch_server.py +++ b/continuedev/src/continuedev/server/meilisearch_server.py @@ -78,7 +78,7 @@ async def ensure_meilisearch_installed() -> bool: pass existing_paths.remove(meilisearchPath) - await download_meilisearch() + await download_meilisearch() # Clear the existing directories for p in existing_paths: diff --git a/continuedev/src/continuedev/tests/util/config.py b/continuedev/src/continuedev/tests/util/config.py index 73d3aeff..dd0e1f13 100644 --- a/continuedev/src/continuedev/tests/util/config.py +++ b/continuedev/src/continuedev/tests/util/config.py @@ -1,12 +1,12 @@ from continuedev.src.continuedev.core.config import ContinueConfig from continuedev.src.continuedev.core.models import Models -from continuedev.src.continuedev.libs.llm.maybe_proxy_openai import MaybeProxyOpenAI +from continuedev.src.continuedev.libs.llm.openai_free_trial import OpenAIFreeTrial config = ContinueConfig( allow_anonymous_telemetry=False, models=Models( - default=MaybeProxyOpenAI(api_key="", model="gpt-4"), - medium=MaybeProxyOpenAI( + default=OpenAIFreeTrial(api_key="", model="gpt-4"), + summarize=OpenAIFreeTrial( api_key="", model="gpt-3.5-turbo", ), diff --git a/docs/docs/customization/models.md b/docs/docs/customization/models.md index ac3b5f44..cebb0667 100644 --- a/docs/docs/customization/models.md +++ b/docs/docs/customization/models.md @@ -4,9 +4,9 @@ Continue makes it easy to swap out different LLM providers. Once you've added an Commercial Models -- [MaybeProxyOpenAI](../reference/Models/maybe_proxy_openai.md) (default) - Use gpt-4 or gpt-3.5-turbo free with our API key, or with your API key. gpt-4 is probably the most capable model of all options. +- [OpenAIFreeTrial](../reference/Models/openaifreetrial.md) (default) - Use gpt-4 or gpt-3.5-turbo free with our API key, or with your API key. gpt-4 is probably the most capable model of all options. - [OpenAI](../reference/Models/openai.md) - Use any OpenAI model with your own key. Can also change the base URL if you have a server that uses the OpenAI API format, including using the Azure OpenAI service, LocalAI, etc. -- [AnthropicLLM](../reference/Models/anthropic.md) - Use claude-2 with your Anthropic API key. Claude 2 is also highly capable, and has a 100,000 token context window. +- [AnthropicLLM](../reference/Models/anthropicllm.md) - Use claude-2 with your Anthropic API key. Claude 2 is also highly capable, and has a 100,000 token context window. Local Models @@ -17,9 +17,9 @@ Local Models Open-Source Models (not local) -- [TogetherLLM](../reference/Models/together.md) - Use any model from the [Together Models list](https://docs.together.ai/docs/models-inference) with your Together API key. -- [ReplicateLLM](../reference/Models/replicate.md) - Use any open-source model from the [Replicate Streaming List](https://replicate.com/collections/streaming-language-models) with your Replicate API key. -- [HuggingFaceInferenceAPI](../reference/Models/hf_inference_api.md) - Use any open-source model from the [Hugging Face Inference API](https://huggingface.co/inference-api) with your Hugging Face token. +- [TogetherLLM](../reference/Models/togetherllm.md) - Use any model from the [Together Models list](https://docs.together.ai/docs/models-inference) with your Together API key. +- [ReplicateLLM](../reference/Models/replicatellm.md) - Use any open-source model from the [Replicate Streaming List](https://replicate.com/collections/streaming-language-models) with your Replicate API key. +- [HuggingFaceInferenceAPI](../reference/Models/huggingfaceinferenceapi.md) - Use any open-source model from the [Hugging Face Inference API](https://huggingface.co/inference-api) with your Hugging Face token. ## Change the default LLM @@ -31,13 +31,13 @@ from continuedev.src.continuedev.core.models import Models config = ContinueConfig( ... models=Models( - default=MaybeProxyOpenAI(model="gpt-4"), - medium=MaybeProxyOpenAI(model="gpt-3.5-turbo") + default=OpenAIFreeTrial(model="gpt-4"), + summarize=OpenAIFreeTrial(model="gpt-3.5-turbo") ) ) ``` -The `default` and `medium` properties are different _model roles_. This allows different models to be used for different tasks. The available roles are `default`, `small`, `medium`, `large`, `edit`, and `chat`. `edit` is used when you use the '/edit' slash command, `chat` is used for all chat responses, and `medium` is used for summarizing. If not set, all roles will fall back to `default`. The values of these fields must be of the [`LLM`](https://github.com/continuedev/continue/blob/main/continuedev/src/continuedev/libs/llm/__init__.py) class, which implements methods for retrieving and streaming completions from an LLM. +The `default` and `summarize` properties are different _model roles_. This allows different models to be used for different tasks. The available roles are `default`, `summarize`, `edit`, and `chat`. `edit` is used when you use the '/edit' slash command, `chat` is used for all chat responses, and `summarize` is used for summarizing. If not set, all roles will fall back to `default`. The values of these fields must be of the [`LLM`](https://github.com/continuedev/continue/blob/main/continuedev/src/continuedev/libs/llm/__init__.py) class, which implements methods for retrieving and streaming completions from an LLM. Below, we describe the `LLM` classes available in the Continue core library, and how they can be used. diff --git a/docs/docs/how-to-use-continue.md b/docs/docs/how-to-use-continue.md index 3f21d92c..21b12395 100644 --- a/docs/docs/how-to-use-continue.md +++ b/docs/docs/how-to-use-continue.md @@ -21,12 +21,6 @@ If you are trying to use it for a new task and don’t have a sense of how much Remember: You are responsible for all code that you ship, whether it was written by you or by an LLM that you directed. This means it is crucial that you review what the LLM writes. To make this easier, we provide natural language descriptions of the actions the LLM took in the Continue GUI. -## Keyboard shortcuts - -Here you will find a list of all of the default keyboard shortcuts in VS Code: - -![keyboard-shortucts](/img/keyboard-shortcuts.png) - ## When to use Continue Here are tasks that Continue excels at helping you complete: diff --git a/docs/docs/reference/Context Providers/diffcontextprovider.md b/docs/docs/reference/Context Providers/diffcontextprovider.md new file mode 100644 index 00000000..54ba54b9 --- /dev/null +++ b/docs/docs/reference/Context Providers/diffcontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# DiffContextProvider + +Type '@diff' to reference all of the changes you've made to your current branch. This is useful if you want to summarize what you've done or ask for a general review of your work before committing. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/diff.py) + +## Properties + +<ClassPropertyRef name='workspace_dir' details='{"title": "Workspace Dir", "description": "The workspace directory in which to run `git diff`", "type": "string"}' required={false} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "diff", "type": "string"}' required={false} default="diff"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "Diff", "type": "string"}' required={false} default="Diff"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Output of 'git diff' in current repo", "type": "string"}' required={false} default="Output of 'git diff' in current repo"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", "default": false, "type": "boolean"}' required={false} default="False"/> diff --git a/docs/docs/reference/Context Providers/filecontextprovider.md b/docs/docs/reference/Context Providers/filecontextprovider.md new file mode 100644 index 00000000..12e68478 --- /dev/null +++ b/docs/docs/reference/Context Providers/filecontextprovider.md @@ -0,0 +1,19 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# FileContextProvider + +The FileContextProvider is a ContextProvider that allows you to search files in the open workspace. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/file.py) + +## Properties + + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "file", "type": "string"}' required={false} default="file"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "Files", "type": "string"}' required={false} default="Files"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Reference files in the current workspace", "type": "string"}' required={false} default="Reference files in the current workspace"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": false, "type": "boolean"}' required={false} default="False"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", "default": false, "type": "boolean"}' required={false} default="False"/> diff --git a/docs/docs/reference/Context Providers/filetreecontextprovider.md b/docs/docs/reference/Context Providers/filetreecontextprovider.md new file mode 100644 index 00000000..a5b11555 --- /dev/null +++ b/docs/docs/reference/Context Providers/filetreecontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# FileTreeContextProvider + +Type '@tree' to reference the contents of your current workspace. The LLM will be able to see the nested directory structure of your project. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/filetree.py) + +## Properties + +<ClassPropertyRef name='workspace_dir' details='{"title": "Workspace Dir", "description": "The workspace directory to display", "type": "string"}' required={false} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "tree", "type": "string"}' required={false} default="tree"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "File Tree", "type": "string"}' required={false} default="File Tree"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Add a formatted file tree of this directory to the context", "type": "string"}' required={false} default="Add a formatted file tree of this directory to the context"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", "default": false, "type": "boolean"}' required={false} default="False"/> diff --git a/docs/docs/reference/Context Providers/githubissuescontextprovider.md b/docs/docs/reference/Context Providers/githubissuescontextprovider.md new file mode 100644 index 00000000..f174df96 --- /dev/null +++ b/docs/docs/reference/Context Providers/githubissuescontextprovider.md @@ -0,0 +1,21 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# GitHubIssuesContextProvider + +The GitHubIssuesContextProvider is a ContextProvider that allows you to search GitHub issues in a repo. Type '@issue' to reference the title and contents of an issue. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/github.py) + +## Properties + +<ClassPropertyRef name='repo_name' details='{"title": "Repo Name", "description": "The name of the GitHub repo from which to pull issues", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='auth_token' details='{"title": "Auth Token", "description": "The GitHub auth token to use to authenticate with the GitHub API", "type": "string"}' required={true} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "issues", "type": "string"}' required={false} default="issues"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "GitHub Issues", "type": "string"}' required={false} default="GitHub Issues"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Reference GitHub issues", "type": "string"}' required={false} default="Reference GitHub issues"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": false, "type": "boolean"}' required={false} default="False"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", "default": false, "type": "boolean"}' required={false} default="False"/> diff --git a/docs/docs/reference/Context Providers/googlecontextprovider.md b/docs/docs/reference/Context Providers/googlecontextprovider.md new file mode 100644 index 00000000..84a9fdb5 --- /dev/null +++ b/docs/docs/reference/Context Providers/googlecontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# GoogleContextProvider + +Type '@google' to reference the results of a Google search. For example, type "@google python tutorial" if you want to search and discuss ways of learning Python. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/google.py) + +## Properties + +<ClassPropertyRef name='serper_api_key' details='{"title": "Serper Api Key", "description": "Your SerpAPI key, used to programmatically make Google searches. You can get a key at https://serper.dev.", "type": "string"}' required={true} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "google", "type": "string"}' required={false} default="google"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "Google", "type": "string"}' required={false} default="Google"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Search Google", "type": "string"}' required={false} default="Search Google"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "default": true, "type": "boolean"}' required={false} default="True"/> diff --git a/docs/docs/reference/Context Providers/searchcontextprovider.md b/docs/docs/reference/Context Providers/searchcontextprovider.md new file mode 100644 index 00000000..9aa22f33 --- /dev/null +++ b/docs/docs/reference/Context Providers/searchcontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# SearchContextProvider + +Type '@search' to reference the results of codebase search, just like the results you would get from VS Code search. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/search.py) + +## Properties + +<ClassPropertyRef name='workspace_dir' details='{"title": "Workspace Dir", "description": "The workspace directory to search", "type": "string"}' required={false} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "search", "type": "string"}' required={false} default="search"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "Search", "type": "string"}' required={false} default="Search"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Search the workspace for all matches of an exact string (e.g. '@search console.log')", "type": "string"}' required={false} default="Search the workspace for all matches of an exact string (e.g. '@search console.log')"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "default": true, "type": "boolean"}' required={false} default="True"/> diff --git a/docs/docs/reference/Context Providers/terminalcontextprovider.md b/docs/docs/reference/Context Providers/terminalcontextprovider.md new file mode 100644 index 00000000..ca4ad01a --- /dev/null +++ b/docs/docs/reference/Context Providers/terminalcontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# TerminalContextProvider + +Type '@terminal' to reference the contents of your IDE's terminal. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/terminal.py) + +## Properties + +<ClassPropertyRef name='get_last_n_commands' details='{"title": "Get Last N Commands", "description": "The number of previous commands to reference", "default": 3, "type": "integer"}' required={false} default="3"/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "terminal", "type": "string"}' required={false} default="terminal"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "Terminal", "type": "string"}' required={false} default="Terminal"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Reference the contents of the terminal", "type": "string"}' required={false} default="Reference the contents of the terminal"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", "default": false, "type": "boolean"}' required={false} default="False"/> diff --git a/docs/docs/reference/Context Providers/urlcontextprovider.md b/docs/docs/reference/Context Providers/urlcontextprovider.md new file mode 100644 index 00000000..38ddc0e5 --- /dev/null +++ b/docs/docs/reference/Context Providers/urlcontextprovider.md @@ -0,0 +1,20 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# URLContextProvider + +Type '@url' to reference the contents of a URL. You can either reference preset URLs, or reference one dynamically by typing '@url https://example.com'. The text contents of the page will be fetched and used as context. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/plugins/context_providers/url.py) + +## Properties + +<ClassPropertyRef name='preset_urls' details='{"title": "Preset Urls", "description": "A list of preset URLs that you will be able to quickly reference by typing '@url'", "default": [], "type": "array", "items": {"type": "string"}}' required={false} default="[]"/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "default": "url", "type": "string"}' required={false} default="url"/> +<ClassPropertyRef name='display_title' details='{"title": "Display Title", "default": "URL", "type": "string"}' required={false} default="URL"/> +<ClassPropertyRef name='description' details='{"title": "Description", "default": "Reference the contents of a webpage", "type": "string"}' required={false} default="Reference the contents of a webpage"/> +<ClassPropertyRef name='dynamic' details='{"title": "Dynamic", "default": true, "type": "boolean"}' required={false} default="True"/> +<ClassPropertyRef name='requires_query' details='{"title": "Requires Query", "default": true, "type": "boolean"}' required={false} default="True"/> diff --git a/docs/docs/reference/Models/anthropic.md b/docs/docs/reference/Models/anthropic.md index e2c6f683..128b706d 100644 --- a/docs/docs/reference/Models/anthropic.md +++ b/docs/docs/reference/Models/anthropic.md @@ -35,4 +35,5 @@ Claude 2 is not yet publicly released. You can request early access [here](https <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> diff --git a/docs/docs/reference/Models/anthropicllm.md b/docs/docs/reference/Models/anthropicllm.md new file mode 100644 index 00000000..128b706d --- /dev/null +++ b/docs/docs/reference/Models/anthropicllm.md @@ -0,0 +1,39 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# AnthropicLLM + +Import the `AnthropicLLM` class and set it as the default model: + +```python +from continuedev.src.continuedev.libs.llm.anthropic import AnthropicLLM + +config = ContinueConfig( + ... + models=Models( + default=AnthropicLLM(api_key="<API_KEY>", model="claude-2") + ) +) +``` + +Claude 2 is not yet publicly released. You can request early access [here](https://www.anthropic.com/earlyaccess). + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/anthropic.py) + +## Properties + + + +### Inherited Properties + +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "claude-2", "type": "string"}' required={false} default="claude-2"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> diff --git a/docs/docs/reference/Models/ggml.md b/docs/docs/reference/Models/ggml.md index d02f6d05..7bdb5441 100644 --- a/docs/docs/reference/Models/ggml.md +++ b/docs/docs/reference/Models/ggml.md @@ -24,7 +24,6 @@ config = ContinueConfig( ## Properties <ClassPropertyRef name='server_url' details='{"title": "Server Url", "description": "URL of the OpenAI-compatible server where the model is being served", "default": "http://localhost:8000", "type": "string"}' required={false} default="http://localhost:8000"/> -<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> ### Inherited Properties @@ -38,5 +37,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/hf_inference_api.md b/docs/docs/reference/Models/hf_inference_api.md index e7857b21..560309f2 100644 --- a/docs/docs/reference/Models/hf_inference_api.md +++ b/docs/docs/reference/Models/hf_inference_api.md @@ -37,5 +37,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/hf_tgi.md b/docs/docs/reference/Models/hf_tgi.md index ab3f4d61..2cee9fe1 100644 --- a/docs/docs/reference/Models/hf_tgi.md +++ b/docs/docs/reference/Models/hf_tgi.md @@ -22,5 +22,6 @@ import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/huggingfaceinferenceapi.md b/docs/docs/reference/Models/huggingfaceinferenceapi.md new file mode 100644 index 00000000..560309f2 --- /dev/null +++ b/docs/docs/reference/Models/huggingfaceinferenceapi.md @@ -0,0 +1,42 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# HuggingFaceInferenceAPI + +Hugging Face Inference API is a great option for newly released language models. Sign up for an account and add billing [here](https://huggingface.co/settings/billing), access the Inference Endpoints [here](https://ui.endpoints.huggingface.co), click on “New endpoint”, and fill out the form (e.g. select a model like [WizardCoder-Python-34B-V1.0](https://huggingface.co/WizardLM/WizardCoder-Python-34B-V1.0)), and then deploy your model by clicking “Create Endpoint”. Change `~/.continue/config.py` to look like this: + +```python +from continuedev.src.continuedev.core.models import Models +from continuedev.src.continuedev.libs.llm.hf_inference_api import HuggingFaceInferenceAPI + +config = ContinueConfig( + ... + models=Models( + default=HuggingFaceInferenceAPI( + endpoint_url: "<INFERENCE_API_ENDPOINT_URL>", + hf_token: "<HUGGING_FACE_TOKEN>", + ) +) +``` + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/hf_inference_api.py) + +## Properties + +<ClassPropertyRef name='hf_token' details='{"title": "Hf Token", "description": "Your Hugging Face API token", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='endpoint_url' details='{"title": "Endpoint Url", "description": "Your Hugging Face Inference API endpoint URL", "type": "string"}' required={false} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to use (optional for the HuggingFaceInferenceAPI class)", "default": "Hugging Face Inference API", "type": "string"}' required={false} default="Hugging Face Inference API"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/huggingfacetgi.md b/docs/docs/reference/Models/huggingfacetgi.md new file mode 100644 index 00000000..2cee9fe1 --- /dev/null +++ b/docs/docs/reference/Models/huggingfacetgi.md @@ -0,0 +1,27 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# HuggingFaceTGI + + + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/hf_tgi.py) + +## Properties + +<ClassPropertyRef name='server_url' details='{"title": "Server Url", "description": "URL of your TGI server", "default": "http://localhost:8080", "type": "string"}' required={false} default="http://localhost:8080"/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "huggingface-tgi", "type": "string"}' required={false} default="huggingface-tgi"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/llamacpp.md b/docs/docs/reference/Models/llamacpp.md index ae4b6e62..8a6be11e 100644 --- a/docs/docs/reference/Models/llamacpp.md +++ b/docs/docs/reference/Models/llamacpp.md @@ -42,5 +42,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/maybe_proxy_openai.md b/docs/docs/reference/Models/maybe_proxy_openai.md index c080b54d..055054fd 100644 --- a/docs/docs/reference/Models/maybe_proxy_openai.md +++ b/docs/docs/reference/Models/maybe_proxy_openai.md @@ -1,8 +1,8 @@ import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; -# MaybeProxyOpenAI +# OpenAIFreeTrial -With the `MaybeProxyOpenAI` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. +With the `OpenAIFreeTrial` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. Once you are using Continue regularly though, you will need to add an OpenAI API key that has access to GPT-4 by following these steps: @@ -15,23 +15,22 @@ API_KEY = "<API_KEY>" config = ContinueConfig( ... models=Models( - default=MaybeProxyOpenAI(model="gpt-4", api_key=API_KEY), - medium=MaybeProxyOpenAI(model="gpt-3.5-turbo", api_key=API_KEY) + default=OpenAIFreeTrial(model="gpt-4", api_key=API_KEY), + medium=OpenAIFreeTrial(model="gpt-3.5-turbo", api_key=API_KEY) ) ) ``` -The `MaybeProxyOpenAI` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. +The `OpenAIFreeTrial` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. These classes support any models available through the OpenAI API, assuming your API key has access, including "gpt-4", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", and "gpt-4-32k". -[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/maybe_proxy_openai.py) +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/openai_free_trial.py) ## Properties <ClassPropertyRef name='llm' details='{"$ref": "#/definitions/LLM"}' required={false} default=""/> - ### Inherited Properties <ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "type": "string"}' required={true} default=""/> @@ -43,5 +42,6 @@ These classes support any models available through the OpenAI API, assuming your <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/ollama.md b/docs/docs/reference/Models/ollama.md index f0370b45..39257395 100644 --- a/docs/docs/reference/Models/ollama.md +++ b/docs/docs/reference/Models/ollama.md @@ -33,5 +33,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/openai.md b/docs/docs/reference/Models/openai.md index f28e0598..e78dd404 100644 --- a/docs/docs/reference/Models/openai.md +++ b/docs/docs/reference/Models/openai.md @@ -32,7 +32,6 @@ Options for serving models locally with an OpenAI-compatible server include: ## Properties -<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use for requests.", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='api_base' details='{"title": "Api Base", "description": "OpenAI API base URL.", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='api_type' details='{"title": "Api Type", "description": "OpenAI API type.", "enum": ["azure", "openai"], "type": "string"}' required={false} default=""/> <ClassPropertyRef name='api_version' details='{"title": "Api Version", "description": "OpenAI API version. For use with Azure OpenAI Service.", "type": "string"}' required={false} default=""/> @@ -51,4 +50,5 @@ Options for serving models locally with an OpenAI-compatible server include: <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use for requests.", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> diff --git a/docs/docs/reference/Models/openai_free_trial.md b/docs/docs/reference/Models/openai_free_trial.md new file mode 100644 index 00000000..cd510aa8 --- /dev/null +++ b/docs/docs/reference/Models/openai_free_trial.md @@ -0,0 +1,48 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# OpenAIFreeTrial + +With the `OpenAIFreeTrial` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. + +Once you are using Continue regularly though, you will need to add an OpenAI API key that has access to GPT-4 by following these steps: + +1. Copy your API key from https://platform.openai.com/account/api-keys +2. Open `~/.continue/config.py`. You can do this by using the '/config' command in Continue +3. Change the default LLMs to look like this: + +```python +API_KEY = "<API_KEY>" +config = ContinueConfig( + ... + models=Models( + default=OpenAIFreeTrial(model="gpt-4", api_key=API_KEY), + medium=OpenAIFreeTrial(model="gpt-3.5-turbo", api_key=API_KEY) + ) +) +``` + +The `OpenAIFreeTrial` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. + +These classes support any models available through the OpenAI API, assuming your API key has access, including "gpt-4", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", and "gpt-4-32k". + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/openai_free_trial.py) + +## Properties + +<ClassPropertyRef name='llm' details='{"$ref": "#/definitions/LLM"}' required={false} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/openaifreetrial.md b/docs/docs/reference/Models/openaifreetrial.md new file mode 100644 index 00000000..a9efa6cc --- /dev/null +++ b/docs/docs/reference/Models/openaifreetrial.md @@ -0,0 +1,47 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# OpenAIFreeTrial + +With the `OpenAIFreeTrial` `LLM`, new users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key. Continue should just work the first time you install the extension in VS Code. + +Once you are using Continue regularly though, you will need to add an OpenAI API key that has access to GPT-4 by following these steps: + +1. Copy your API key from https://platform.openai.com/account/api-keys +2. Open `~/.continue/config.py`. You can do this by using the '/config' command in Continue +3. Change the default LLMs to look like this: + +```python +API_KEY = "<API_KEY>" +config = ContinueConfig( + ... + models=Models( + default=OpenAIFreeTrial(model="gpt-4", api_key=API_KEY), + summarize=OpenAIFreeTrial(model="gpt-3.5-turbo", api_key=API_KEY) + ) +) +``` + +The `OpenAIFreeTrial` class will automatically switch to using your API key instead of ours. If you'd like to explicitly use one or the other, you can use the `ProxyServer` or `OpenAI` classes instead. + +These classes support any models available through the OpenAI API, assuming your API key has access, including "gpt-4", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", and "gpt-4-32k". + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/openai_free_trial.py) + +## Properties + +<ClassPropertyRef name='llm' details='{"$ref": "#/definitions/LLM"}' required={false} default=""/> + +### Inherited Properties + +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/queued.md b/docs/docs/reference/Models/queued.md index 231aa4dc..06942e3e 100644 --- a/docs/docs/reference/Models/queued.md +++ b/docs/docs/reference/Models/queued.md @@ -35,5 +35,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> <ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/queuedllm.md b/docs/docs/reference/Models/queuedllm.md new file mode 100644 index 00000000..06942e3e --- /dev/null +++ b/docs/docs/reference/Models/queuedllm.md @@ -0,0 +1,40 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# QueuedLLM + +QueuedLLM exists to make up for LLM servers that cannot handle multiple requests at once. It uses a lock to ensure that only one request is being processed at a time. + +If you are already using another LLM class and are experiencing this problem, you can just wrap it with the QueuedLLM class like this: + +```python +from continuedev.src.continuedev.libs.llm.queued import QueuedLLM + +config = ContinueConfig( + ... + models=Models( + default=QueuedLLM(llm=<OTHER_LLM_CLASS>) + ) +) +``` + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/queued.py) + +## Properties + +<ClassPropertyRef name='llm' details='{"title": "Llm", "description": "The LLM to wrap with a lock", "allOf": [{"$ref": "#/definitions/LLM"}]}' required={true} default=""/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "queued", "type": "string"}' required={false} default="queued"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {}, "type": "object"}' required={false} default="{}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/replicate.md b/docs/docs/reference/Models/replicate.md index 83bfd383..879459e0 100644 --- a/docs/docs/reference/Models/replicate.md +++ b/docs/docs/reference/Models/replicate.md @@ -38,4 +38,5 @@ If you don't specify the `model` parameter, it will default to `replicate/llama- <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> diff --git a/docs/docs/reference/Models/replicatellm.md b/docs/docs/reference/Models/replicatellm.md new file mode 100644 index 00000000..879459e0 --- /dev/null +++ b/docs/docs/reference/Models/replicatellm.md @@ -0,0 +1,42 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# ReplicateLLM + +Replicate is a great option for newly released language models or models that you've deployed through their platform. Sign up for an account [here](https://replicate.ai/), copy your API key, and then select any model from the [Replicate Streaming List](https://replicate.com/collections/streaming-language-models). Change `~/.continue/config.py` to look like this: + +```python +from continuedev.src.continuedev.core.models import Models +from continuedev.src.continuedev.libs.llm.replicate import ReplicateLLM + +config = ContinueConfig( + ... + models=Models( + default=ReplicateLLM( + model="replicate/codellama-13b-instruct:da5676342de1a5a335b848383af297f592b816b950a43d251a0a9edd0113604b", + api_key="my-replicate-api-key") + ) +) +``` + +If you don't specify the `model` parameter, it will default to `replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781`. + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/replicate.py) + +## Properties + + + +### Inherited Properties + +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "Replicate API key", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781", "type": "string"}' required={false} default="replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> diff --git a/docs/docs/reference/Models/text_gen_interface.md b/docs/docs/reference/Models/text_gen_interface.md index d910bee2..bb8dce1d 100644 --- a/docs/docs/reference/Models/text_gen_interface.md +++ b/docs/docs/reference/Models/text_gen_interface.md @@ -36,5 +36,6 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Here is the code before editing:\n```\n{{code_to_edit}}\n```\n\nHere is the edit requested:\n\"{{user_input}}\"\n\nHere is the code after editing:"}, "type": "object"}' required={false} default="{'edit': 'Here is the code before editing:\n```\n{{code_to_edit}}\n```\n\nHere is the edit requested:\n"{{user_input}}"\n\nHere is the code after editing:'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Here is the code before editing:\n```\n{{{code_to_edit}}}\n```\n\nHere is the edit requested:\n\"{{{user_input}}}\"\n\nHere is the code after editing:"}, "type": "object"}' required={false} default="{'edit': 'Here is the code before editing:\n```\n{{{code_to_edit}}}\n```\n\nHere is the edit requested:\n"{{{user_input}}}"\n\nHere is the code after editing:'}"/> <ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/textgenui.md b/docs/docs/reference/Models/textgenui.md new file mode 100644 index 00000000..bb8dce1d --- /dev/null +++ b/docs/docs/reference/Models/textgenui.md @@ -0,0 +1,41 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# TextGenUI + +TextGenUI is a comprehensive, open-source language model UI and local server. You can set it up with an OpenAI-compatible server plugin, but if for some reason that doesn't work, you can use this class like so: + +```python +from continuedev.src.continuedev.libs.llm.text_gen_interface import TextGenUI + +config = ContinueConfig( + ... + models=Models( + default=TextGenUI( + model="<MODEL_NAME>", + ) + ) +) +``` + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/text_gen_interface.py) + +## Properties + +<ClassPropertyRef name='server_url' details='{"title": "Server Url", "description": "URL of your TextGenUI server", "default": "http://localhost:5000", "type": "string"}' required={false} default="http://localhost:5000"/> +<ClassPropertyRef name='streaming_url' details='{"title": "Streaming Url", "description": "URL of your TextGenUI streaming server (separate from main server URL)", "default": "http://localhost:5005", "type": "string"}' required={false} default="http://localhost:5005"/> + + +### Inherited Properties + +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "text-gen-ui", "type": "string"}' required={false} default="text-gen-ui"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Here is the code before editing:\n```\n{{{code_to_edit}}}\n```\n\nHere is the edit requested:\n\"{{{user_input}}}\"\n\nHere is the code after editing:"}, "type": "object"}' required={false} default="{'edit': 'Here is the code before editing:\n```\n{{{code_to_edit}}}\n```\n\nHere is the edit requested:\n"{{{user_input}}}"\n\nHere is the code after editing:'}"/> +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "The API key for the LLM provider.", "type": "string"}' required={false} default=""/> diff --git a/docs/docs/reference/Models/together.md b/docs/docs/reference/Models/together.md index 6838ba36..3718f046 100644 --- a/docs/docs/reference/Models/together.md +++ b/docs/docs/reference/Models/together.md @@ -38,4 +38,5 @@ config = ContinueConfig( <ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> <ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> <ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> -<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{code_to_edit}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{user_input}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> diff --git a/docs/docs/reference/Models/togetherllm.md b/docs/docs/reference/Models/togetherllm.md new file mode 100644 index 00000000..3718f046 --- /dev/null +++ b/docs/docs/reference/Models/togetherllm.md @@ -0,0 +1,42 @@ +import ClassPropertyRef from '@site/src/components/ClassPropertyRef.tsx'; + +# TogetherLLM + +The Together API is a cloud platform for running large AI models. You can sign up [here](https://api.together.xyz/signup), copy your API key on the initial welcome screen, and then hit the play button on any model from the [Together Models list](https://docs.together.ai/docs/models-inference). Change `~/.continue/config.py` to look like this: + +```python +from continuedev.src.continuedev.core.models import Models +from continuedev.src.continuedev.libs.llm.together import TogetherLLM + +config = ContinueConfig( + ... + models=Models( + default=TogetherLLM( + api_key="<API_KEY>", + model="togethercomputer/llama-2-13b-chat" + ) + ) +) +``` + +[View the source](https://github.com/continuedev/continue/tree/main/continuedev/src/continuedev/libs/llm/together.py) + +## Properties + +<ClassPropertyRef name='base_url' details='{"title": "Base Url", "description": "The base URL for your Together API instance", "default": "https://api.together.xyz", "type": "string"}' required={false} default="https://api.together.xyz"/> + + +### Inherited Properties + +<ClassPropertyRef name='api_key' details='{"title": "Api Key", "description": "Together API key", "type": "string"}' required={true} default=""/> +<ClassPropertyRef name='title' details='{"title": "Title", "description": "A title that will identify this model in the model selection dropdown", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='system_message' details='{"title": "System Message", "description": "A system message that will always be followed by the LLM", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='context_length' details='{"title": "Context Length", "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", "default": 2048, "type": "integer"}' required={false} default="2048"/> +<ClassPropertyRef name='unique_id' details='{"title": "Unique Id", "description": "The unique ID of the user.", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='model' details='{"title": "Model", "description": "The name of the model to be used (e.g. gpt-4, codellama)", "default": "togethercomputer/RedPajama-INCITE-7B-Instruct", "type": "string"}' required={false} default="togethercomputer/RedPajama-INCITE-7B-Instruct"/> +<ClassPropertyRef name='stop_tokens' details='{"title": "Stop Tokens", "description": "Tokens that will stop the completion.", "type": "array", "items": {"type": "string"}}' required={false} default=""/> +<ClassPropertyRef name='timeout' details='{"title": "Timeout", "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", "default": 300, "type": "integer"}' required={false} default="300"/> +<ClassPropertyRef name='verify_ssl' details='{"title": "Verify Ssl", "description": "Whether to verify SSL certificates for requests.", "type": "boolean"}' required={false} default=""/> +<ClassPropertyRef name='ca_bundle_path' details='{"title": "Ca Bundle Path", "description": "Path to a custom CA bundle to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='proxy' details='{"title": "Proxy", "description": "Proxy URL to use when making the HTTP request", "type": "string"}' required={false} default=""/> +<ClassPropertyRef name='prompt_templates' details='{"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.", "default": {"edit": "Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags."}, "type": "object"}' required={false} default="{'edit': 'Consider the following code:\n```\n{{{code_to_edit}}}\n```\nEdit the code to perfectly satisfy the following user request:\n{{{user_input}}}\nOutput nothing except for the code. No code block, no English explanation, no start/end tags.'}"/> diff --git a/docs/docs/reference/config.md b/docs/docs/reference/config.md index f867ee1e..1f683ed2 100644 --- a/docs/docs/reference/config.md +++ b/docs/docs/reference/config.md @@ -11,7 +11,7 @@ Continue can be deeply customized by editing the `ContinueConfig` object in `~/. <ClassPropertyRef name='steps_on_startup' details='{"title": "Steps On Startup", "description": "Steps that will be automatically run at the beginning of a new session", "default": [], "type": "array", "items": {"$ref": "#/definitions/Step"}}' required={false} default="[]"/> <ClassPropertyRef name='disallowed_steps' details='{"title": "Disallowed Steps", "description": "Steps that are not allowed to be run, and will be skipped if attempted", "default": [], "type": "array", "items": {"type": "string"}}' required={false} default="[]"/> <ClassPropertyRef name='allow_anonymous_telemetry' details='{"title": "Allow Anonymous Telemetry", "description": "If this field is set to True, we will collect anonymous telemetry as described in the documentation page on telemetry. If set to False, we will not collect any data.", "default": true, "type": "boolean"}' required={false} default="True"/> -<ClassPropertyRef name='models' details='{"title": "Models", "description": "Configuration for the models used by Continue. Read more about how to configure models in the documentation.", "default": {"default": {"title": null, "system_message": null, "context_length": 2048, "model": "gpt-4", "stop_tokens": null, "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "MaybeProxyOpenAI"}, "small": null, "medium": {"title": null, "system_message": null, "context_length": 2048, "model": "gpt-3.5-turbo", "stop_tokens": null, "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "MaybeProxyOpenAI"}, "large": null, "edit": null, "chat": null, "unused": []}, "allOf": [{"$ref": "#/definitions/Models"}]}' required={false} default="{'default': {'title': None, 'system_message': None, 'context_length': 2048, 'model': 'gpt-4', 'stop_tokens': None, 'timeout': 300, 'verify_ssl': None, 'ca_bundle_path': None, 'prompt_templates': {}, 'api_key': None, 'llm': None, 'class_name': 'MaybeProxyOpenAI'}, 'small': None, 'medium': {'title': None, 'system_message': None, 'context_length': 2048, 'model': 'gpt-3.5-turbo', 'stop_tokens': None, 'timeout': 300, 'verify_ssl': None, 'ca_bundle_path': None, 'prompt_templates': {}, 'api_key': None, 'llm': None, 'class_name': 'MaybeProxyOpenAI'}, 'large': None, 'edit': None, 'chat': None, 'unused': []}"/> +<ClassPropertyRef name='models' details='{"title": "Models", "description": "Configuration for the models used by Continue. Read more about how to configure models in the documentation.", "default": {"default": {"title": null, "system_message": null, "context_length": 2048, "model": "gpt-4", "stop_tokens": null, "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, "proxy": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "OpenAIFreeTrial"}, "summarize": {"title": null, "system_message": null, "context_length": 2048, "model": "gpt-3.5-turbo", "stop_tokens": null, "timeout": 300, "verify_ssl": null, "ca_bundle_path": null, "proxy": null, "prompt_templates": {}, "api_key": null, "llm": null, "class_name": "OpenAIFreeTrial"}, "edit": null, "chat": null, "saved": []}, "allOf": [{"$ref": "#/definitions/Models"}]}' required={false} default="{'default': {'title': None, 'system_message': None, 'context_length': 2048, 'model': 'gpt-4', 'stop_tokens': None, 'timeout': 300, 'verify_ssl': None, 'ca_bundle_path': None, 'proxy': None, 'prompt_templates': {}, 'api_key': None, 'llm': None, 'class_name': 'OpenAIFreeTrial'}, 'summarize': {'title': None, 'system_message': None, 'context_length': 2048, 'model': 'gpt-3.5-turbo', 'stop_tokens': None, 'timeout': 300, 'verify_ssl': None, 'ca_bundle_path': None, 'proxy': None, 'prompt_templates': {}, 'api_key': None, 'llm': None, 'class_name': 'OpenAIFreeTrial'}, 'edit': None, 'chat': None, 'saved': []}"/> <ClassPropertyRef name='temperature' details='{"title": "Temperature", "description": "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.", "default": 0.5, "type": "number"}' required={false} default="0.5"/> <ClassPropertyRef name='custom_commands' details='{"title": "Custom Commands", "description": "An array of custom commands that allow you to reuse prompts. Each has name, description, and prompt properties. When you enter /<name> in the text input, it will act as a shortcut to the prompt.", "default": [{"name": "test", "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.", "description": "This is an example custom command. Use /config to edit it and create more"}], "type": "array", "items": {"$ref": "#/definitions/CustomCommand"}}' required={false} default="[{'name': 'test', '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.", 'description': 'This is an example custom command. Use /config to edit it and create more'}]"/> <ClassPropertyRef name='slash_commands' details='{"title": "Slash Commands", "description": "An array of slash commands that let you map custom Steps to a shortcut.", "default": [], "type": "array", "items": {"$ref": "#/definitions/SlashCommand"}}' required={false} default="[]"/> @@ -23,6 +23,4 @@ Continue can be deeply customized by editing the `ContinueConfig` object in `~/. <ClassPropertyRef name='data_server_url' details='{"title": "Data Server Url", "description": "The URL of the server where development data is sent. No data is sent unless a valid user token is provided.", "default": "https://us-west1-autodebug.cloudfunctions.net", "type": "string"}' required={false} default="https://us-west1-autodebug.cloudfunctions.net"/> <ClassPropertyRef name='disable_summaries' details='{"title": "Disable Summaries", "description": "If set to `True`, Continue will not generate summaries for each Step. This can be useful if you want to save on compute.", "default": false, "type": "boolean"}' required={false} default="False"/> - ### Inherited Properties - diff --git a/docs/docs/walkthroughs/create-a-recipe.md b/docs/docs/walkthroughs/create-a-recipe.md index 3ec641c6..0d92fb92 100644 --- a/docs/docs/walkthroughs/create-a-recipe.md +++ b/docs/docs/walkthroughs/create-a-recipe.md @@ -31,7 +31,7 @@ If you'd like to override the default description of your steps, which is just t - Return a static string
- Store state in a class attribute (prepend with a double underscore, which signifies (through Pydantic) that this is not a parameter for the Step, just internal state) during the run method, and then grab this in the describe method.
-- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.medium.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`.
+- Use state in conjunction with the `models` parameter of the describe method to autogenerate a description with a language model. For example, if you'd used an attribute called `__code_written` to store a string representing some code that was written, you could implement describe as `return models.summarize.complete(f"{self.\_\_code_written}\n\nSummarize the changes made in the above code.")`.
## 2. Compose steps together into a complete recipe
diff --git a/docs/static/img/keyboard-shortcuts.png b/docs/static/img/keyboard-shortcuts.png Binary files differdeleted file mode 100644 index a9b75fc5..00000000 --- a/docs/static/img/keyboard-shortcuts.png +++ /dev/null diff --git a/extension/react-app/public/logos/anthropic.png b/extension/react-app/public/logos/anthropic.png Binary files differnew file mode 100644 index 00000000..9adf1b71 --- /dev/null +++ b/extension/react-app/public/logos/anthropic.png diff --git a/extension/react-app/public/logos/hf.png b/extension/react-app/public/logos/hf.png Binary files differnew file mode 100644 index 00000000..49e2841d --- /dev/null +++ b/extension/react-app/public/logos/hf.png diff --git a/extension/react-app/public/logos/llamacpp.png b/extension/react-app/public/logos/llamacpp.png Binary files differnew file mode 100644 index 00000000..119087b0 --- /dev/null +++ b/extension/react-app/public/logos/llamacpp.png diff --git a/extension/react-app/public/logos/lmstudio.png b/extension/react-app/public/logos/lmstudio.png Binary files differnew file mode 100644 index 00000000..b2b73591 --- /dev/null +++ b/extension/react-app/public/logos/lmstudio.png diff --git a/extension/react-app/public/logos/meta.svg b/extension/react-app/public/logos/meta.svg new file mode 100644 index 00000000..ba1d38d2 --- /dev/null +++ b/extension/react-app/public/logos/meta.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#fff" width="800px" height="800px" viewBox="0 0 32 32" id="Camada_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M5,19.5c0-4.6,2.3-9.4,5-9.4c1.5,0,2.7,0.9,4.6,3.6c-1.8,2.8-2.9,4.5-2.9,4.5c-2.4,3.8-3.2,4.6-4.5,4.6 C5.9,22.9,5,21.7,5,19.5 M20.7,17.8L19,15c-0.4-0.7-0.9-1.4-1.3-2c1.5-2.3,2.7-3.5,4.2-3.5c3,0,5.4,4.5,5.4,10.1 c0,2.1-0.7,3.3-2.1,3.3S23.3,22,20.7,17.8 M16.4,11c-2.2-2.9-4.1-4-6.3-4C5.5,7,2,13.1,2,19.5c0,4,1.9,6.5,5.1,6.5 c2.3,0,3.9-1.1,6.9-6.3c0,0,1.2-2.2,2.1-3.7c0.3,0.5,0.6,1,0.9,1.6l1.4,2.4c2.7,4.6,4.2,6.1,6.9,6.1c3.1,0,4.8-2.6,4.8-6.7 C30,12.6,26.4,7,22.1,7C19.8,7,18,8.8,16.4,11"/></svg>
\ No newline at end of file diff --git a/extension/react-app/public/logos/ollama.png b/extension/react-app/public/logos/ollama.png Binary files differnew file mode 100644 index 00000000..56ef23f4 --- /dev/null +++ b/extension/react-app/public/logos/ollama.png diff --git a/extension/react-app/public/logos/openai.svg b/extension/react-app/public/logos/openai.svg new file mode 100644 index 00000000..9aacd2a1 --- /dev/null +++ b/extension/react-app/public/logos/openai.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#fff" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
\ No newline at end of file diff --git a/extension/react-app/public/logos/replicate.png b/extension/react-app/public/logos/replicate.png Binary files differnew file mode 100644 index 00000000..f71504f5 --- /dev/null +++ b/extension/react-app/public/logos/replicate.png diff --git a/extension/react-app/public/logos/together.png b/extension/react-app/public/logos/together.png Binary files differnew file mode 100644 index 00000000..21295358 --- /dev/null +++ b/extension/react-app/public/logos/together.png diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx index edcac4a0..bbb1a952 100644 --- a/extension/react-app/src/App.tsx +++ b/extension/react-app/src/App.tsx @@ -1,5 +1,6 @@ import GUI from "./pages/gui"; import History from "./pages/history"; +import Help from "./pages/help"; import Layout from "./components/Layout"; import { createContext, useEffect } from "react"; import useContinueGUIProtocol from "./hooks/useWebsocket"; @@ -18,6 +19,8 @@ import { postVscMessage } from "./vscode"; import { RouterProvider, createMemoryRouter } from "react-router-dom"; import ErrorPage from "./pages/error"; import SettingsPage from "./pages/settings"; +import Models from "./pages/models"; +import HelpPage from "./pages/help"; const router = createMemoryRouter([ { @@ -38,9 +41,21 @@ const router = createMemoryRouter([ element: <History />, }, { + path: "/help", + element: <Help />, + }, + { path: "/settings", element: <SettingsPage />, }, + { + path: "/models", + element: <Models />, + }, + { + path: "/help", + element: <HelpPage />, + }, ], }, ]); diff --git a/extension/react-app/src/components/CheckDiv.tsx b/extension/react-app/src/components/CheckDiv.tsx index e595d70b..eaea0dc1 100644 --- a/extension/react-app/src/components/CheckDiv.tsx +++ b/extension/react-app/src/components/CheckDiv.tsx @@ -30,6 +30,9 @@ const StyledDiv = styled.div<{ checked: boolean }>` margin: 0.5rem; height: 1.4em; + + overflow: hidden; + text-overflow: ellipsis; `; function CheckDiv(props: CheckDivProps) { diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 48df368b..e63499bc 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -3,12 +3,12 @@ import React, { useContext, useEffect, useImperativeHandle, + useLayoutEffect, useState, } from "react"; import { useCombobox } from "downshift"; import styled from "styled-components"; import { - StyledTooltip, buttonColor, defaultBorderRadius, lightGray, @@ -19,53 +19,51 @@ import { import PillButton from "./PillButton"; import HeaderButtonWithText from "./HeaderButtonWithText"; import { - BookmarkIcon, - DocumentPlusIcon, - FolderArrowDownIcon, ArrowLeftIcon, - PlusIcon, ArrowRightIcon, + MagnifyingGlassIcon, + TrashIcon, } from "@heroicons/react/24/outline"; -import { ContextItem } from "../../../schema/FullState"; import { postVscMessage } from "../vscode"; import { GUIClientContext } from "../App"; import { MeiliSearch } from "meilisearch"; -import { - setBottomMessage, - setDialogMessage, - setShowDialog, -} from "../redux/slices/uiStateSlice"; +import { setBottomMessage } from "../redux/slices/uiStateSlice"; import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; -import SelectContextGroupDialog from "./dialogs/SelectContextGroupDialog"; -import AddContextGroupDialog from "./dialogs/AddContextGroupDialog"; +import ContinueButton from "./ContinueButton"; const SEARCH_INDEX_NAME = "continue_context_items"; // #region styled components -const mainInputFontSize = 13; -const EmptyPillDiv = styled.div` - padding: 4px; - padding-left: 8px; - padding-right: 8px; - border-radius: ${defaultBorderRadius}; - border: 1px dashed ${lightGray}; - color: ${lightGray}; - background-color: ${vscBackground}; - overflow: hidden; +const HiddenHeaderButtonWithText = styled.button` + opacity: 0; + background-color: transparent; + border: none; + outline: none; + color: ${vscForeground}; + cursor: pointer; display: flex; align-items: center; - text-align: center; - cursor: pointer; - font-size: 13px; + justify-content: center; + height: 0; + aspect-ratio: 1; + padding: 0; + margin-left: -8px; + + border-radius: ${defaultBorderRadius}; - &:hover { - background-color: ${lightGray}; - color: ${vscBackground}; + &:focus { + margin-left: 1px; + height: fit-content; + padding: 3px; + opacity: 1; + outline: 1px solid ${lightGray}; } `; +const mainInputFontSize = 13; + const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>` resize: none; @@ -79,20 +77,20 @@ const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>` background-color: ${secondaryDark}; color: ${vscForeground}; z-index: 1; - border: 1px solid + border: 0.5px solid ${(props) => props.inQueryForDynamicProvider ? buttonColor : "transparent"}; &:focus { - outline: 1px solid + outline: 0.5px solid ${(props) => (props.inQueryForDynamicProvider ? buttonColor : lightGray)}; - border: 1px solid transparent; + border: 0.5px solid transparent; background-color: ${(props) => props.inQueryForDynamicProvider ? `${buttonColor}22` : secondaryDark}; } &::placeholder { - color: ${lightGray}80; + color: ${lightGray}cc; } `; @@ -110,23 +108,6 @@ const DynamicQueryTitleDiv = styled.div` background-color: ${buttonColor}; `; -const StyledPlusIcon = styled(PlusIcon)` - position: absolute; - right: 0px; - top: 0px; - height: fit-content; - padding: 0; - cursor: pointer; - border-radius: ${defaultBorderRadius}; - z-index: 2; - - background-color: ${vscBackground}; - - &:hover { - background-color: ${secondaryDark}; - } -`; - const UlMaxHeight = 300; const Ul = styled.ul<{ hidden: boolean; @@ -137,7 +118,7 @@ const Ul = styled.ul<{ ${(props) => props.showAbove ? `transform: translateY(-${props.ulHeightPixels + 8}px);` - : `transform: translateY(${5 * mainInputFontSize}px);`} + : `transform: translateY(${5 * mainInputFontSize - 2}px);`} position: absolute; background: ${vscBackground}; color: ${vscForeground}; @@ -148,7 +129,7 @@ const Ul = styled.ul<{ padding: 0; ${({ hidden }) => hidden && "display: none;"} border-radius: ${defaultBorderRadius}; - outline: 1px solid ${lightGray}; + outline: 0.5px solid ${lightGray}; z-index: 2; -ms-overflow-style: none; @@ -180,14 +161,17 @@ const Li = styled.li<{ // #endregion +interface ComboBoxItem { + name: string; + description: string; + id?: string; + content?: string; +} interface ComboBoxProps { - items: { name: string; description: string; id?: string; content?: string }[]; onInputValueChange: (inputValue: string) => void; disabled?: boolean; - onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void; - selectedContextItems: ContextItem[]; + onEnter: (e?: React.KeyboardEvent<HTMLInputElement>) => void; onToggleAddContext: () => void; - addingHighlightedCode: boolean; } const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { @@ -197,14 +181,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const workspacePaths = useSelector( (state: RootStore) => state.config.workspacePaths ); - const savedContextGroups = useSelector( - (state: RootStore) => state.serverState.saved_context_groups - ); const [history, setHistory] = React.useState<string[]>([]); // The position of the current command you are typing now, so the one that will be appended to history once you press enter const [positionInHistory, setPositionInHistory] = React.useState<number>(0); - const [items, setItems] = React.useState(props.items); + const [items, setItems] = React.useState<ComboBoxItem[]>([]); const inputRef = React.useRef<HTMLInputElement>(null); @@ -217,6 +198,27 @@ 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) => { + return { + name: `/${cmd.name}`, + description: cmd.description, + }; + }); + const selectedContextItems = useSelector( + (state: RootStore) => state.serverState.selected_context_items + ); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [sessionId, inputRef.current]); + useEffect(() => { if (!currentlyInContextQuery) { setNestedContextProvider(undefined); @@ -237,7 +239,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { useEffect(() => { if (!nestedContextProvider) { - console.log("setting items", nestedContextProvider); setItems( contextProviders?.map((provider) => ({ name: provider.display_title, @@ -248,6 +249,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { } }, [nestedContextProvider]); + const [prevInputValue, setPrevInputValue] = useState(""); + const onInputValueChangeCallback = useCallback( ({ inputValue, highlightedIndex }: any) => { // Clear the input @@ -257,6 +260,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { setCurrentlyInContextQuery(false); return; } + + // Hacky way of stopping bug where first context provider title is injected into input + if ( + prevInputValue === "" && + contextProviders.some((p) => p.display_title === inputValue) + ) { + downshiftProps.setInputValue(""); + setPrevInputValue(""); + return; + } + setPrevInputValue(inputValue); + if ( inQueryForContextProvider && !inputValue.startsWith(`@${inQueryForContextProvider.title}`) @@ -277,9 +292,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { if (nestedContextProvider && !inputValue.endsWith("@")) { // Search only within this specific context provider + const spaceSegs = providerAndQuery.split(" "); getFilteredContextItemsForProvider( nestedContextProvider.title, - providerAndQuery + spaceSegs.length > 1 ? spaceSegs[1] : "" ).then((res) => { setItems(res); }); @@ -316,48 +332,19 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // Handle slash commands setItems( - props.items?.filter((item) => - item.name.toLowerCase().startsWith(inputValue.toLowerCase()) + availableSlashCommands?.filter((slashCommand) => + slashCommand.name.toLowerCase().startsWith(inputValue.toLowerCase()) ) || [] ); }, [ - props.items, + availableSlashCommands, currentlyInContextQuery, nestedContextProvider, inQueryForContextProvider, ] ); - const onSelectedItemChangeCallback = useCallback( - ({ selectedItem }: any) => { - if (!selectedItem) return; - if (selectedItem.id) { - // Get the query from the input value - const segs = downshiftProps.inputValue.split("@"); - const query = segs[segs.length - 1]; - - // Tell server the context item was selected - client?.selectContextItem(selectedItem.id, query); - if (downshiftProps.inputValue.includes("@")) { - const selectedNestedContextProvider = contextProviders.find( - (provider) => provider.title === selectedItem.id - ); - if ( - !nestedContextProvider && - !selectedNestedContextProvider?.dynamic - ) { - downshiftProps.setInputValue(`@${selectedItem.id} `); - setNestedContextProvider(selectedNestedContextProvider); - } else { - downshiftProps.setInputValue(""); - } - } - } - }, - [nestedContextProvider, contextProviders, client] - ); - const getFilteredContextItemsForProvider = async ( provider: string, query: string @@ -390,7 +377,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { }; const { getInputProps, ...downshiftProps } = useCombobox({ - onSelectedItemChange: onSelectedItemChangeCallback, onInputValueChange: onInputValueChangeCallback, items, itemToString(item) { @@ -427,7 +413,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const focusedItemIndex = focusableItemsArray.findIndex( (item) => item === document.activeElement ); - console.log(focusedItemIndex, focusableItems); if (focusedItemIndex === focusableItemsArray.length - 1) { inputRef.current?.focus(); } else if (focusedItemIndex !== -1) { @@ -457,6 +442,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { } }, []); + useLayoutEffect(() => { + if (!ulRef.current) { + return; + } + downshiftProps.setHighlightedIndex(0); + }, [items, downshiftProps.setHighlightedIndex, ulRef.current]); + const [metaKeyPressed, setMetaKeyPressed] = useState(false); const [focused, setFocused] = useState(false); useEffect(() => { @@ -476,7 +468,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); }; - }); + }, []); useEffect(() => { if (!inputRef.current) { @@ -489,7 +481,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { } else if (event.data.type === "focusContinueInputWithEdit") { inputRef.current!.focus(); - downshiftProps.setInputValue("/edit "); + if (!inputRef.current?.value.startsWith("/edit")) { + downshiftProps.setInputValue("/edit "); + } } }; window.addEventListener("message", handler); @@ -500,21 +494,69 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { const selectContextItemFromDropdown = useCallback( (event: any) => { - const newProviderName = items[downshiftProps.highlightedIndex].name; + const newItem = items[downshiftProps.highlightedIndex]; + const newProviderName = newItem?.name; const newProvider = contextProviders.find( (provider) => provider.display_title === newProviderName ); if (!newProvider) { + if (nestedContextProvider && newItem.id) { + // Tell server the context item was selected + client?.selectContextItem(newItem.id, ""); + + // Clear the input + downshiftProps.setInputValue(""); + setCurrentlyInContextQuery(false); + setNestedContextProvider(undefined); + setInQueryForContextProvider(undefined); + (event.nativeEvent as any).preventDownshiftDefault = true; + event.preventDefault(); + return; + } + // This is a slash command (event.nativeEvent as any).preventDownshiftDefault = true; + event.preventDefault(); return; } else if (newProvider.dynamic && newProvider.requires_query) { + // This is a dynamic context provider that requires a query, like URL / Search setInQueryForContextProvider(newProvider); downshiftProps.setInputValue(`@${newProvider.title} `); (event.nativeEvent as any).preventDownshiftDefault = true; event.preventDefault(); return; } else if (newProvider.dynamic) { + // This is a normal dynamic context provider like Diff or Terminal + if (!newItem.id) return; + + // Get the query from the input value + const segs = downshiftProps.inputValue.split("@"); + const query = segs[segs.length - 1]; + + // Tell server the context item was selected + client?.selectContextItem(newItem.id, query); + if (downshiftProps.inputValue.includes("@")) { + const selectedNestedContextProvider = contextProviders.find( + (provider) => provider.title === newItem.id + ); + if ( + !nestedContextProvider && + !selectedNestedContextProvider?.dynamic + ) { + downshiftProps.setInputValue(`@${newItem.id} `); + setNestedContextProvider(selectedNestedContextProvider); + } else { + downshiftProps.setInputValue(""); + } + } + + // Clear the input + downshiftProps.setInputValue(""); + setCurrentlyInContextQuery(false); + setNestedContextProvider(undefined); + setInQueryForContextProvider(undefined); + (event.nativeEvent as any).preventDownshiftDefault = true; + event.preventDefault(); return; } @@ -531,25 +573,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { downshiftProps.highlightedIndex, contextProviders, nestedContextProvider, + downshiftProps.inputValue, ] ); - const showSelectContextGroupDialog = () => { - dispatch(setDialogMessage(<SelectContextGroupDialog />)); - dispatch(setShowDialog(true)); - }; - - const showDialogToSaveContextGroup = () => { - dispatch( - setDialogMessage( - <AddContextGroupDialog - selectedContextItems={props.selectedContextItems} - /> - ) - ); - dispatch(setShowDialog(true)); - }; - const [isComposing, setIsComposing] = useState(false); return ( @@ -558,18 +585,36 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { className="px-2 flex gap-2 items-center flex-wrap mt-2" ref={contextItemsDivRef} > - {props.selectedContextItems.map((item, idx) => { + <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") { + client?.deleteContextWithIds( + selectedContextItems.map((item) => item.description.id) + ); + inputRef.current?.focus(); + } + }} + > + <TrashIcon width="1.4em" height="1.4em" /> + </HiddenHeaderButtonWithText> + {selectedContextItems.map((item, idx) => { return ( <PillButton - areMultipleItems={props.selectedContextItems.length > 1} + areMultipleItems={selectedContextItems.length > 1} key={`${item.description.id.item_id}${idx}`} item={item} - warning={ - item.content.length > 4000 && item.editing - ? "Editing such a large range may be slow" - : undefined + editing={ + item.editing && + (inputRef.current as any)?.value?.startsWith("/edit") } - addingHighlightedCode={props.addingHighlightedCode} + editingAny={(inputRef.current as any)?.value?.startsWith("/edit")} index={idx} onDelete={() => { client?.deleteContextWithIds([item.description.id]); @@ -578,64 +623,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { /> ); })} - <HeaderButtonWithText - text="Load bookmarked context" - onClick={() => { - showSelectContextGroupDialog(); - }} - className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid" - onKeyDown={(e: KeyboardEvent) => { - e.preventDefault(); - if (e.key === "Enter") { - showSelectContextGroupDialog(); - } - }} - > - <FolderArrowDownIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - {props.selectedContextItems.length > 0 && ( - <> - {props.addingHighlightedCode ? ( - <EmptyPillDiv - onClick={() => { - props.onToggleAddContext(); - }} - > - Highlight code section - </EmptyPillDiv> - ) : ( - <HeaderButtonWithText - text="Add more code to context" - onClick={() => { - props.onToggleAddContext(); - }} - className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid" - onKeyDown={(e: KeyboardEvent) => { - e.preventDefault(); - if (e.key === "Enter") { - props.onToggleAddContext(); - } - }} - > - <DocumentPlusIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - )} - <HeaderButtonWithText - text="Bookmark context" - onClick={() => { - showDialogToSaveContextGroup(); - }} - className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid" - onKeyDown={(e: KeyboardEvent) => { - e.preventDefault(); - if (e.key === "Enter") { - showDialogToSaveContextGroup(); - } - }} - > - <BookmarkIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - </> + + {selectedContextItems.length > 0 && ( + <HeaderButtonWithText + onClick={() => { + client?.showContextVirtualFile(); + }} + text="View Current Context" + > + <MagnifyingGlassIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> )} </div> <div @@ -648,7 +645,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { typeof inQueryForContextProvider !== "undefined" } disabled={props.disabled} - placeholder={`Ask a question, type '/' for slash commands, or '@' to add context`} + placeholder={`Ask a question, '/' for slash commands, '@' to add context`} {...getInputProps({ onCompositionStart: () => setIsComposing(true), onCompositionEnd: () => setIsComposing(false), @@ -701,13 +698,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { } } setCurrentlyInContextQuery(false); + } else if (event.key === "Enter" && currentlyInContextQuery) { + // Handle "Enter" for Context Providers + selectContextItemFromDropdown(event); } else if ( - event.key === "Enter" && - currentlyInContextQuery && - nestedContextProvider === undefined + event.key === "Tab" && + downshiftProps.isOpen && + items.length > 0 && + items[downshiftProps.highlightedIndex]?.name.startsWith("/") ) { - selectContextItemFromDropdown(event); - } else if (event.key === "Tab" && items.length > 0) { downshiftProps.setInputValue(items[0].name); event.preventDefault(); } else if (event.key === "Tab") { @@ -789,25 +788,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ref: inputRef, })} /> - {inQueryForContextProvider ? ( + {inQueryForContextProvider && ( <DynamicQueryTitleDiv> Enter {inQueryForContextProvider.display_title} Query </DynamicQueryTitleDiv> - ) : ( - <> - <StyledPlusIcon - width="1.4em" - height="1.4em" - data-tooltip-id="add-context-button" - onClick={() => { - downshiftProps.setInputValue("@"); - inputRef.current?.focus(); - }} - /> - <StyledTooltip id="add-context-button" place="bottom"> - Add Context to Prompt - </StyledTooltip> - </> )} <Ul @@ -816,13 +800,17 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { })} showAbove={showAbove()} ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} - hidden={!downshiftProps.isOpen || items.length === 0} + hidden={ + !downshiftProps.isOpen || + items.length === 0 || + inputRef.current?.value === "" + } > {nestedContextProvider && ( <div style={{ backgroundColor: secondaryDark, - borderBottom: `1px solid ${lightGray}`, + borderBottom: `0.5px solid ${lightGray}`, display: "flex", gap: "4px", position: "sticky", @@ -846,27 +834,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { items.map((item, index) => ( <Li style={{ - borderTop: index === 0 ? "none" : undefined, + borderTop: index === 0 ? "none" : `0.5px solid ${lightGray}`, }} key={`${item.name}${index}`} {...downshiftProps.getItemProps({ item, index })} highlighted={downshiftProps.highlightedIndex === index} selected={downshiftProps.selectedItem === item} onClick={(e) => { - // e.stopPropagation(); - // e.preventDefault(); - // (e.nativeEvent as any).preventDownshiftDefault = true; - // downshiftProps.selectItem(item); selectContextItemFromDropdown(e); - onSelectedItemChangeCallback({ selectedItem: item }); + e.stopPropagation(); + e.preventDefault(); + inputRef.current?.focus(); }} > - <span> + <span className="flex justify-between w-full"> {item.name} {" "} <span style={{ color: lightGray, + float: "right", + textAlign: "right", }} > {item.description} @@ -888,7 +876,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ))} </Ul> </div> - {props.selectedContextItems.length === 0 && + {selectedContextItems.length === 0 && (downshiftProps.inputValue?.startsWith("/edit") || (focused && metaKeyPressed && @@ -897,6 +885,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { Inserting at cursor </div> )} + <ContinueButton + disabled={!(inputRef.current as any)?.value} + onClick={() => props.onEnter(undefined)} + /> </> ); }); diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx index 10ecd94a..95dde177 100644 --- a/extension/react-app/src/components/ContinueButton.tsx +++ b/extension/react-app/src/components/ContinueButton.tsx @@ -1,26 +1,42 @@ -import styled, { keyframes } from "styled-components"; +import styled from "styled-components"; import { Button } from "."; import { PlayIcon } from "@heroicons/react/24/outline"; import { useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { useEffect, useState } from "react"; -let StyledButton = styled(Button)<{ color?: string | null }>` +const StyledButton = styled(Button)<{ + color?: string | null; + isDisabled: boolean; +}>` margin: auto; margin-top: 8px; margin-bottom: 16px; display: grid; grid-template-columns: 22px 1fr; align-items: center; - background: ${(props) => props.color || "#be1b55"}; + background-color: ${(props) => props.color || "#be1b55"}; - &:hover { - transition-property: "background"; - opacity: 0.7; + opacity: ${(props) => (props.isDisabled ? 0.5 : 1.0)}; + + cursor: ${(props) => (props.isDisabled ? "default" : "pointer")}; + + &:hover:enabled { + background-color: ${(props) => props.color || "#be1b55"}; + ${(props) => + props.isDisabled + ? "cursor: default;" + : ` + opacity: 0.7; + `} } `; -function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) { +function ContinueButton(props: { + onClick?: () => void; + hidden?: boolean; + disabled: boolean; +}) { const vscMediaUrl = useSelector( (state: RootStore) => state.config.vscMediaUrl ); @@ -49,7 +65,8 @@ function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) { hidden={props.hidden} style={{ fontSize: "10px" }} className="m-auto press-start-2p" - onClick={props.onClick} + onClick={props.disabled ? undefined : props.onClick} + isDisabled={props.disabled} > {vscMediaUrl ? ( <img src={`${vscMediaUrl}/play_button.png`} width="16px" /> diff --git a/extension/react-app/src/components/ErrorStepContainer.tsx b/extension/react-app/src/components/ErrorStepContainer.tsx new file mode 100644 index 00000000..e8ab7950 --- /dev/null +++ b/extension/react-app/src/components/ErrorStepContainer.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import styled from "styled-components"; +import { HistoryNode } from "../../../schema/HistoryNode"; +import { defaultBorderRadius, vscBackground } from "."; +import HeaderButtonWithText from "./HeaderButtonWithText"; +import { + MinusCircleIcon, + MinusIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; + +const Div = styled.div` + padding: 8px; + background-color: #ff000011; + border-radius: ${defaultBorderRadius}; + border: 1px solid #cc0000; +`; + +interface ErrorStepContainerProps { + historyNode: HistoryNode; + onClose: () => void; + onDelete: () => void; +} + +function ErrorStepContainer(props: ErrorStepContainerProps) { + return ( + <div style={{ backgroundColor: vscBackground, position: "relative" }}> + <div + style={{ + position: "absolute", + right: "4px", + top: "4px", + display: "flex", + }} + > + <HeaderButtonWithText text="Collapse" onClick={() => props.onClose()}> + <MinusCircleIcon width="1.3em" height="1.3em" /> + </HeaderButtonWithText> + <HeaderButtonWithText text="Collapse" onClick={() => props.onDelete()}> + <XMarkIcon width="1.3em" height="1.3em" /> + </HeaderButtonWithText> + </div> + <Div> + <pre className="overflow-x-scroll"> + {props.historyNode.observation?.error as string} + </pre> + </Div> + </div> + ); +} + +export default ErrorStepContainer; diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx index 3122c287..ca359250 100644 --- a/extension/react-app/src/components/HeaderButtonWithText.tsx +++ b/extension/react-app/src/components/HeaderButtonWithText.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { HeaderButton, StyledTooltip } from "."; +import ReactDOM from "react-dom"; interface HeaderButtonWithTextProps { text: string; @@ -14,6 +15,9 @@ interface HeaderButtonWithTextProps { const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { const [hover, setHover] = useState(false); + + const tooltipPortalDiv = document.getElementById("tooltip-portal-div"); + return ( <> <HeaderButton @@ -34,9 +38,13 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { > {props.children} </HeaderButton> - <StyledTooltip id={`header_button_${props.text}`} place="bottom"> - {props.text} - </StyledTooltip> + {tooltipPortalDiv && + ReactDOM.createPortal( + <StyledTooltip id={`header_button_${props.text}`} place="bottom"> + {props.text} + </StyledTooltip>, + tooltipPortalDiv + )} </> ); }; diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx index 6410db8a..9ec2e671 100644 --- a/extension/react-app/src/components/Layout.tsx +++ b/extension/react-app/src/components/Layout.tsx @@ -1,7 +1,6 @@ import styled from "styled-components"; import { defaultBorderRadius, secondaryDark, vscForeground } from "."; import { Outlet } from "react-router-dom"; -import Onboarding from "./Onboarding"; import TextDialog from "./TextDialog"; import { useContext, useEffect, useState } from "react"; import { GUIClientContext } from "../App"; @@ -15,10 +14,9 @@ import { import { PlusIcon, FolderIcon, - BookOpenIcon, - ChatBubbleOvalLeftEllipsisIcon, SparklesIcon, Cog6ToothIcon, + QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; import HeaderButtonWithText from "./HeaderButtonWithText"; import { useNavigate, useLocation } from "react-router-dom"; @@ -62,6 +60,8 @@ const Footer = styled.footer` align-items: center; width: calc(100% - 16px); height: ${FOOTER_HEIGHT}; + + overflow: hidden; `; const GridDiv = styled.div` @@ -98,11 +98,20 @@ const Layout = () => { (state: RootStore) => state.uiState.displayBottomMessageOnBottom ); + const timeline = useSelector( + (state: RootStore) => state.serverState.history.timeline + ); + // #endregion useEffect(() => { const handleKeyDown = (event: any) => { - if (event.metaKey && event.altKey && event.code === "KeyN") { + if ( + event.metaKey && + event.altKey && + event.code === "KeyN" && + timeline.filter((n) => !n.step.hide).length > 0 + ) { client?.loadSession(undefined); } if ((event.metaKey || event.ctrlKey) && event.code === "KeyC") { @@ -121,7 +130,7 @@ const Layout = () => { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [client]); + }, [client, timeline]); return ( <LayoutTopDiv> @@ -133,7 +142,6 @@ const Layout = () => { gridTemplateRows: "1fr auto", }} > - <Onboarding /> <TextDialog showDialog={showDialog} onEnter={() => { @@ -176,54 +184,26 @@ const Layout = () => { color="yellow" /> )} - <ModelSelect /> - {defaultModel === "MaybeProxyOpenAI" && + {defaultModel === "OpenAIFreeTrial" && (location.pathname === "/settings" || - parseInt(localStorage.getItem("freeTrialCounter") || "0") >= - 125) && ( + parseInt(localStorage.getItem("ftc") || "0") >= 125) && ( <ProgressBar - completed={parseInt( - localStorage.getItem("freeTrialCounter") || "0" - )} + completed={parseInt(localStorage.getItem("ftc") || "0")} total={250} /> )} </div> <HeaderButtonWithText + text="Help" onClick={() => { - client?.loadSession(undefined); + navigate("/help"); }} - text="New Session (⌥⌘N)" > - <PlusIcon width="1.4em" height="1.4em" /> + <QuestionMarkCircleIcon width="1.4em" height="1.4em" /> </HeaderButtonWithText> <HeaderButtonWithText onClick={() => { - navigate("/history"); - }} - text="History" - > - <FolderIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - <a - href="https://continue.dev/docs/how-to-use-continue" - className="no-underline" - > - <HeaderButtonWithText text="Docs"> - <BookOpenIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - </a> - <a - href="https://github.com/continuedev/continue/issues/new/choose" - className="no-underline" - > - <HeaderButtonWithText text="Feedback"> - <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - </a> - <HeaderButtonWithText - onClick={() => { navigate("/settings"); }} text="Settings" @@ -248,6 +228,7 @@ const Layout = () => { {bottomMessage} </BottomMessageDiv> </div> + <div id="tooltip-portal-div" /> </LayoutTopDiv> ); }; diff --git a/extension/react-app/src/components/ModelCard.tsx b/extension/react-app/src/components/ModelCard.tsx new file mode 100644 index 00000000..a537c5f4 --- /dev/null +++ b/extension/react-app/src/components/ModelCard.tsx @@ -0,0 +1,122 @@ +import React, { useContext } from "react"; +import styled from "styled-components"; +import { buttonColor, defaultBorderRadius, lightGray, vscForeground } from "."; +import { setShowDialog } from "../redux/slices/uiStateSlice"; +import { GUIClientContext } from "../App"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { RootStore } from "../redux/store"; +import { BookOpenIcon } from "@heroicons/react/24/outline"; +import HeaderButtonWithText from "./HeaderButtonWithText"; +import ReactDOM from "react-dom"; + +export enum ModelTag { + "Requires API Key" = "Requires API Key", + "Local" = "Local", + "Free" = "Free", + "Open-Source" = "Open-Source", +} + +const MODEL_TAG_COLORS: any = {}; +MODEL_TAG_COLORS[ModelTag["Requires API Key"]] = "#FF0000"; +MODEL_TAG_COLORS[ModelTag["Local"]] = "#00bb00"; +MODEL_TAG_COLORS[ModelTag["Open-Source"]] = "#0033FF"; +MODEL_TAG_COLORS[ModelTag["Free"]] = "#ffff00"; + +export interface ModelInfo { + title: string; + class: string; + args: any; + description: string; + icon?: string; + tags?: ModelTag[]; +} + +const Div = styled.div<{ color: string }>` + border: 1px solid ${lightGray}; + border-radius: ${defaultBorderRadius}; + cursor: pointer; + padding: 4px 8px; + position: relative; + width: 100%; + transition: all 0.5s; + + &:hover { + border: 1px solid ${(props) => props.color}; + background-color: ${(props) => props.color}22; + } +`; + +interface ModelCardProps { + modelInfo: ModelInfo; +} + +function ModelCard(props: ModelCardProps) { + const client = useContext(GUIClientContext); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const vscMediaUrl = useSelector( + (state: RootStore) => state.config.vscMediaUrl + ); + + return ( + <Div + color={buttonColor} + onClick={(e) => { + if ((e.target as any).closest("a")) { + return; + } + client?.addModelForRole( + "*", + props.modelInfo.class, + props.modelInfo.args + ); + dispatch(setShowDialog(false)); + navigate("/"); + }} + > + <div style={{ display: "flex", alignItems: "center" }}> + {vscMediaUrl && ( + <img + src={`${vscMediaUrl}/logos/${props.modelInfo.icon}`} + height="24px" + style={{ marginRight: "10px" }} + /> + )} + <h3>{props.modelInfo.title}</h3> + </div> + {props.modelInfo.tags?.map((tag) => { + return ( + <span + style={{ + backgroundColor: `${MODEL_TAG_COLORS[tag]}55`, + color: "white", + padding: "2px 4px", + borderRadius: defaultBorderRadius, + marginRight: "4px", + }} + > + {tag} + </span> + ); + })} + <p>{props.modelInfo.description}</p> + + <a + style={{ + position: "absolute", + right: "8px", + top: "8px", + }} + href={`https://continue.dev/docs/reference/Models/${props.modelInfo.class.toLowerCase()}`} + target="_blank" + > + <HeaderButtonWithText text="Read the docs"> + <BookOpenIcon width="1.6em" height="1.6em" /> + </HeaderButtonWithText> + </a> + </Div> + ); +} + +export default ModelCard; diff --git a/extension/react-app/src/components/ModelSelect.tsx b/extension/react-app/src/components/ModelSelect.tsx index 0b1829f1..29d9250e 100644 --- a/extension/react-app/src/components/ModelSelect.tsx +++ b/extension/react-app/src/components/ModelSelect.tsx @@ -10,8 +10,9 @@ import { useContext } from "react"; import { GUIClientContext } from "../App"; import { RootStore } from "../redux/store"; import { useDispatch, useSelector } from "react-redux"; -import { PlusIcon } from "@heroicons/react/24/outline"; +import { ArrowLeftIcon, PlusIcon } from "@heroicons/react/24/outline"; import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; +import { useNavigate } from "react-router-dom"; const MODEL_INFO: { title: string; class: string; args: any }[] = [ { @@ -83,7 +84,7 @@ const MODEL_INFO: { title: string; class: string; args: any }[] = [ }, { title: "GPT-4 limited free trial", - class: "MaybeProxyOpenAI", + class: "OpenAIFreeTrial", args: { model: "gpt-4", }, @@ -159,10 +160,12 @@ function ModelSelect(props: {}) { const defaultModel = useSelector( (state: RootStore) => (state.serverState.config as any)?.models?.default ); - const unusedModels = useSelector( - (state: RootStore) => (state.serverState.config as any)?.models?.unused + const savedModels = useSelector( + (state: RootStore) => (state.serverState.config as any)?.models?.saved ); + const navigate = useNavigate(); + return ( <GridDiv> <Select @@ -173,7 +176,7 @@ function ModelSelect(props: {}) { defaultValue={0} onChange={(e) => { const value = JSON.parse(e.target.value); - if (value.t === "unused") { + if (value.t === "saved") { client?.setModelForRoleFromIndex("*", value.idx); } }} @@ -188,11 +191,11 @@ function ModelSelect(props: {}) { {modelSelectTitle(defaultModel)} </option> )} - {unusedModels?.map((model: any, idx: number) => { + {savedModels?.map((model: any, idx: number) => { return ( <option value={JSON.stringify({ - t: "unused", + t: "saved", idx, })} > @@ -206,31 +209,7 @@ function ModelSelect(props: {}) { width="1.3em" height="1.3em" onClick={() => { - dispatch( - setDialogMessage( - <div> - <div className="text-lg font-bold p-2"> - Setup a new model provider - </div> - <br /> - {MODEL_INFO.map((model, idx) => { - return ( - <NewProviderDiv - onClick={() => { - const model = MODEL_INFO[idx]; - client?.addModelForRole("*", model.class, model.args); - dispatch(setShowDialog(false)); - }} - > - {model.title} - </NewProviderDiv> - ); - })} - <br /> - </div> - ) - ); - dispatch(setShowDialog(true)); + navigate("/models"); }} /> </GridDiv> diff --git a/extension/react-app/src/components/ModelSettings.tsx b/extension/react-app/src/components/ModelSettings.tsx index 99200502..06516687 100644 --- a/extension/react-app/src/components/ModelSettings.tsx +++ b/extension/react-app/src/components/ModelSettings.tsx @@ -27,7 +27,7 @@ const DefaultModelOptions: { api_key: "", model: "gpt-4", }, - MaybeProxyOpenAI: { + OpenAIFreeTrial: { api_key: "", model: "gpt-4", }, diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx deleted file mode 100644 index 588f7298..00000000 --- a/extension/react-app/src/components/Onboarding.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState, useEffect } from "react"; -import styled from "styled-components"; -import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; -import { defaultBorderRadius } from "."; -import Loader from "./Loader"; - -const StyledDiv = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #1e1e1e; - z-index: 200; - - color: white; -`; - -const StyledSpan = styled.span` - padding: 8px; - border-radius: ${defaultBorderRadius}; - &:hover { - background-color: #ffffff33; - } - white-space: nowrap; -`; - -const Onboarding = () => { - const [counter, setCounter] = useState(4); - const gifs = ["intro", "highlight", "question", "help"]; - const topMessages = [ - "Welcome!", - "Highlight code", - "Ask a question", - "Use /help to learn more", - ]; - - useEffect(() => { - const hasVisited = localStorage.getItem("hasVisited"); - if (hasVisited) { - setCounter(4); - } else { - setCounter(0); - localStorage.setItem("hasVisited", "true"); - } - }, []); - - const [loading, setLoading] = useState(true); - - useEffect(() => { - setLoading(true); - }, [counter]); - - return ( - <StyledDiv hidden={counter >= 4}> - <div - style={{ - display: "grid", - justifyContent: "center", - alignItems: "center", - height: "100%", - textAlign: "center", - paddingLeft: "16px", - paddingRight: "16px", - }} - > - <h1>{topMessages[counter]}</h1> - <div style={{ display: "flex", justifyContent: "center" }}> - {loading && ( - <div style={{ margin: "auto", position: "absolute", zIndex: 0 }}> - <Loader /> - </div> - )} - {counter < 4 && - (counter % 2 === 0 ? ( - <img - src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`} - width="100%" - key={"even-gif"} - alt={topMessages[counter]} - onLoad={() => { - setLoading(false); - }} - style={{ zIndex: 1 }} - /> - ) : ( - <img - src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`} - width="100%" - key={"odd-gif"} - alt={topMessages[counter]} - onLoad={() => { - setLoading(false); - }} - style={{ zIndex: 1 }} - /> - ))} - </div> - <p - style={{ - paddingLeft: "50px", - paddingRight: "50px", - paddingBottom: "50px", - textAlign: "center", - cursor: "pointer", - whiteSpace: "nowrap", - }} - > - <StyledSpan - hidden={counter === 0} - onClick={() => setCounter((prev) => Math.max(prev - 1, 0))} - > - <ArrowLeftIcon width="18px" strokeWidth="2px" /> Previous - </StyledSpan> - <span hidden={counter === 0}>{" | "}</span> - <StyledSpan onClick={() => setCounter((prev) => prev + 1)}> - {counter === 0 - ? "Click to learn how to use Continue" - : counter === 3 - ? "Get Started" - : "Next"}{" "} - <ArrowRightIcon width="18px" strokeWidth="2px" /> - </StyledSpan> - </p> - </div> - </StyledDiv> - ); -}; - -export default Onboarding; diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index 1ffdeeed..4b602619 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import styled from "styled-components"; import { StyledTooltip, @@ -15,13 +15,8 @@ import { } from "@heroicons/react/24/outline"; import { GUIClientContext } from "../App"; import { useDispatch } from "react-redux"; -import { - setBottomMessage, - setBottomMessageCloseTimeout, -} from "../redux/slices/uiStateSlice"; +import { setBottomMessage } from "../redux/slices/uiStateSlice"; import { ContextItem } from "../../../schema/FullState"; -import { ReactMarkdown } from "react-markdown/lib/react-markdown"; -import StyledMarkdownPreview from "./StyledMarkdownPreview"; const Button = styled.button` border: none; @@ -80,33 +75,27 @@ const CircleDiv = styled.div` interface PillButtonProps { onHover?: (arg0: boolean) => void; item: ContextItem; - warning?: string; + editing: boolean; + editingAny: boolean; index: number; - addingHighlightedCode?: boolean; areMultipleItems?: boolean; onDelete?: () => void; } interface StyledButtonProps { - warning: string; + borderColor?: string; editing?: boolean; - areMultipleItems?: boolean; } const StyledButton = styled(Button)<StyledButtonProps>` position: relative; - border-color: ${(props) => - props.warning - ? "red" - : props.editing && props.areMultipleItems - ? vscForeground - : "transparent"}; + border-color: ${(props) => props.borderColor || "transparent"}; border-width: 1px; border-style: solid; &:focus { outline: none; - border-color: ${vscForeground}; + border-color: ${lightGray}; border-width: 1px; border-style: solid; } @@ -116,82 +105,56 @@ const PillButton = (props: PillButtonProps) => { const [isHovered, setIsHovered] = useState(false); const client = useContext(GUIClientContext); - const dispatch = useDispatch(); + const [warning, setWarning] = useState<string | undefined>(undefined); useEffect(() => { - if (isHovered) { - dispatch(setBottomMessageCloseTimeout(undefined)); - dispatch( - setBottomMessage( - <> - <b>{props.item.description.name}</b>:{" "} - {props.item.description.description} - <pre> - <code - style={{ - fontSize: "12px", - backgroundColor: "transparent", - color: vscForeground, - whiteSpace: "pre-wrap", - wordWrap: "break-word", - }} - > - {props.item.content} - </code> - </pre> - </> - ) - ); + if (props.editing && props.item.content.length > 4000) { + setWarning("Editing such a large range may be slow"); } else { - dispatch( - setBottomMessageCloseTimeout( - setTimeout(() => { - if (!isHovered) { - dispatch(setBottomMessage(undefined)); - } - }, 2000) - ) - ); + setWarning(undefined); } - }, [isHovered]); + }, [props.editing, props.item]); + + const dispatch = useDispatch(); return ( - <> - <div style={{ position: "relative" }}> - <StyledButton - areMultipleItems={props.areMultipleItems} - warning={props.warning || ""} - editing={props.item.editing} - onMouseEnter={() => { - setIsHovered(true); - if (props.onHover) { - props.onHover(true); - } - }} - 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 - ? "1fr 1fr" - : "1fr", - backgroundColor: vscBackground, - }} - > - {props.item.editable && props.areMultipleItems && ( + <div style={{ position: "relative" }}> + <StyledButton + borderColor={props.editing ? (warning ? "red" : undefined) : undefined} + onMouseEnter={() => { + setIsHovered(true); + if (props.onHover) { + props.onHover(true); + } + }} + 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"} @@ -205,30 +168,31 @@ const PillButton = (props: PillButtonProps) => { </ButtonDiv> )} - <StyledTooltip id={`pin-${props.index}`}> - Edit this range - </StyledTooltip> - <ButtonDiv - data-tooltip-id={`delete-${props.index}`} - backgroundColor={"#cc000055"} - onClick={() => { - client?.deleteContextWithIds([props.item.description.id]); - dispatch(setBottomMessage(undefined)); - }} - > - <TrashIcon style={{ margin: "auto" }} width="1.6em" /> - </ButtonDiv> - </GridDiv> - )} - {props.item.description.name} - </StyledButton> - <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.warning && ( + <StyledTooltip id={`pin-${props.index}`}> + Edit this range + </StyledTooltip> + <ButtonDiv + data-tooltip-id={`delete-${props.index}`} + backgroundColor={"#cc000055"} + onClick={() => { + client?.deleteContextWithIds([props.item.description.id]); + dispatch(setBottomMessage(undefined)); + }} + > + <TrashIcon style={{ margin: "auto" }} width="1.6em" /> + </ButtonDiv> + </GridDiv> + )} + {props.item.description.name} + </StyledButton> + <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}`} @@ -240,12 +204,32 @@ const PillButton = (props: PillButtonProps) => { /> </CircleDiv> <StyledTooltip id={`circle-div-${props.item.description.name}`}> - {props.warning} + {warning} </StyledTooltip> </> - )} - </div> - </> + ) : ( + <> + <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> + </> + ))} + </div> ); }; diff --git a/extension/react-app/src/components/ProgressBar.tsx b/extension/react-app/src/components/ProgressBar.tsx index 4efee776..27972ffc 100644 --- a/extension/react-app/src/components/ProgressBar.tsx +++ b/extension/react-app/src/components/ProgressBar.tsx @@ -28,9 +28,12 @@ const GridDiv = styled.div` const P = styled.p` margin: 0; margin-top: 2px; - font-size: 12px; + font-size: 11.5px; color: ${lightGray}; text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; interface ProgressBarProps { @@ -45,7 +48,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => { <> <a href="https://continue.dev/docs/customization/models" - className="no-underline" + className="no-underline ml-2" > <GridDiv data-tooltip-id="usage_progress_bar"> <ProgressBarWrapper> @@ -61,7 +64,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => { /> </ProgressBarWrapper> <P> - Free Usage: {completed} / {total} + Free Uses: {completed} / {total} </P> </GridDiv> </a> diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index a05aefb0..61529227 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -1,18 +1,9 @@ -import { useContext, useEffect, useRef, useState } from "react"; -import styled, { keyframes } from "styled-components"; -import { secondaryDark, vscBackground } from "."; -import { - ChevronDownIcon, - ChevronRightIcon, - ArrowPathIcon, - XMarkIcon, - MagnifyingGlassIcon, - StopCircleIcon, -} from "@heroicons/react/24/outline"; +import { useEffect, useRef, useState } from "react"; +import styled from "styled-components"; +import { defaultBorderRadius, secondaryDark, vscBackground } from "."; +import { ArrowPathIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { HistoryNode } from "../../../schema/HistoryNode"; import HeaderButtonWithText from "./HeaderButtonWithText"; -import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util"; -import { GUIClientContext } from "../App"; import StyledMarkdownPreview from "./StyledMarkdownPreview"; interface StepContainerProps { @@ -23,11 +14,10 @@ interface StepContainerProps { onRetry: () => void; onDelete: () => void; open: boolean; - onToggleAll: () => void; - onToggle: () => void; isFirst: boolean; isLast: boolean; index: number; + noUserInputParent: boolean; } // #region styled components @@ -35,74 +25,30 @@ interface StepContainerProps { const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean; -}>` - opacity: ${(props) => (props.inFuture ? 0.3 : 1)}; - overflow: hidden; - margin-left: 0px; - margin-right: 0px; -`; +}>``; -const HeaderDiv = styled.div<{ error: boolean; loading: boolean }>` - background-color: ${(props) => (props.error ? "#522" : vscBackground)}; - display: grid; - grid-template-columns: 1fr auto auto; +const ButtonsDiv = styled.div` + display: flex; + gap: 2px; align-items: center; - padding-right: 8px; -`; + background-color: ${vscBackground}; + box-shadow: 1px 1px 10px ${vscBackground}; + border-radius: ${defaultBorderRadius}; -const LeftHeaderSubDiv = styled.div` - margin: 8px; - display: grid; - grid-template-columns: auto 1fr; - align-items: center; - grid-gap: 2px; + position: absolute; + right: 0; + top: 0; + height: 0; `; const ContentDiv = styled.div<{ isUserInput: boolean }>` - padding-left: 4px; - padding-right: 2px; + padding: 2px; + padding-right: 0px; background-color: ${(props) => props.isUserInput ? secondaryDark : vscBackground}; font-size: 13px; -`; - -const gradient = keyframes` - 0% { - background-position: 0px 0; - } - 100% { - background-position: 100em 0; - } -`; - -const GradientBorder = styled.div<{ - borderWidth?: number; - borderRadius?: string; - borderColor?: string; - isFirst: boolean; - isLast: boolean; - loading: boolean; -}>` - border-radius: ${(props) => props.borderRadius || "0"}; - padding-top: ${(props) => - `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`}; - padding-bottom: ${(props) => - `${(props.borderWidth || 1) / (props.isLast ? 1 : 2)}px`}; - 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%; + border-radius: ${defaultBorderRadius}; + overflow: hidden; `; // #endregion @@ -112,7 +58,6 @@ function StepContainer(props: StepContainerProps) { const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null); const userInputRef = useRef<HTMLInputElement>(null); const isUserInput = props.historyNode.step.name === "UserInputStep"; - const client = useContext(GUIClientContext); useEffect(() => { if (userInputRef?.current) { @@ -139,91 +84,11 @@ function StepContainer(props: StepContainerProps) { hidden={props.historyNode.step.hide as any} > <div> - <GradientBorder - loading={props.historyNode.active as boolean} - isFirst={props.isFirst} - isLast={props.isLast} - borderColor={ - props.historyNode.observation?.error - ? "#f005" - : props.historyNode.active - ? undefined - : "transparent" - } - className="overflow-hidden cursor-pointer" - onClick={(e) => { - if (isMetaEquivalentKeyPressed(e)) { - props.onToggleAll(); - } else { - props.onToggle(); - } - }} - > - <HeaderDiv - loading={(props.historyNode.active as boolean) || false} - error={props.historyNode.observation?.error ? true : false} - > - <LeftHeaderSubDiv - style={ - props.historyNode.observation?.error ? { color: "white" } : {} - } - > - {!isUserInput && - (props.open ? ( - <ChevronDownIcon width="1.4em" height="1.4em" /> - ) : ( - <ChevronRightIcon width="1.4em" height="1.4em" /> - ))} - {props.historyNode.observation?.title || - (props.historyNode.step.name as any)} - </LeftHeaderSubDiv> - {/* <HeaderButton - onClick={(e) => { - e.stopPropagation(); - props.onReverse(); - }} - > - <Backward size="1.6em" onClick={props.onReverse}></Backward> - </HeaderButton> */} - {(isHovered || (props.historyNode.active as boolean)) && ( - <div className="flex gap-2 items-center"> - {(props.historyNode.logs as any)?.length > 0 && ( - <HeaderButtonWithText - text="Logs" - onClick={(e) => { - e.stopPropagation(); - client?.showLogsAtIndex(props.index); - }} - > - <MagnifyingGlassIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> - )} - <HeaderButtonWithText - onClick={(e) => { - e.stopPropagation(); - props.onDelete(); - }} - text={ - props.historyNode.active - ? `Stop (${getMetaKeyLabel()}⌫)` - : "Delete" - } - > - {props.historyNode.active ? ( - <StopCircleIcon - width="1.4em" - height="1.4em" - onClick={props.onDelete} - /> - ) : ( - <XMarkIcon - width="1.4em" - height="1.4em" - onClick={props.onDelete} - /> - )} - </HeaderButtonWithText> - {props.historyNode.observation?.error ? ( + {isHovered && + (props.historyNode.observation?.error || props.noUserInputParent) && ( + <ButtonsDiv> + {props.historyNode.observation?.error && + (( <HeaderButtonWithText text="Retry" onClick={(e) => { @@ -237,39 +102,33 @@ function StepContainer(props: StepContainerProps) { onClick={props.onRetry} /> </HeaderButtonWithText> - ) : ( - <></> - )} - </div> - )} - </HeaderDiv> - </GradientBorder> - <ContentDiv hidden={!props.open} isUserInput={isUserInput}> - {props.open && false && ( - <> - <pre className="overflow-x-scroll"> - Step Details: - <br /> - {JSON.stringify(props.historyNode.step, null, 2)} - </pre> - </> - )} + ) as any)} - {props.historyNode.observation?.error ? ( - <details> - <summary>View Traceback</summary> - <pre className="overflow-x-scroll"> - {props.historyNode.observation.error as string} - </pre> - </details> - ) : ( - <StyledMarkdownPreview - source={props.historyNode.step.description || ""} - wrapperElement={{ - "data-color-mode": "dark", - }} - /> + {props.noUserInputParent && ( + <HeaderButtonWithText + text="Delete" + onClick={(e) => { + e.stopPropagation(); + props.onDelete(); + }} + > + <XMarkIcon + width="1.4em" + height="1.4em" + onClick={props.onRetry} + /> + </HeaderButtonWithText> + )} + </ButtonsDiv> )} + + <ContentDiv hidden={!props.open} isUserInput={isUserInput}> + <StyledMarkdownPreview + source={props.historyNode.step.description || ""} + wrapperElement={{ + "data-color-mode": "dark", + }} + /> </ContentDiv> </div> </MainDiv> diff --git a/extension/react-app/src/components/Suggestions.tsx b/extension/react-app/src/components/Suggestions.tsx new file mode 100644 index 00000000..1709288c --- /dev/null +++ b/extension/react-app/src/components/Suggestions.tsx @@ -0,0 +1,228 @@ +import React, { useCallback, useEffect, useState } from "react"; +import styled from "styled-components"; +import { + StyledTooltip, + defaultBorderRadius, + lightGray, + secondaryDark, + vscForeground, +} from "."; +import { + PaperAirplaneIcon, + SparklesIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { useSelector } from "react-redux"; +import { RootStore } from "../redux/store"; +import HeaderButtonWithText from "./HeaderButtonWithText"; + +const Div = styled.div<{ isDisabled: boolean }>` + border-radius: ${defaultBorderRadius}; + cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")}; + padding: 8px 8px; + background-color: ${secondaryDark}; + border: 1px solid transparent; + + display: flex; + justify-content: space-between; + align-items: center; + + color: ${(props) => (props.isDisabled ? lightGray : vscForeground)}; + + &:hover { + border: ${(props) => + props.isDisabled ? "1px solid transparent" : `1px solid ${lightGray}`}; + } +`; + +const P = styled.p` + font-size: 13px; + margin: 0; +`; + +interface SuggestionsDivProps { + title: string; + description: string; + textInput: string; + onClick?: () => void; + disabled: boolean; +} + +function SuggestionsDiv(props: SuggestionsDivProps) { + const [isHovered, setIsHovered] = useState(false); + + return ( + <> + <Div + data-tooltip-id={`suggestion-disabled-${props.textInput.replace( + " ", + "" + )}`} + onClick={props.onClick} + onMouseEnter={() => { + if (props.disabled) return; + setIsHovered(true); + }} + onMouseLeave={() => setIsHovered(false)} + isDisabled={props.disabled} + > + <P>{props.description}</P> + <PaperAirplaneIcon + width="1.6em" + height="1.6em" + style={{ + opacity: isHovered ? 1 : 0, + backgroundColor: secondaryDark, + boxShadow: `1px 1px 10px ${secondaryDark}`, + borderRadius: defaultBorderRadius, + }} + /> + </Div> + <StyledTooltip + id={`suggestion-disabled-${props.textInput.replace(" ", "")}`} + place="bottom" + hidden={!props.disabled} + > + Must highlight code first + </StyledTooltip> + </> + ); +} + +const stageDescriptions = [ + <p>Ask a question</p>, + <ol> + <li>Highlight code in the editor</li> + <li>Press cmd+M to select the code</li> + <li>Ask a question</li> + </ol>, + <ol> + <li>Highlight code in the editor</li> + <li>Press cmd+shift+M to select the code</li> + <li>Request and edit</li> + </ol>, +]; + +const suggestionsStages: any[][] = [ + [ + { + title: stageDescriptions[0], + description: "How does merge sort work?", + textInput: "How does merge sort work?", + }, + { + title: stageDescriptions[0], + description: "How do I sum over a column in SQL?", + textInput: "How do I sum over a column in SQL?", + }, + ], + [ + { + title: stageDescriptions[1], + description: "Is there any way to make this code more efficient?", + textInput: "Is there any way to make this code more efficient?", + }, + { + title: stageDescriptions[1], + description: "What does this function do?", + textInput: "What does this function do?", + }, + ], + [ + { + title: stageDescriptions[2], + description: "/edit write comments for this code", + textInput: "/edit write comments for this code", + }, + { + title: stageDescriptions[2], + description: "/edit make this code more efficient", + textInput: "/edit make this code more efficient", + }, + ], +]; + +const TutorialDiv = styled.div` + margin: 4px; + position: relative; + background-color: #ff02; + border-radius: ${defaultBorderRadius}; + padding: 8px 4px; +`; + +function SuggestionsArea(props: { onClick: (textInput: string) => void }) { + const [stage, setStage] = useState( + parseInt(localStorage.getItem("stage") || "0") + ); + const timeline = useSelector( + (state: RootStore) => state.serverState.history.timeline + ); + const sessionId = useSelector( + (state: RootStore) => state.serverState.session_info?.session_id + ); + const codeIsHighlighted = useSelector((state: RootStore) => + state.serverState.selected_context_items.some( + (item) => item.description.id.provider_title === "code" + ) + ); + + const [hide, setHide] = useState(false); + + useEffect(() => { + setHide(false); + }, [sessionId]); + + const [numTutorialInputs, setNumTutorialInputs] = useState(0); + + const inputsAreOnlyTutorial = useCallback(() => { + const inputs = timeline.filter( + (node) => !node.step.hide && node.step.name === "User Input" + ); + return inputs.length - numTutorialInputs === 0; + }, [timeline, numTutorialInputs]); + + return ( + <> + {hide || stage > 2 || !inputsAreOnlyTutorial() || ( + <TutorialDiv> + <div className="flex"> + <SparklesIcon width="1.3em" height="1.3em" color="yellow" /> + <b className="ml-1">Tutorial</b> + </div> + <p style={{ color: lightGray }}> + {stage < suggestionsStages.length && + suggestionsStages[stage][0]?.title} + </p> + <HeaderButtonWithText + className="absolute right-1 top-1 cursor-pointer" + text="Close Tutorial" + onClick={() => { + console.log("HIDE"); + setHide(true); + }} + > + <XMarkIcon width="1.2em" height="1.2em" /> + </HeaderButtonWithText> + <div className="grid grid-cols-2 gap-2 mt-2"> + {suggestionsStages[stage]?.map((suggestion) => ( + <SuggestionsDiv + disabled={stage > 0 && !codeIsHighlighted} + {...suggestion} + onClick={() => { + if (stage > 0 && !codeIsHighlighted) return; + props.onClick(suggestion.textInput); + setStage(stage + 1); + localStorage.setItem("stage", (stage + 1).toString()); + setHide(true); + setNumTutorialInputs((prev) => prev + 1); + }} + /> + ))} + </div> + </TutorialDiv> + )} + </> + ); +} + +export default SuggestionsArea; diff --git a/extension/react-app/src/components/TimelineItem.tsx b/extension/react-app/src/components/TimelineItem.tsx new file mode 100644 index 00000000..78568890 --- /dev/null +++ b/extension/react-app/src/components/TimelineItem.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { lightGray, secondaryDark, vscBackground } from "."; +import styled from "styled-components"; +import { ChatBubbleOvalLeftIcon, PlusIcon } from "@heroicons/react/24/outline"; + +const CollapseButton = styled.div` + background-color: ${vscBackground}; + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + flex-grow: 0; + margin-left: 5px; + cursor: pointer; +`; + +const CollapsedDiv = styled.div` + margin-top: 8px; + margin-bottom: 8px; + margin-left: 8px; + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + min-height: 16px; +`; + +interface TimelineItemProps { + historyNode: any; + open: boolean; + onToggle: () => void; + children: any; + iconElement?: any; +} + +function TimelineItem(props: TimelineItemProps) { + return props.open ? ( + props.children + ) : ( + <CollapsedDiv> + <CollapseButton + onClick={() => { + props.onToggle(); + }} + > + {props.iconElement || ( + <ChatBubbleOvalLeftIcon width="16px" height="16px" /> + )} + </CollapseButton> + <span style={{ color: lightGray }}> + {props.historyNode.observation?.error + ? props.historyNode.observation?.title + : props.historyNode.step.name} + </span> + </CollapsedDiv> + ); +} + +export default TimelineItem; diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx index 866fef58..76a3c615 100644 --- a/extension/react-app/src/components/UserInputContainer.tsx +++ b/extension/react-app/src/components/UserInputContainer.tsx @@ -1,5 +1,11 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; -import styled from "styled-components"; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import styled, { keyframes } from "styled-components"; import { defaultBorderRadius, lightGray, @@ -8,69 +14,115 @@ import { vscForeground, } from "."; import HeaderButtonWithText from "./HeaderButtonWithText"; -import { XMarkIcon, CheckIcon } from "@heroicons/react/24/outline"; +import { + XMarkIcon, + CheckIcon, + ChevronDownIcon, + ChevronRightIcon, + MagnifyingGlassIcon, + StopCircleIcon, +} from "@heroicons/react/24/outline"; import { HistoryNode } from "../../../schema/HistoryNode"; import { GUIClientContext } from "../App"; +import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util"; +import { RootStore } from "../redux/store"; +import { useSelector } from "react-redux"; interface UserInputContainerProps { onDelete: () => void; children: string; historyNode: HistoryNode; index: number; + onToggle: (arg0: boolean) => void; + onToggleAll: (arg0: boolean) => void; + isToggleOpen: boolean; + active: boolean; + groupIndices: number[]; } -const StyledDiv = styled.div` - position: relative; - background-color: ${secondaryDark}; - font-size: 13px; +const gradient = keyframes` + 0% { + background-position: 0px 0; + } + 100% { + background-position: 100em 0; + } +`; + +const ToggleDiv = styled.div` display: flex; align-items: center; - border-bottom: 1px solid ${vscBackground}; - padding: 8px; - padding-top: 0px; - padding-bottom: 0px; + justify-content: center; + cursor: pointer; - border-bottom: 0.5px solid ${lightGray}; - border-top: 0.5px solid ${lightGray}; -`; + height: 100%; + padding: 0 4px; -const DeleteButtonDiv = styled.div` - position: absolute; - top: 8px; - right: 8px; + &:hover { + background-color: ${vscBackground}; + } `; -const StyledPre = styled.pre` - margin-right: 22px; - margin-left: 8px; - white-space: pre-wrap; - word-wrap: break-word; - font-family: "Lexend", sans-serif; - font-size: 13px; +const GradientBorder = styled.div<{ + borderWidth?: number; + borderRadius?: string; + borderColor?: string; + isFirst: boolean; + isLast: boolean; + loading: boolean; +}>` + border-radius: ${(props) => props.borderRadius || "0"}; + padding: ${(props) => + `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`}; + 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%; `; -const TextArea = styled.textarea` - margin: 8px; - margin-right: 22px; - padding: 8px; - white-space: pre-wrap; - word-wrap: break-word; - font-family: "Lexend", sans-serif; +const StyledDiv = styled.div<{ editing: boolean }>` font-size: 13px; - width: 100%; + font-family: inherit; border-radius: ${defaultBorderRadius}; - height: 100%; - border: none; - background-color: ${vscBackground}; - resize: none; - outline: none; - border: none; + height: auto; + background-color: ${secondaryDark}; color: ${vscForeground}; + align-items: center; + position: relative; + z-index: 1; + overflow: hidden; + display: grid; + grid-template-columns: auto 1fr; - &:focus { - border: none; - outline: none; - } + outline: ${(props) => (props.editing ? `1px solid ${lightGray}` : "none")}; + cursor: text; +`; + +const DeleteButtonDiv = styled.div` + position: absolute; + top: 8px; + right: 8px; + background-color: ${secondaryDark}; + box-shadow: 2px 2px 10px ${secondaryDark}; + border-radius: ${defaultBorderRadius}; +`; + +const GridDiv = styled.div` + display: grid; + grid-template-columns: auto 1fr; + grid-gap: 8px; + align-items: center; `; function stringWithEllipsis(str: string, maxLen: number) { @@ -84,108 +136,194 @@ const UserInputContainer = (props: UserInputContainerProps) => { const [isHovered, setIsHovered] = useState(false); const [isEditing, setIsEditing] = useState(false); - const textAreaRef = useRef<HTMLTextAreaElement>(null); + const divRef = useRef<HTMLDivElement>(null); const client = useContext(GUIClientContext); + const [prevContent, setPrevContent] = useState(""); + + const history = useSelector((state: RootStore) => state.serverState.history); + useEffect(() => { - if (isEditing && textAreaRef.current) { - textAreaRef.current.focus(); - // Select all text - textAreaRef.current.setSelectionRange( - 0, - textAreaRef.current.value.length - ); - // Change the size to match the contents (up to a max) - textAreaRef.current.style.height = "auto"; - textAreaRef.current.style.height = - (textAreaRef.current.scrollHeight > 500 - ? 500 - : textAreaRef.current.scrollHeight) + "px"; + if (isEditing && divRef.current) { + setPrevContent(divRef.current.innerText); + divRef.current.focus(); + + if (divRef.current.innerText !== "") { + const range = document.createRange(); + const sel = window.getSelection(); + range.setStart(divRef.current, 0); + range.setEnd(divRef.current, 1); + sel?.removeAllRanges(); + sel?.addRange(range); + } } - }, [isEditing]); + }, [isEditing, divRef.current]); + + const onBlur = useCallback(() => { + setIsEditing(false); + if (divRef.current) { + divRef.current.innerText = prevContent; + divRef.current.blur(); + } + }, [divRef.current]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { - setIsEditing(false); + onBlur(); } }; - document.addEventListener("keydown", handleKeyDown); + divRef.current?.addEventListener("keydown", handleKeyDown); return () => { - document.removeEventListener("keydown", handleKeyDown); + divRef.current?.removeEventListener("keydown", handleKeyDown); }; - }, []); + }, [prevContent, divRef.current, isEditing, onBlur]); const doneEditing = (e: any) => { - if (!textAreaRef.current?.value) { + if (!divRef.current?.innerText) { return; } - client?.editStepAtIndex(textAreaRef.current.value, props.index); + setPrevContent(divRef.current.innerText); + client?.editStepAtIndex(divRef.current.innerText, props.index); setIsEditing(false); e.stopPropagation(); + divRef.current?.blur(); }; return ( - <StyledDiv - onMouseEnter={() => { - setIsHovered(true); - }} - onMouseLeave={() => { - setIsHovered(false); - }} + <GradientBorder + loading={props.active} + isFirst={false} + isLast={false} + borderColor={props.active ? undefined : vscBackground} + borderRadius={defaultBorderRadius} > - {isEditing ? ( - <TextArea - ref={textAreaRef} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - doneEditing(e); + <StyledDiv + editing={isEditing} + onMouseEnter={() => { + setIsHovered(true); + }} + onMouseLeave={() => { + setIsHovered(false); + }} + onClick={() => { + setIsEditing(true); + }} + > + <GridDiv> + <ToggleDiv + onClick={ + props.isToggleOpen + ? (e) => { + e.stopPropagation(); + if (isMetaEquivalentKeyPressed(e)) { + props.onToggleAll(false); + } else { + props.onToggle(false); + } + } + : (e) => { + e.stopPropagation(); + if (isMetaEquivalentKeyPressed(e)) { + props.onToggleAll(true); + } else { + props.onToggle(true); + } + } } - }} - defaultValue={props.children} - onBlur={() => { - setIsEditing(false); - }} - /> - ) : ( - <StyledPre - onClick={() => { - setIsEditing(true); - }} - className="mr-6 cursor-text w-full" - > - {stringWithEllipsis(props.children, 600)} - </StyledPre> - )} - {/* <ReactMarkdown children={props.children} className="w-fit mr-10" /> */} - <DeleteButtonDiv> - {(isHovered || isEditing) && ( - <div className="flex"> - {isEditing ? ( - <HeaderButtonWithText - onClick={(e) => { - doneEditing(e); - }} - text="Done" - > - <CheckIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> + > + {props.isToggleOpen ? ( + <ChevronDownIcon width="1.4em" height="1.4em" /> ) : ( - <HeaderButtonWithText - onClick={(e) => { - props.onDelete(); - e.stopPropagation(); - }} - text="Delete" - > - <XMarkIcon width="1.4em" height="1.4em" /> - </HeaderButtonWithText> + <ChevronRightIcon width="1.4em" height="1.4em" /> )} + </ToggleDiv> + <div + style={{ + padding: "8px", + paddingTop: "4px", + paddingBottom: "4px", + }} + > + <div + ref={divRef} + onBlur={() => { + onBlur(); + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + doneEditing(e); + } + }} + contentEditable={true} + suppressContentEditableWarning={true} + className="mr-6 ml-1 cursor-text w-full py-2 flex items-center content-center outline-none" + > + {isEditing + ? props.children + : stringWithEllipsis(props.children, 600)} + </div> + <DeleteButtonDiv> + {(isHovered || isEditing) && ( + <div className="flex"> + {isEditing ? ( + <HeaderButtonWithText + onClick={(e) => { + doneEditing(e); + }} + text="Done" + > + <CheckIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> + ) : ( + <> + {history.timeline + .filter( + (h, i: number) => + props.groupIndices.includes(i) && h.logs + ) + .some((h) => h.logs!.length > 0) && ( + <HeaderButtonWithText + onClick={(e) => { + e.stopPropagation(); + client?.showLogsAtIndex(props.groupIndices[1]); + }} + text="Context Used" + > + <MagnifyingGlassIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> + )} + <HeaderButtonWithText + onClick={(e) => { + e.stopPropagation(); + if (props.active) { + 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> </div> - )} - </DeleteButtonDiv> - </StyledDiv> + </GridDiv> + </StyledDiv> + </GradientBorder> ); }; export default UserInputContainer; diff --git a/extension/react-app/src/components/dialogs/FTCDialog.tsx b/extension/react-app/src/components/dialogs/FTCDialog.tsx new file mode 100644 index 00000000..3ea753bc --- /dev/null +++ b/extension/react-app/src/components/dialogs/FTCDialog.tsx @@ -0,0 +1,72 @@ +import React, { useContext } from "react"; +import styled from "styled-components"; +import { Button, TextInput } from ".."; +import { useNavigate } from "react-router-dom"; +import { GUIClientContext } from "../../App"; +import { useDispatch } from "react-redux"; +import { setShowDialog } from "../../redux/slices/uiStateSlice"; + +const GridDiv = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 8px; + align-items: center; +`; + +function FTCDialog() { + const navigate = useNavigate(); + const [apiKey, setApiKey] = React.useState(""); + const client = useContext(GUIClientContext); + const dispatch = useDispatch(); + + return ( + <div className="p-4"> + <h3>Free Trial Limit Reached</h3> + <p> + You've reached the free trial limit of 250 free inputs with Continue's + OpenAI API key. To keep using Continue, you can either use your own API + key, or use a local LLM. To read more about the options, see our{" "} + <a + href="https://continue.dev/docs/customization/models" + target="_blank" + > + documentation + </a> + . If you're just looking for fastest way to keep going, type '/config' + to open your Continue config file and paste your API key into the + OpenAIFreeTrial object. + </p> + + <TextInput + type="text" + placeholder="Enter your OpenAI API key" + value={apiKey} + onChange={(e) => setApiKey(e.target.value)} + /> + <GridDiv> + <Button + onClick={() => { + navigate("/models"); + }} + > + Select model + </Button> + <Button + disabled={!apiKey} + onClick={() => { + client?.addModelForRole("*", "OpenAI", { + model: "gpt-4", + api_key: apiKey, + title: "GPT-4", + }); + dispatch(setShowDialog(false)); + }} + > + Use my API key + </Button> + </GridDiv> + </div> + ); +} + +export default FTCDialog; diff --git a/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx new file mode 100644 index 00000000..2a7b735c --- /dev/null +++ b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx @@ -0,0 +1,129 @@ +import React from "react"; +import styled from "styled-components"; +import { + defaultBorderRadius, + lightGray, + secondaryDark, + vscForeground, +} from ".."; +import { getPlatform } from "../../util"; + +const GridDiv = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 2rem; + padding: 1rem; + justify-items: center; + align-items: center; + + border-top: 0.5px solid ${lightGray}; +`; + +const KeyDiv = styled.div` + border: 0.5px solid ${lightGray}; + border-radius: ${defaultBorderRadius}; + padding: 4px; + color: ${vscForeground}; + + width: 16px; + height: 16px; + + display: flex; + justify-content: center; + align-items: center; +`; + +interface KeyboardShortcutProps { + mac: string; + windows: string; + description: string; +} + +function KeyboardShortcut(props: KeyboardShortcutProps) { + const shortcut = getPlatform() === "windows" ? props.windows : props.mac; + return ( + <div className="flex justify-between w-full items-center"> + <span + style={{ + color: vscForeground, + }} + > + {props.description} + </span> + <div className="flex gap-2 float-right"> + {shortcut.split(" ").map((key) => { + return <KeyDiv>{key}</KeyDiv>; + })} + </div> + </div> + ); +} + +const shortcuts: KeyboardShortcutProps[] = [ + { + mac: "⌘ M", + windows: "⌃ M", + description: "Ask about Highlighted Code", + }, + { + mac: "⌘ ⇧ M", + windows: "⌃ ⇧ M", + description: "Edit Highlighted Code", + }, + { + mac: "⌘ ⇧ ↵", + windows: "⌃ ⇧ ↵", + description: "Accept Diff", + }, + { + mac: "⌘ ⇧ ⌫", + windows: "⌃ ⇧ ⌫", + description: "Reject Diff", + }, + { + mac: "⌘ ⇧ L", + windows: "⌃ ⇧ L", + description: "Quick Text Entry", + }, + { + mac: "⌥ ⌘ M", + windows: "⌥ ⌃ M", + description: "Toggle Auxiliary Bar", + }, + { + mac: "⌘ ⇧ R", + windows: "⌃ ⇧ R", + description: "Debug Terminal", + }, + { + mac: "⌥ ⌘ N", + windows: "⌥ ⌃ N", + description: "New Session", + }, + { + mac: "⌘ ⌫", + windows: "⌃ ⌫", + description: "Stop Active Step", + }, +]; + +function KeyboardShortcutsDialog() { + return ( + <div className="p-2"> + <h3 className="my-3 mx-auto text-center">Keyboard Shortcuts</h3> + <GridDiv> + {shortcuts.map((shortcut) => { + return ( + <KeyboardShortcut + mac={shortcut.mac} + windows={shortcut.windows} + description={shortcut.description} + /> + ); + })} + </GridDiv> + </div> + ); +} + +export default KeyboardShortcutsDialog; diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts index 1f418c94..6f5a2f37 100644 --- a/extension/react-app/src/components/index.ts +++ b/extension/react-app/src/components/index.ts @@ -7,7 +7,7 @@ export const lightGray = "#646464"; // export const vscBackground = "rgb(30 30 30)"; export const vscBackgroundTransparent = "#1e1e1ede"; export const buttonColor = "#1bbe84"; -export const buttonColorHover = "1bbe84a8"; +export const buttonColorHover = "#1bbe84a8"; export const secondaryDark = "var(--vscode-list-hoverBackground)"; export const vscBackground = "var(--vscode-editor-background)"; @@ -17,7 +17,6 @@ export const Button = styled.button` padding: 10px 12px; margin: 8px 0; border-radius: ${defaultBorderRadius}; - cursor: pointer; border: none; color: white; @@ -28,7 +27,7 @@ export const Button = styled.button` } &:hover:enabled { - background-color: ${buttonColorHover}; + cursor: pointer; } `; @@ -56,6 +55,8 @@ export const TextArea = styled.textarea` z-index: 1; border: 1px solid transparent; + resize: vertical; + &:focus { outline: 1px solid ${lightGray}; border: 1px solid transparent; diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts index 9944f221..d71186d7 100644 --- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts @@ -29,6 +29,8 @@ abstract class AbstractContinueGUIClientProtocol { abstract showLogsAtIndex(index: number): void; + abstract showContextVirtualFile(): void; + abstract selectContextItem(id: string, query: string): void; abstract loadSession(session_id?: string): void; @@ -52,6 +54,8 @@ abstract class AbstractContinueGUIClientProtocol { abstract selectContextGroup(id: string): void; abstract deleteContextGroup(id: string): void; + + abstract setCurrentSessionTitle(title: string): void; } export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index fe1b654b..8205a629 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -23,12 +23,8 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { ? new VscodeMessenger(serverUrlWithSessionId) : new WebsocketMessenger(serverUrlWithSessionId); - this.messenger.onClose(() => { - console.log("GUI -> IDE websocket closed"); - }); - this.messenger.onError((error) => { - console.log("GUI -> IDE websocket error", error); - }); + this.messenger.onClose(() => {}); + this.messenger.onError((error) => {}); this.messenger.onMessageType("reconnect_at_session", (data: any) => { if (data.session_id) { @@ -52,6 +48,7 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { } onReconnectAtSession(session_id: string): void { + console.log("Reconnecting at session: ", session_id); this.connectMessenger( `${this.serverUrlWithSessionId.split("?")[0]}?session_id=${session_id}`, this.useVscodeMessagePassing @@ -122,6 +119,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { this.messenger?.send("show_logs_at_index", { index }); } + showContextVirtualFile(): void { + this.messenger?.send("show_context_virtual_file", {}); + } + selectContextItem(id: string, query: string): void { this.messenger?.send("select_context_item", { id, query }); } @@ -163,6 +164,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { deleteContextGroup(id: string): void { this.messenger?.send("delete_context_group", { id }); } + + setCurrentSessionTitle(title: string): void { + this.messenger?.send("set_current_session_title", { title }); + } } export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css index 269da69a..3ecef025 100644 --- a/extension/react-app/src/index.css +++ b/extension/react-app/src/index.css @@ -11,7 +11,7 @@ --vscode-editor-background: rgb(30, 30, 30); --vscode-editor-foreground: rgb(197, 200, 198); - --vscode-textBlockQuote-background: rgba(255, 255, 255, 0.05); + --vscode-textBlockQuote-background: rgba(255, 255, 255, 1); } html, @@ -33,3 +33,7 @@ body { .press-start-2p { font-family: "Press Start 2P", "Lexend", sans-serif; } + +a:focus { + outline: none; +}
\ No newline at end of file diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx index 9f58c505..78b7a970 100644 --- a/extension/react-app/src/pages/gui.tsx +++ b/extension/react-app/src/pages/gui.tsx @@ -1,7 +1,5 @@ import styled from "styled-components"; -import { defaultBorderRadius } from "../components"; -import Loader from "../components/Loader"; -import ContinueButton from "../components/ContinueButton"; +import { TextInput, defaultBorderRadius, lightGray } from "../components"; import { FullState } from "../../../schema/FullState"; import { useEffect, @@ -9,6 +7,7 @@ import { useState, useContext, useLayoutEffect, + useCallback, } from "react"; import { HistoryNode } from "../../../schema/HistoryNode"; import StepContainer from "../components/StepContainer"; @@ -32,6 +31,19 @@ import { setServerState, temporarilyPushToUserInputQueue, } from "../redux/slices/serverStateReducer"; +import TimelineItem from "../components/TimelineItem"; +import ErrorStepContainer from "../components/ErrorStepContainer"; +import { + ChatBubbleOvalLeftIcon, + CodeBracketSquareIcon, + ExclamationTriangleIcon, + FolderIcon, + PlusIcon, +} from "@heroicons/react/24/outline"; +import FTCDialog from "../components/dialogs/FTCDialog"; +import HeaderButtonWithText from "../components/HeaderButtonWithText"; +import { useNavigate } from "react-router-dom"; +import SuggestionsArea from "../components/Suggestions"; const TopGuiDiv = styled.div` overflow-y: scroll; @@ -44,6 +56,44 @@ const TopGuiDiv = styled.div` } `; +const TitleTextInput = styled(TextInput)` + border: none; + outline: none; + + font-size: 16px; + font-weight: bold; + margin: 0; + margin-right: 8px; + padding-top: 6px; + padding-bottom: 6px; + + &:focus { + outline: 1px solid ${lightGray}; + } +`; + +const StepsDiv = styled.div` + position: relative; + background-color: transparent; + padding-left: 8px; + padding-right: 8px; + + & > * { + z-index: 1; + position: relative; + } + + &::before { + content: ""; + position: absolute; + height: calc(100% - 24px); + border-left: 2px solid ${lightGray}; + left: 28px; + z-index: 0; + bottom: 24px; + } +`; + const UserInputQueueItem = styled.div` border-radius: ${defaultBorderRadius}; color: gray; @@ -52,6 +102,16 @@ const UserInputQueueItem = styled.div` text-align: center; `; +const GUIHeaderDiv = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px; + padding-left: 8px; + padding-right: 8px; + border-bottom: 0.5px solid ${lightGray}; +`; + interface GUIProps { firstObservation?: any; } @@ -61,6 +121,7 @@ function GUI(props: GUIProps) { const client = useContext(GUIClientContext); const posthog = usePostHog(); const dispatch = useDispatch(); + const navigate = useNavigate(); // #endregion @@ -73,26 +134,16 @@ function GUI(props: GUIProps) { const user_input_queue = useSelector( (state: RootStore) => state.serverState.user_input_queue ); - const adding_highlighted_code = useSelector( - (state: RootStore) => state.serverState.adding_highlighted_code - ); - const selected_context_items = useSelector( - (state: RootStore) => state.serverState.selected_context_items + + const sessionTitle = useSelector( + (state: RootStore) => state.serverState.session_info?.title ); // #endregion // #region State const [waitingForSteps, setWaitingForSteps] = useState(false); - const [availableSlashCommands, setAvailableSlashCommands] = useState< - { name: string; description: string }[] - >([]); - const [stepsOpen, setStepsOpen] = useState<boolean[]>([ - true, - true, - true, - true, - ]); + const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]); const [waitingForClient, setWaitingForClient] = useState(true); const [showLoading, setShowLoading] = useState(false); @@ -150,7 +201,7 @@ function GUI(props: GUIProps) { topGuiDivRef.current?.scrollTo({ top: topGuiDivRef.current?.scrollHeight, - behavior: "smooth" as any, + behavior: "instant" as any, }); }, [topGuiDivRef.current?.scrollHeight, history.timeline]); @@ -160,6 +211,7 @@ function GUI(props: GUIProps) { if ( e.key === "Backspace" && isMetaEquivalentKeyPressed(e) && + !e.shiftKey && typeof history?.current_index !== "undefined" && history.timeline[history.current_index]?.active ) { @@ -188,14 +240,6 @@ function GUI(props: GUIProps) { dispatch(setServerState(state)); setWaitingForSteps(waitingForSteps); - setAvailableSlashCommands( - state.slash_commands.map((c: any) => { - return { - name: `/${c.name}`, - description: c.description, - }; - }) - ); setStepsOpen((prev) => { const nextStepsOpen = [...prev]; for ( @@ -203,7 +247,7 @@ function GUI(props: GUIProps) { i < state.history.timeline.length; i++ ) { - nextStepsOpen.push(true); + nextStepsOpen.push(undefined); } return nextStepsOpen; }); @@ -214,7 +258,6 @@ function GUI(props: GUIProps) { useEffect(() => { if (client && waitingForClient) { - console.log("sending user input queue, ", user_input_queue); setWaitingForClient(false); for (const input of user_input_queue) { client.sendMainInput(input); @@ -244,43 +287,22 @@ function GUI(props: GUIProps) { return; } - // Increment localstorage counter for usage of free trial if ( - defaultModel === "MaybeProxyOpenAI" && + defaultModel === "OpenAIFreeTrial" && (!input.startsWith("/") || input.startsWith("/edit")) ) { - const freeTrialCounter = localStorage.getItem("freeTrialCounter"); - if (freeTrialCounter) { - const usages = parseInt(freeTrialCounter); - localStorage.setItem("freeTrialCounter", (usages + 1).toString()); + const ftc = localStorage.getItem("ftc"); + if (ftc) { + const u = parseInt(ftc); + localStorage.setItem("ftc", (u + 1).toString()); - if (usages >= 250) { - console.log("Free trial limit reached"); + if (u >= 250) { dispatch(setShowDialog(true)); - dispatch( - setDialogMessage( - <div className="p-4"> - <h3>Free Trial Limit Reached</h3> - You've reached the free trial limit of 250 free inputs with - Continue's OpenAI API key. To keep using Continue, you can - either use your own API key, or use a local LLM. To read more - about the options, see our{" "} - <a - href="https://continue.dev/docs/customization/models" - target="_blank" - > - documentation - </a> - . If you're just looking for fastest way to keep going, type - '/config' to open your Continue config file and paste your API - key into the MaybeProxyOpenAI object. - </div> - ) - ); + dispatch(setDialogMessage(<FTCDialog />)); return; } } else { - localStorage.setItem("freeTrialCounter", "1"); + localStorage.setItem("ftc", "1"); } } @@ -391,6 +413,69 @@ function GUI(props: GUIProps) { client.sendStepUserInput(input, index); }; + const getStepsInUserInputGroup = useCallback( + (index: number): number[] => { + // index is the index in the entire timeline, hidden steps included + const stepsInUserInputGroup: number[] = []; + + // First find the closest above UserInputStep + let userInputIndex = -1; + for (let i = index; i >= 0; i--) { + if ( + history?.timeline.length > i && + history.timeline[i].step.name === "User Input" && + history.timeline[i].step.hide === false + ) { + stepsInUserInputGroup.push(i); + userInputIndex = i; + break; + } + } + if (stepsInUserInputGroup.length === 0) return []; + + for (let i = userInputIndex + 1; i < history?.timeline.length; i++) { + if ( + history?.timeline.length > i && + history.timeline[i].step.name === "User Input" && + history.timeline[i].step.hide === false + ) { + break; + } + stepsInUserInputGroup.push(i); + } + return stepsInUserInputGroup; + }, + [history.timeline] + ); + + const onToggleAtIndex = useCallback( + (index: number) => { + // Check if all steps after the User Input are closed + const groupIndices = getStepsInUserInputGroup(index); + const userInputIndex = groupIndices[0]; + setStepsOpen((prev) => { + const nextStepsOpen = [...prev]; + nextStepsOpen[index] = !nextStepsOpen[index]; + const allStepsAfterUserInputAreClosed = !groupIndices.some( + (i, j) => j > 0 && nextStepsOpen[i] + ); + if (allStepsAfterUserInputAreClosed) { + nextStepsOpen[userInputIndex] = false; + } else { + const allStepsAfterUserInputAreOpen = !groupIndices.some( + (i, j) => j > 0 && !nextStepsOpen[i] + ); + if (allStepsAfterUserInputAreOpen) { + nextStepsOpen[userInputIndex] = true; + } + } + + return nextStepsOpen; + }); + }, + [getStepsInUserInputGroup] + ); + useEffect(() => { const timeout = setTimeout(() => { setShowLoading(true); @@ -400,6 +485,17 @@ function GUI(props: GUIProps) { clearTimeout(timeout); }; }, []); + + useEffect(() => { + if (sessionTitle) { + setSessionTitleInput(sessionTitle); + } + }, [sessionTitle]); + + const [sessionTitleInput, setSessionTitleInput] = useState<string>( + sessionTitle || "New Session" + ); + return ( <TopGuiDiv ref={topGuiDivRef} @@ -409,6 +505,51 @@ function GUI(props: GUIProps) { } }} > + <GUIHeaderDiv> + <TitleTextInput + onClick={(e) => { + // Select all text + (e.target as any).setSelectionRange( + 0, + (e.target as any).value.length + ); + }} + value={sessionTitleInput} + onChange={(e) => setSessionTitleInput(e.target.value)} + onBlur={(e) => { + client?.setCurrentSessionTitle(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + (e.target as any).blur(); + } + }} + /> + <div className="flex"> + {history.timeline.filter((n) => !n.step.hide).length > 0 && ( + <HeaderButtonWithText + onClick={() => { + if (history.timeline.filter((n) => !n.step.hide).length > 0) { + client?.loadSession(undefined); + } + }} + text="New Session (⌥⌘N)" + > + <PlusIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> + )} + + <HeaderButtonWithText + onClick={() => { + navigate("/history"); + }} + text="History" + > + <FolderIcon width="1.4em" height="1.4em" /> + </HeaderButtonWithText> + </div> + </GUIHeaderDiv> {showLoading && typeof client === "undefined" && ( <> <RingLoader /> @@ -478,63 +619,128 @@ function GUI(props: GUIProps) { </u> </p> </div> - - <div className="w-3/4 m-auto text-center text-xs"> - {/* Tip: Drag the Continue logo from the far left of the window to the - right, then toggle Continue using option/alt+command+m. */} - {/* Tip: If there is an error in the terminal, use COMMAND+D to - automatically debug */} - </div> </> )} - {history?.timeline.map((node: HistoryNode, index: number) => { - return node.step.name === "User Input" ? ( - node.step.hide || ( - <UserInputContainer - index={index} - onDelete={() => { - client?.deleteAtIndex(index); - }} - historyNode={node} - > - {node.step.description as string} - </UserInputContainer> - ) - ) : ( - <StepContainer - index={index} - isLast={index === history.timeline.length - 1} - isFirst={index === 0} - open={stepsOpen[index]} - onToggle={() => { - const nextStepsOpen = [...stepsOpen]; - nextStepsOpen[index] = !nextStepsOpen[index]; - setStepsOpen(nextStepsOpen); - }} - onToggleAll={() => { - const shouldOpen = !stepsOpen[index]; - setStepsOpen((prev) => prev.map(() => shouldOpen)); - }} - key={index} - onUserInput={(input: string) => { - onStepUserInput(input, index); - }} - inFuture={index > history?.current_index} - historyNode={node} - onReverse={() => { - client?.reverseToIndex(index); - }} - onRetry={() => { - client?.retryAtIndex(index); - setWaitingForSteps(true); - }} - onDelete={() => { - client?.deleteAtIndex(index); - }} - /> - ); - })} - {waitingForSteps && <Loader />} + <br /> + <SuggestionsArea + onClick={(textInput) => { + client?.sendMainInput(textInput); + }} + /> + <StepsDiv> + {history?.timeline.map((node: HistoryNode, index: number) => { + if (node.step.hide) return null; + return ( + <> + {node.step.name === "User Input" ? ( + node.step.hide || ( + <UserInputContainer + active={getStepsInUserInputGroup(index).some((i) => { + return history.timeline[i].active; + })} + groupIndices={getStepsInUserInputGroup(index)} + onToggle={(isOpen: boolean) => { + // Collapse all steps in the section + setStepsOpen((prev) => { + const nextStepsOpen = [...prev]; + getStepsInUserInputGroup(index).forEach((i) => { + nextStepsOpen[i] = isOpen; + }); + return nextStepsOpen; + }); + }} + onToggleAll={(isOpen: boolean) => { + // Collapse _all_ steps + setStepsOpen((prev) => { + return prev.map((_) => isOpen); + }); + }} + isToggleOpen={ + typeof stepsOpen[index] === "undefined" + ? true + : stepsOpen[index]! + } + index={index} + onDelete={() => { + // Delete the input and all steps until the next user input + getStepsInUserInputGroup(index).forEach((i) => { + client?.deleteAtIndex(i); + }); + }} + historyNode={node} + > + {node.step.description as string} + </UserInputContainer> + ) + ) : ( + <TimelineItem + historyNode={node} + iconElement={ + node.step.class_name === "DefaultModelEditCodeStep" ? ( + <CodeBracketSquareIcon width="16px" height="16px" /> + ) : node.observation?.error ? ( + <ExclamationTriangleIcon + width="16px" + height="16px" + color="red" + /> + ) : ( + <ChatBubbleOvalLeftIcon width="16px" height="16px" /> + ) + } + open={ + typeof stepsOpen[index] === "undefined" + ? node.observation?.error + ? false + : true + : stepsOpen[index]! + } + onToggle={() => onToggleAtIndex(index)} + > + {node.observation?.error ? ( + <ErrorStepContainer + onClose={() => onToggleAtIndex(index)} + historyNode={node} + onDelete={() => client?.deleteAtIndex(index)} + /> + ) : ( + <StepContainer + index={index} + isLast={index === history.timeline.length - 1} + isFirst={index === 0} + open={ + typeof stepsOpen[index] === "undefined" + ? true + : stepsOpen[index]! + } + key={index} + onUserInput={(input: string) => { + onStepUserInput(input, index); + }} + inFuture={index > history?.current_index} + historyNode={node} + onReverse={() => { + client?.reverseToIndex(index); + }} + onRetry={() => { + client?.retryAtIndex(index); + setWaitingForSteps(true); + }} + onDelete={() => { + client?.deleteAtIndex(index); + }} + noUserInputParent={ + getStepsInUserInputGroup(index).length === 0 + } + /> + )} + </TimelineItem> + )} + {/* <div className="h-2"></div> */} + </> + ); + })} + </StepsDiv> <div> {user_input_queue?.map?.((input) => { @@ -547,18 +753,14 @@ function GUI(props: GUIProps) { ref={mainTextInputRef} onEnter={(e) => { onMainTextInput(e); - e.stopPropagation(); - e.preventDefault(); + e?.stopPropagation(); + e?.preventDefault(); }} onInputValueChange={() => {}} - items={availableSlashCommands} - selectedContextItems={selected_context_items} onToggleAddContext={() => { client?.toggleAddingHighlightedCode(); }} - addingHighlightedCode={adding_highlighted_code} /> - <ContinueButton onClick={onMainTextInput} /> </TopGuiDiv> ); } diff --git a/extension/react-app/src/pages/help.tsx b/extension/react-app/src/pages/help.tsx new file mode 100644 index 00000000..3e2e93d2 --- /dev/null +++ b/extension/react-app/src/pages/help.tsx @@ -0,0 +1,98 @@ +import { useNavigate } from "react-router-dom"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts"; +import { buttonColor, lightGray, vscBackground } from "../components"; +import styled from "styled-components"; + +const IconDiv = styled.div<{ backgroundColor?: string }>` + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + height: 100%; + padding: 0 4px; + + &:hover { + background-color: ${(props) => props.backgroundColor || lightGray}; + } +`; + +function HelpPage() { + const navigate = useNavigate(); + + return ( + <div className="overflow-scroll"> + <div + className="items-center flex m-0 p-0 sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Help Center</h3> + </div> + + <div className="grid grid-cols-2 grid-rows-2"> + <IconDiv backgroundColor="rgb(234, 51, 35)"> + <a href="https://youtu.be/3Ocrc-WX4iQ?si=eDLYtkc6CXQoHsEc"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-5.2 -4.5 60 60" + fill="white" + className="w-full h-full" + > + <path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"></path> + </svg> + </a> + </IconDiv> + <IconDiv backgroundColor={buttonColor}> + <a href="https://continue.dev/docs/how-to-use-continue"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-2.2 -2 28 28" + fill="white" + className="w-full h-full flex items-center justify-center" + > + <path d="M11.25 4.533A9.707 9.707 0 006 3a9.735 9.735 0 00-3.25.555.75.75 0 00-.5.707v14.25a.75.75 0 001 .707A8.237 8.237 0 016 18.75c1.995 0 3.823.707 5.25 1.886V4.533zM12.75 20.636A8.214 8.214 0 0118 18.75c.966 0 1.89.166 2.75.47a.75.75 0 001-.708V4.262a.75.75 0 00-.5-.707A9.735 9.735 0 0018 3a9.707 9.707 0 00-5.25 1.533v16.103z" /> + </svg> + </a> + </IconDiv> + <IconDiv backgroundColor="rgb(88, 98, 227)"> + <a href="https://discord.gg/vapESyrFmJ"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-5 -5.5 60 60" + fill="white" + className="w-full h-full" + > + <path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"></path> + </svg> + </a> + </IconDiv> + <IconDiv> + <a href="https://github.com/continuedev/continue/issues/new/choose"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="-1.2 -1.2 32 32" + fill="white" + className="w-full h-full" + > + <path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"></path> + </svg> + </a> + </IconDiv> + </div> + + <KeyboardShortcutsDialog></KeyboardShortcutsDialog> + </div> + ); +} + +export default HelpPage; diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx index b901dd55..b6de0520 100644 --- a/extension/react-app/src/pages/history.tsx +++ b/extension/react-app/src/pages/history.tsx @@ -1,13 +1,14 @@ import React, { useContext, useEffect, useState } from "react"; import { SessionInfo } from "../../../schema/SessionInfo"; import { GUIClientContext } from "../App"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { useNavigate } from "react-router-dom"; -import { secondaryDark, vscBackground } from "../components"; +import { lightGray, secondaryDark, vscBackground } from "../components"; import styled from "styled-components"; import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import CheckDiv from "../components/CheckDiv"; +import { temporarilyClearSession } from "../redux/slices/serverStateReducer"; const Tr = styled.tr` &:hover { @@ -41,6 +42,7 @@ function lastPartOfPath(path: string): string { function History() { const navigate = useNavigate(); + const dispatch = useDispatch(); const [sessions, setSessions] = useState<SessionInfo[]>([]); const client = useContext(GUIClientContext); const apiUrl = useSelector((state: RootStore) => state.config.apiUrl); @@ -67,78 +69,106 @@ function History() { fetchSessions(); }, [client]); - console.log(sessions.map((session) => session.date_created)); - return ( - <div className="w-full"> - <div className="items-center flex"> - <ArrowLeftIcon - width="1.4em" - height="1.4em" - onClick={() => navigate("/")} - className="inline-block ml-4 cursor-pointer" - /> - <h1 className="text-xl font-bold m-4 inline-block">History</h1> + <div className="overflow-y-scroll"> + <div className="sticky top-0" style={{ backgroundColor: vscBackground }}> + <div + className="items-center flex m-0 p-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">History</h3> + </div> + {workspacePaths && workspacePaths.length > 0 && ( + <CheckDiv + checked={filteringByWorkspace} + onClick={() => setFilteringByWorkspace((prev) => !prev)} + title={`Show only sessions from ${lastPartOfPath( + workspacePaths[workspacePaths.length - 1] + )}/`} + /> + )} </div> - {workspacePaths && workspacePaths.length > 0 && ( - <CheckDiv - checked={filteringByWorkspace} - onClick={() => setFilteringByWorkspace((prev) => !prev)} - title={`Show only sessions from ${lastPartOfPath( - workspacePaths[workspacePaths.length - 1] - )}/`} - /> + + {sessions.filter((session) => { + if ( + !filteringByWorkspace || + typeof workspacePaths === "undefined" || + typeof session.workspace_directory === "undefined" + ) { + return true; + } + return workspacePaths.includes(session.workspace_directory); + }).length === 0 && ( + <div className="text-center my-4"> + No past sessions found. To start a new session, either click the "+" + button or use the keyboard shortcut: <b>Option + Command + N</b> + </div> )} - <table className="w-full"> - <tbody> - {sessions - .filter((session) => { - if ( - !filteringByWorkspace || - typeof workspacePaths === "undefined" || - typeof session.workspace_directory === "undefined" - ) { - return true; - } - return workspacePaths.includes(session.workspace_directory); - }) - .sort( - (a, b) => - parseDate(b.date_created).getTime() - - parseDate(a.date_created).getTime() - ) - .map((session, index) => ( - <Tr key={index}> - <td> - <TdDiv - onClick={() => { - client?.loadSession(session.session_id); - navigate("/"); - }} - > - <div className="text-md">{session.title}</div> - <div className="text-gray-400"> - {parseDate(session.date_created).toLocaleString("en-US", { - weekday: "short", - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - })} - {" | "} - {lastPartOfPath(session.workspace_directory || "")}/ - </div> - </TdDiv> - </td> - </Tr> - ))} - </tbody> - </table> - <br /> - <i className="text-sm ml-4"> - All session data is saved in ~/.continue/sessions - </i> + + <div> + <table className="w-full"> + <tbody> + {sessions + .filter((session) => { + if ( + !filteringByWorkspace || + typeof workspacePaths === "undefined" || + typeof session.workspace_directory === "undefined" + ) { + return true; + } + return workspacePaths.includes(session.workspace_directory); + }) + .sort( + (a, b) => + parseDate(b.date_created).getTime() - + parseDate(a.date_created).getTime() + ) + .map((session, index) => ( + <Tr key={index}> + <td> + <TdDiv + onClick={() => { + client?.loadSession(session.session_id); + dispatch(temporarilyClearSession()); + navigate("/"); + }} + > + <div className="text-md">{session.title}</div> + <div className="text-gray-400"> + {parseDate(session.date_created).toLocaleString( + "en-US", + { + year: "2-digit", + month: "2-digit", + day: "2-digit", + hour: "numeric", + minute: "2-digit", + hour12: true, + } + )} + {" | "} + {lastPartOfPath(session.workspace_directory || "")}/ + </div> + </TdDiv> + </td> + </Tr> + ))} + </tbody> + </table> + <br /> + <i className="text-sm ml-4"> + All session data is saved in ~/.continue/sessions + </i> + </div> </div> ); } diff --git a/extension/react-app/src/pages/models.tsx b/extension/react-app/src/pages/models.tsx new file mode 100644 index 00000000..1a6f275b --- /dev/null +++ b/extension/react-app/src/pages/models.tsx @@ -0,0 +1,167 @@ +import React from "react"; +import ModelCard, { ModelInfo, ModelTag } from "../components/ModelCard"; +import styled from "styled-components"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { lightGray, vscBackground } from "../components"; +import { useNavigate } from "react-router-dom"; + +const MODEL_INFO: ModelInfo[] = [ + { + title: "OpenAI", + class: "OpenAI", + description: "Use gpt-4, gpt-3.5-turbo, or any other OpenAI model", + args: { + model: "gpt-4", + api_key: "", + title: "OpenAI", + }, + icon: "openai.svg", + tags: [ModelTag["Requires API Key"]], + }, + { + title: "Anthropic", + class: "AnthropicLLM", + description: + "Claude-2 is a highly capable model with a 100k context length", + args: { + model: "claude-2", + api_key: "<ANTHROPIC_API_KEY>", + title: "Anthropic", + }, + icon: "anthropic.png", + tags: [ModelTag["Requires API Key"]], + }, + { + title: "Ollama", + class: "Ollama", + description: + "One of the fastest ways to get started with local models on Mac", + args: { + model: "codellama", + title: "Ollama", + }, + icon: "ollama.png", + tags: [ModelTag["Local"], ModelTag["Open-Source"]], + }, + { + title: "TogetherAI", + class: "TogetherLLM", + description: + "Use the TogetherAI API for extremely fast streaming of open-source models", + args: { + model: "togethercomputer/CodeLlama-13b-Instruct", + api_key: "<TOGETHER_API_KEY>", + title: "TogetherAI", + }, + icon: "together.png", + tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]], + }, + { + title: "LM Studio", + class: "GGML", + description: + "One of the fastest ways to get started with local models on Mac or Windows", + args: { + server_url: "http://localhost:1234", + title: "LM Studio", + }, + icon: "lmstudio.png", + tags: [ModelTag["Local"], ModelTag["Open-Source"]], + }, + { + title: "Replicate", + class: "ReplicateLLM", + description: "Use the Replicate API to run open-source models", + args: { + model: + "replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781", + api_key: "<REPLICATE_API_KEY>", + title: "Replicate", + }, + icon: "replicate.png", + tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]], + }, + { + title: "llama.cpp", + class: "LlamaCpp", + description: "If you are running the llama.cpp server from source", + args: { + title: "llama.cpp", + }, + icon: "llamacpp.png", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "HuggingFace TGI", + class: "HuggingFaceTGI", + description: + "HuggingFace Text Generation Inference is an advanced, highly performant option for serving open-source models to multiple people", + args: { + title: "HuggingFace TGI", + }, + icon: "hf.png", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "Other OpenAI-compatible API", + class: "GGML", + description: + "If you are using any other OpenAI-compatible API, for example text-gen-webui, FastChat, LocalAI, or llama-cpp-python, you can simply enter your server URL", + args: { + server_url: "<SERVER_URL>", + }, + icon: "openai.svg", + tags: [ModelTag.Local, ModelTag["Open-Source"]], + }, + { + title: "GPT-4 limited free trial", + class: "OpenAIFreeTrial", + description: + "New users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key", + args: { + model: "gpt-4", + title: "GPT-4 Free Trial", + }, + icon: "openai.svg", + tags: [ModelTag.Free], + }, +]; + +const GridDiv = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: 2rem; + padding: 1rem; + justify-items: center; + align-items: center; +`; + +function Models() { + const navigate = useNavigate(); + return ( + <div className="overflow-y-scroll"> + <div + className="items-center flex m-0 p-0 sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={() => navigate("/")} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Add a new model</h3> + </div> + <GridDiv> + {MODEL_INFO.map((model) => ( + <ModelCard modelInfo={model} /> + ))} + </GridDiv> + </div> + ); +} + +export default Models; diff --git a/extension/react-app/src/pages/settings.tsx b/extension/react-app/src/pages/settings.tsx index 8b3d9c5b..4bd51163 100644 --- a/extension/react-app/src/pages/settings.tsx +++ b/extension/react-app/src/pages/settings.tsx @@ -1,15 +1,23 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { useContext } from "react"; import { GUIClientContext } from "../App"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootStore } from "../redux/store"; import { useNavigate } from "react-router-dom"; import { ContinueConfig } from "../../../schema/ContinueConfig"; -import { Button, TextArea, lightGray, secondaryDark } from "../components"; +import { + Button, + TextArea, + lightGray, + secondaryDark, + vscBackground, +} from "../components"; import styled from "styled-components"; -import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { ArrowLeftIcon, Squares2X2Icon } from "@heroicons/react/24/outline"; import Loader from "../components/Loader"; import InfoHover from "../components/InfoHover"; import { FormProvider, useForm } from "react-hook-form"; +import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice"; +import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts"; const Hr = styled.hr` border: 0.5px solid ${lightGray}; @@ -70,7 +78,7 @@ const Slider = styled.input.attrs({ type: "range" })` border: none; } `; -const ALL_MODEL_ROLES = ["default", "small", "medium", "large", "edit", "chat"]; +const ALL_MODEL_ROLES = ["default", "summarize", "edit", "chat"]; function Settings() { const formMethods = useForm<ContinueConfig>(); @@ -79,6 +87,7 @@ function Settings() { const navigate = useNavigate(); const client = useContext(GUIClientContext); const config = useSelector((state: RootStore) => state.serverState.config); + const dispatch = useDispatch(); const submitChanges = () => { if (!client) return; @@ -106,17 +115,23 @@ function Settings() { return ( <FormProvider {...formMethods}> - <div className="w-full"> + <div className="overflow-scroll"> + <div + className="items-center flex sticky top-0" + style={{ + borderBottom: `0.5px solid ${lightGray}`, + backgroundColor: vscBackground, + }} + > + <ArrowLeftIcon + width="1.2em" + height="1.2em" + onClick={submitAndLeave} + className="inline-block ml-4 cursor-pointer" + /> + <h3 className="text-lg font-bold m-2 inline-block">Settings</h3> + </div> <form onSubmit={formMethods.handleSubmit(onSubmit)}> - <div className="items-center flex"> - <ArrowLeftIcon - width="1.4em" - height="1.4em" - onClick={submitAndLeave} - className="inline-block ml-4 cursor-pointer" - /> - <h1 className="text-2xl font-bold m-4 inline-block">Settings</h1> - </div> {config ? ( <div className="p-2"> <h3 className="flex gap-1"> diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts index 904b0e76..3a2e455a 100644 --- a/extension/react-app/src/redux/slices/serverStateReducer.ts +++ b/extension/react-app/src/redux/slices/serverStateReducer.ts @@ -1,6 +1,74 @@ import { createSlice } from "@reduxjs/toolkit"; import { FullState } from "../../../../schema/FullState"; +const TEST_TIMELINE = [ + { + step: { + description: "Hi, please write bubble sort in python", + name: "User Input", + }, + }, + { + step: { + description: `\`\`\`python +def bubble_sort(arr): + n = len(arr) + for i in range(n): + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr +\`\`\``, + name: "Bubble Sort in Python", + }, + }, + { + step: { + description: "Now write it in Rust", + name: "User Input", + }, + }, + { + step: { + description: "Hello! This is a test...\n\n1, 2, 3, testing...", + name: "Testing", + }, + }, + { + step: { + description: `Sure, here's bubble sort written in rust: \n\`\`\`rust +fn bubble_sort<T: Ord>(values: &mut[T]) { + let len = values.len(); + for i in 0..len { + for j in 0..(len - i - 1) { + if values[j] > values[j + 1] { + values.swap(j, j + 1); + } + } + } +} +\`\`\`\nIs there anything else I can answer?`, + name: "Rust Bubble Sort", + }, + active: true, + }, +]; + +const TEST_SLASH_COMMANDS = [ + { + name: "edit", + description: "Edit the code", + }, + { + name: "cmd", + description: "Generate a command", + }, + { + name: "help", + description: "Get help using Continue", + }, +]; + const initialState: FullState = { history: { timeline: [], @@ -30,9 +98,21 @@ export const serverStateSlice = createSlice({ temporarilyPushToUserInputQueue: (state, action) => { state.user_input_queue = [...state.user_input_queue, action.payload]; }, + temporarilyClearSession: (state) => { + state.history.timeline = []; + state.selected_context_items = []; + state.session_info = { + title: "Loading session...", + session_id: "", + date_created: "", + }; + }, }, }); -export const { setServerState, temporarilyPushToUserInputQueue } = - serverStateSlice.actions; +export const { + setServerState, + temporarilyPushToUserInputQueue, + temporarilyClearSession, +} = serverStateSlice.actions; export default serverStateSlice.reducer; diff --git a/extension/schema/ContinueConfig.d.ts b/extension/schema/ContinueConfig.d.ts index 5341056f..92f6e047 100644 --- a/extension/schema/ContinueConfig.d.ts +++ b/extension/schema/ContinueConfig.d.ts @@ -9,6 +9,7 @@ export type ContinueConfig = ContinueConfig1; export type Name = string; export type Hide = boolean; export type Description = string; +export type ClassName = string; export type SystemMessage = string; export type Role = "assistant" | "user" | "system" | "function"; export type Content = string; @@ -18,36 +19,146 @@ export type Name2 = string; export type Arguments = string; export type ChatContext = ChatMessage[]; export type ManageOwnChatContext = boolean; +/** + * Steps that will be automatically run at the beginning of a new session + */ export type StepsOnStartup = Step[]; +/** + * Steps that are not allowed to be run, and will be skipped if attempted + */ export type DisallowedSteps = string[]; +/** + * If this field is set to True, we will collect anonymous telemetry as described in the documentation page on telemetry. If set to False, we will not collect any data. + */ export type AllowAnonymousTelemetry = boolean; +/** + * Configuration for the models used by Continue. Read more about how to configure models in the documentation. + */ export type Models = Models1; -export type RequiresApiKey = string; -export type RequiresUniqueId = boolean; -export type RequiresWriteLog = boolean; +/** + * A title that will identify this model in the model selection dropdown + */ +export type Title = string; +/** + * A system message that will always be followed by the LLM + */ export type SystemMessage1 = string; +/** + * The maximum context length of the LLM in tokens, as counted by count_tokens. + */ +export type ContextLength = number; +/** + * The unique ID of the user. + */ +export type UniqueId = string; +/** + * The name of the model to be used (e.g. gpt-4, codellama) + */ +export type Model = string; +/** + * Tokens that will stop the completion. + */ +export type StopTokens = string[]; +/** + * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts. + */ +export type Timeout = number; +/** + * Whether to verify SSL certificates for requests. + */ +export type VerifySsl = boolean; +/** + * Path to a custom CA bundle to use when making the HTTP request + */ +export type CaBundlePath = string; +/** + * The API key for the LLM provider. + */ +export type ApiKey = string; +export type Unused = 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. + */ export type Temperature = number; export type Name3 = string; export type Prompt = string; export type Description1 = string; +/** + * An array of custom commands that allow you to reuse prompts. Each has name, description, and prompt properties. When you enter /<name> in the text input, it will act as a shortcut to the prompt. + */ export type CustomCommands = CustomCommand[]; export type Name4 = string; export type Description2 = string; +/** + * An array of slash commands that let you map custom Steps to a shortcut. + */ export type SlashCommands = SlashCommand[]; +/** + * The step that will be run when a traceback is detected (when you use the shortcut cmd+shift+R) + */ +export type OnTraceback = Step; +/** + * A system message that will always be followed by the LLM + */ export type SystemMessage2 = string; -export type Title = string; -export type Name5 = string; +/** + * A Policy object that can be used to override the default behavior of Continue, for example in order to build custom agents that take multiple steps at a time. + */ +export type PolicyOverride = Policy; +/** + * The title of the ContextProvider. This is what must be typed in the input to trigger the ContextProvider. + */ +export type Title1 = string; +/** + * The ContinueSDK instance accessible by the ContextProvider + */ +export type Sdk = ContinueSDK1; +/** + * The display title of the ContextProvider shown in the dropdown menu + */ +export type DisplayTitle = string; +/** + * A description of the ContextProvider displayed in the dropdown menu + */ export type Description3 = string; +/** + * Indicates whether the ContextProvider is dynamic + */ +export type Dynamic = boolean; +/** + * Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query. + */ +export type RequiresQuery = boolean; +export type Name5 = string; +export type Description4 = string; export type ProviderTitle = string; export type ItemId = string; export type Content1 = string; export type Editing = boolean; export type Editable = boolean; +/** + * List of selected items in the ContextProvider + */ export type SelectedItems = ContextItem[]; +/** + * A list of ContextProvider objects that can be used to provide context to the LLM by typing '@'. Read more about ContextProviders in the documentation. + */ export type ContextProviders = ContextProvider[]; +/** + * An optional token to identify the user. + */ +export type UserToken = string; +/** + * The URL of the server where development data is sent. No data is sent unless a valid user token is provided. + */ +export type DataServerUrl = string; +/** + * If set to `True`, Continue will not generate summaries for each Step. This can be useful if you want to save on compute. + */ +export type DisableSummaries = boolean; /** - * A pydantic class for the continue config file. + * Continue can be deeply customized by editing the `ContinueConfig` object in `~/.continue/config.py` (`%userprofile%\.continue\config.py` for Windows) on your machine. This class is instantiated from the config file for every new session. */ export interface ContinueConfig1 { steps_on_startup?: StepsOnStartup; @@ -57,16 +168,20 @@ export interface ContinueConfig1 { temperature?: Temperature; custom_commands?: CustomCommands; slash_commands?: SlashCommands; - on_traceback?: Step; + on_traceback?: OnTraceback; system_message?: SystemMessage2; - policy_override?: Policy; + policy_override?: PolicyOverride; context_providers?: ContextProviders; + user_token?: UserToken; + data_server_url?: DataServerUrl; + disable_summaries?: DisableSummaries; [k: string]: unknown; } export interface Step { name?: Name; hide?: Hide; description?: Description; + class_name?: ClassName; system_message?: SystemMessage; chat_context?: ChatContext; manage_own_chat_context?: ManageOwnChatContext; @@ -95,14 +210,28 @@ export interface Models1 { large?: LLM; edit?: LLM; chat?: LLM; + unused?: Unused; sdk?: ContinueSDK; [k: string]: unknown; } export interface LLM { - requires_api_key?: RequiresApiKey; - requires_unique_id?: RequiresUniqueId; - requires_write_log?: RequiresWriteLog; + title?: Title; system_message?: SystemMessage1; + context_length?: ContextLength; + unique_id?: UniqueId; + model: Model; + stop_tokens?: StopTokens; + timeout?: Timeout; + verify_ssl?: VerifySsl; + ca_bundle_path?: CaBundlePath; + prompt_templates?: PromptTemplates; + api_key?: ApiKey; + [k: string]: unknown; +} +/** + * 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. + */ +export interface PromptTemplates { [k: string]: unknown; } export interface ContinueSDK { @@ -140,8 +269,12 @@ export interface Policy { * When you hit enter on an option, the context provider will add that item to the autopilot's list of context (which is all stored in the ContextManager object). */ export interface ContextProvider { - title: Title; - sdk?: ContinueSDK1; + title: Title1; + sdk?: Sdk; + display_title: DisplayTitle; + description: Description3; + dynamic: Dynamic; + requires_query?: RequiresQuery; selected_items?: SelectedItems; [k: string]: unknown; } @@ -168,7 +301,7 @@ export interface ContextItem { */ export interface ContextItemDescription { name: Name5; - description: Description3; + description: Description4; id: ContextItemId; [k: string]: unknown; } diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts index a847a608..5d5a5444 100644 --- a/extension/schema/FullState.d.ts +++ b/extension/schema/FullState.d.ts @@ -9,6 +9,7 @@ export type FullState = FullState1; export type Name = string; export type Hide = boolean; export type Description = string; +export type ClassName = string; export type SystemMessage = string; export type Role = "assistant" | "user" | "system" | "function"; export type Content = string; @@ -44,6 +45,12 @@ export type DateCreated = string; export type WorkspaceDirectory = string; export type SystemMessage1 = string; export type Temperature = number; +export type Title1 = string; +export type DisplayTitle = string; +export type Description3 = string; +export type Dynamic = boolean; +export type RequiresQuery = boolean; +export type ContextProviders = ContextProviderDescription[]; /** * A full state of the program, including the history @@ -58,6 +65,7 @@ export interface FullState1 { session_info?: SessionInfo; config: ContinueConfig; saved_context_groups?: SavedContextGroups; + context_providers?: ContextProviders; [k: string]: unknown; } /** @@ -84,6 +92,7 @@ export interface Step { name?: Name; hide?: Hide; description?: Description; + class_name?: ClassName; system_message?: SystemMessage; chat_context?: ChatContext; manage_own_chat_context?: ManageOwnChatContext; @@ -154,3 +163,11 @@ export interface ContinueConfig { export interface SavedContextGroups { [k: string]: ContextItem[]; } +export interface ContextProviderDescription { + title: Title1; + display_title: DisplayTitle; + description: Description3; + dynamic: Dynamic; + requires_query: RequiresQuery; + [k: string]: unknown; +} diff --git a/extension/schema/History.d.ts b/extension/schema/History.d.ts index 90124f4a..b00a1505 100644 --- a/extension/schema/History.d.ts +++ b/extension/schema/History.d.ts @@ -9,6 +9,7 @@ export type History = History1; export type Name = string; export type Hide = boolean; export type Description = string; +export type ClassName = string; export type SystemMessage = string; export type Role = "assistant" | "user" | "system" | "function"; export type Content = string; @@ -49,6 +50,7 @@ export interface Step { name?: Name; hide?: Hide; description?: Description; + class_name?: ClassName; system_message?: SystemMessage; chat_context?: ChatContext; manage_own_chat_context?: ManageOwnChatContext; diff --git a/extension/schema/HistoryNode.d.ts b/extension/schema/HistoryNode.d.ts index 5ad32061..08424d75 100644 --- a/extension/schema/HistoryNode.d.ts +++ b/extension/schema/HistoryNode.d.ts @@ -9,6 +9,7 @@ export type HistoryNode = HistoryNode1; export type Name = string; export type Hide = boolean; export type Description = string; +export type ClassName = string; export type SystemMessage = string; export type Role = "assistant" | "user" | "system" | "function"; export type Content = string; @@ -39,6 +40,7 @@ export interface Step { name?: Name; hide?: Hide; description?: Description; + class_name?: ClassName; system_message?: SystemMessage; chat_context?: ChatContext; manage_own_chat_context?: ManageOwnChatContext; diff --git a/extension/schema/LLM.d.ts b/extension/schema/LLM.d.ts index 255c752e..31d38456 100644 --- a/extension/schema/LLM.d.ts +++ b/extension/schema/LLM.d.ts @@ -6,15 +6,64 @@ */ export type LLM = LLM1; -export type RequiresApiKey = string; -export type RequiresUniqueId = boolean; -export type RequiresWriteLog = boolean; +/** + * A title that will identify this model in the model selection dropdown + */ +export type Title = string; +/** + * A system message that will always be followed by the LLM + */ export type SystemMessage = string; +/** + * The maximum context length of the LLM in tokens, as counted by count_tokens. + */ +export type ContextLength = number; +/** + * The unique ID of the user. + */ +export type UniqueId = string; +/** + * The name of the model to be used (e.g. gpt-4, codellama) + */ +export type Model = string; +/** + * Tokens that will stop the completion. + */ +export type StopTokens = string[]; +/** + * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts. + */ +export type Timeout = number; +/** + * Whether to verify SSL certificates for requests. + */ +export type VerifySsl = boolean; +/** + * Path to a custom CA bundle to use when making the HTTP request + */ +export type CaBundlePath = string; +/** + * The API key for the LLM provider. + */ +export type ApiKey = string; export interface LLM1 { - requires_api_key?: RequiresApiKey; - requires_unique_id?: RequiresUniqueId; - requires_write_log?: RequiresWriteLog; + title?: Title; system_message?: SystemMessage; + context_length?: ContextLength; + unique_id?: UniqueId; + model: Model; + stop_tokens?: StopTokens; + timeout?: Timeout; + verify_ssl?: VerifySsl; + ca_bundle_path?: CaBundlePath; + prompt_templates?: PromptTemplates; + api_key?: ApiKey; + [k: string]: unknown; +} +/** + * 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. + */ +export interface PromptTemplates { [k: string]: unknown; } diff --git a/extension/schema/Models.d.ts b/extension/schema/Models.d.ts index 33e068b5..9005c08c 100644 --- a/extension/schema/Models.d.ts +++ b/extension/schema/Models.d.ts @@ -6,10 +6,47 @@ */ export type Models = Models1; -export type RequiresApiKey = string; -export type RequiresUniqueId = boolean; -export type RequiresWriteLog = boolean; +/** + * A title that will identify this model in the model selection dropdown + */ +export type Title = string; +/** + * A system message that will always be followed by the LLM + */ export type SystemMessage = string; +/** + * The maximum context length of the LLM in tokens, as counted by count_tokens. + */ +export type ContextLength = number; +/** + * The unique ID of the user. + */ +export type UniqueId = string; +/** + * The name of the model to be used (e.g. gpt-4, codellama) + */ +export type Model = string; +/** + * Tokens that will stop the completion. + */ +export type StopTokens = string[]; +/** + * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts. + */ +export type Timeout = number; +/** + * Whether to verify SSL certificates for requests. + */ +export type VerifySsl = boolean; +/** + * Path to a custom CA bundle to use when making the HTTP request + */ +export type CaBundlePath = string; +/** + * The API key for the LLM provider. + */ +export type ApiKey = string; +export type Unused = LLM[]; /** * Main class that holds the current model configuration @@ -21,14 +58,28 @@ export interface Models1 { large?: LLM; edit?: LLM; chat?: LLM; + unused?: Unused; sdk?: ContinueSDK; [k: string]: unknown; } export interface LLM { - requires_api_key?: RequiresApiKey; - requires_unique_id?: RequiresUniqueId; - requires_write_log?: RequiresWriteLog; + title?: Title; system_message?: SystemMessage; + context_length?: ContextLength; + unique_id?: UniqueId; + model: Model; + stop_tokens?: StopTokens; + timeout?: Timeout; + verify_ssl?: VerifySsl; + ca_bundle_path?: CaBundlePath; + prompt_templates?: PromptTemplates; + api_key?: ApiKey; + [k: string]: unknown; +} +/** + * 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. + */ +export interface PromptTemplates { [k: string]: unknown; } export interface ContinueSDK { diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index 7481c211..0dce250c 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -93,4 +93,6 @@ export async function activateExtension(context: vscode.ExtensionContext) { } ) ); + + vscode.commands.executeCommand("continue.focusContinueInput"); } diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 479e8db0..4e2f4571 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -9,6 +9,36 @@ import { ideProtocolClient } from "./activation/activate"; let focusedOnContinueInput = false; +function addHighlightedCodeToContext(edit: boolean) { + focusedOnContinueInput = !focusedOnContinueInput; + const editor = vscode.window.activeTextEditor; + if (editor) { + const selection = editor.selection; + if (selection.isEmpty) return; + const range = new vscode.Range(selection.start, selection.end); + const contents = editor.document.getText(range); + ideProtocolClient?.sendHighlightedCode( + [ + { + filepath: editor.document.uri.fsPath, + contents, + range: { + start: { + line: selection.start.line, + character: selection.start.character, + }, + end: { + line: selection.end.line, + character: selection.end.character, + }, + }, + }, + ], + edit + ); + } +} + export const setFocusedOnContinueInput = (value: boolean) => { focusedOnContinueInput = value; }; @@ -32,11 +62,11 @@ const commandsMap: { [command: string]: (...args: any) => any } = { debugPanelWebview?.postMessage({ type: "focusContinueInput", }); - - focusedOnContinueInput = !focusedOnContinueInput; + addHighlightedCodeToContext(false); }, "continue.focusContinueInputWithEdit": async () => { vscode.commands.executeCommand("continue.continueGUIView.focus"); + addHighlightedCodeToContext(true); debugPanelWebview?.postMessage({ type: "focusContinueInputWithEdit", }); @@ -47,8 +77,7 @@ const commandsMap: { [command: string]: (...args: any) => any } = { }, "continue.quickTextEntry": async () => { const text = await vscode.window.showInputBox({ - placeHolder: - "Ask a question or enter a slash command", + placeHolder: "Ask a question or enter a slash command", title: "Continue Quick Input", }); if (text) { diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index 5c04e351..e2c86bdf 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -176,37 +176,37 @@ class IdeProtocolClient { }); // Setup listeners for any selection changes in open editors - vscode.window.onDidChangeTextEditorSelection((event) => { - if (!this.editorIsCode(event.textEditor)) { - return; - } - if (this._highlightDebounce) { - clearTimeout(this._highlightDebounce); - } - this._highlightDebounce = setTimeout(() => { - const highlightedCode = event.textEditor.selections - .filter((s) => !s.isEmpty) - .map((selection) => { - const range = new vscode.Range(selection.start, selection.end); - const contents = event.textEditor.document.getText(range); - return { - filepath: event.textEditor.document.uri.fsPath, - contents, - range: { - start: { - line: selection.start.line, - character: selection.start.character, - }, - end: { - line: selection.end.line, - character: selection.end.character, - }, - }, - }; - }); - this.sendHighlightedCode(highlightedCode); - }, 100); - }); + // vscode.window.onDidChangeTextEditorSelection((event) => { + // if (!this.editorIsCode(event.textEditor)) { + // return; + // } + // if (this._highlightDebounce) { + // clearTimeout(this._highlightDebounce); + // } + // this._highlightDebounce = setTimeout(() => { + // const highlightedCode = event.textEditor.selections + // .filter((s) => !s.isEmpty) + // .map((selection) => { + // const range = new vscode.Range(selection.start, selection.end); + // const contents = event.textEditor.document.getText(range); + // return { + // filepath: event.textEditor.document.uri.fsPath, + // contents, + // range: { + // start: { + // line: selection.start.line, + // character: selection.start.character, + // }, + // end: { + // line: selection.end.line, + // character: selection.end.character, + // }, + // }, + // }; + // }); + // this.sendHighlightedCode(highlightedCode); + // }, 100); + // }); // Register a content provider for the readonly virtual documents const documentContentProvider = new (class @@ -659,6 +659,11 @@ class IdeProtocolClient { ); const terminalContents = await vscode.env.clipboard.readText(); await vscode.env.clipboard.writeText(tempCopyBuffer); + + if (tempCopyBuffer === terminalContents) { + // This means there is no terminal open to select text from + return ""; + } return terminalContents; } @@ -729,16 +734,19 @@ class IdeProtocolClient { this.messenger?.send("commandOutput", { output }); } - sendHighlightedCode(highlightedCode: (RangeInFile & { contents: string })[]) { - this.messenger?.send("highlightedCodePush", { highlightedCode }); + sendHighlightedCode( + highlightedCode: (RangeInFile & { contents: string })[], + edit?: boolean + ) { + this.messenger?.send("highlightedCodePush", { highlightedCode, edit }); } sendAcceptRejectSuggestion(accepted: boolean) { this.messenger?.send("acceptRejectSuggestion", { accepted }); } - sendAcceptRejectDiff(accepted: boolean) { - this.messenger?.send("acceptRejectDiff", { accepted }); + sendAcceptRejectDiff(accepted: boolean, stepIndex: number) { + this.messenger?.send("acceptRejectDiff", { accepted, stepIndex }); } sendMainUserInput(input: string) { diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts index 4c077a25..426415fc 100644 --- a/extension/src/diffs.ts +++ b/extension/src/diffs.ts @@ -3,7 +3,7 @@ import * as path from "path"; import * as fs from "fs"; import * as vscode from "vscode"; import { extensionContext, ideProtocolClient } from "./activation/activate"; -import { getMetaKeyLabel } from "./util/util"; +import { getMetaKeyLabel, getPlatform } from "./util/util"; import { devDataPath } from "./activation/environmentSetup"; import { uriFromFilePath } from "./util/vscode"; @@ -194,10 +194,15 @@ class DiffManager { this.diffs.set(newFilepath, diffInfo); } - vscode.commands.executeCommand( - "workbench.action.files.revert", - uriFromFilePath(newFilepath) - ); + if (getPlatform() === "windows") { + // Just a matter of how it renders + // Lags on windows without this + // Flashes too much on mac with it + vscode.commands.executeCommand( + "workbench.action.files.revert", + uriFromFilePath(newFilepath) + ); + } return newFilepath; } @@ -271,6 +276,8 @@ class DiffManager { }); await recordAcceptReject(true, diffInfo); + + ideProtocolClient.sendAcceptRejectDiff(true, diffInfo.step_index); } async rejectDiff(newFilepath?: string) { @@ -302,6 +309,8 @@ class DiffManager { }); await recordAcceptReject(false, diffInfo); + + ideProtocolClient.sendAcceptRejectDiff(false, diffInfo.step_index); } } @@ -339,10 +348,8 @@ async function recordAcceptReject(accepted: boolean, diffInfo: DiffInfo) { export async function acceptDiffCommand(newFilepath?: string) { await diffManager.acceptDiff(newFilepath); - ideProtocolClient.sendAcceptRejectDiff(true); } export async function rejectDiffCommand(newFilepath?: string) { await diffManager.rejectDiff(newFilepath); - ideProtocolClient.sendAcceptRejectDiff(false); } diff --git a/extension/src/util/util.ts b/extension/src/util/util.ts index 38c955e7..1ce4a8aa 100644 --- a/extension/src/util/util.ts +++ b/extension/src/util/util.ts @@ -65,7 +65,7 @@ export function debounced(delay: number, fn: Function) { type Platform = "mac" | "linux" | "windows" | "unknown"; -function getPlatform(): Platform { +export function getPlatform(): Platform { const platform = os.platform(); if (platform === "darwin") { return "mac"; diff --git a/schema/json/ContinueConfig.json b/schema/json/ContinueConfig.json index b4f104cd..8666c420 100644 --- a/schema/json/ContinueConfig.json +++ b/schema/json/ContinueConfig.json @@ -15,10 +15,7 @@ "type": "string" } }, - "required": [ - "name", - "arguments" - ] + "required": ["name", "arguments"] }, "ChatMessage": { "title": "ChatMessage", @@ -26,12 +23,7 @@ "properties": { "role": { "title": "Role", - "enum": [ - "assistant", - "user", - "system", - "function" - ], + "enum": ["assistant", "user", "system", "function"], "type": "string" }, "content": { @@ -50,10 +42,7 @@ "$ref": "#/definitions/FunctionCall" } }, - "required": [ - "role", - "summary" - ] + "required": ["role", "summary"] }, "Step": { "title": "Step", @@ -72,6 +61,11 @@ "title": "Description", "type": "string" }, + "class_name": { + "title": "Class Name", + "default": "Step", + "type": "string" + }, "system_message": { "title": "System Message", "type": "string" @@ -95,25 +89,69 @@ "title": "LLM", "type": "object", "properties": { - "requires_api_key": { - "title": "Requires Api Key", + "title": { + "title": "Title", + "description": "A title that will identify this model in the model selection dropdown", "type": "string" }, - "requires_unique_id": { - "title": "Requires Unique Id", - "default": false, - "type": "boolean" + "system_message": { + "title": "System Message", + "description": "A system message that will always be followed by the LLM", + "type": "string" }, - "requires_write_log": { - "title": "Requires Write Log", - "default": false, + "context_length": { + "title": "Context Length", + "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", + "default": 2048, + "type": "integer" + }, + "unique_id": { + "title": "Unique Id", + "description": "The unique ID of the user.", + "type": "string" + }, + "model": { + "title": "Model", + "description": "The name of the model to be used (e.g. gpt-4, codellama)", + "type": "string" + }, + "stop_tokens": { + "title": "Stop Tokens", + "description": "Tokens that will stop the completion.", + "type": "array", + "items": { + "type": "string" + } + }, + "timeout": { + "title": "Timeout", + "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", + "default": 300, + "type": "integer" + }, + "verify_ssl": { + "title": "Verify Ssl", + "description": "Whether to verify SSL certificates for requests.", "type": "boolean" }, - "system_message": { - "title": "System Message", + "ca_bundle_path": { + "title": "Ca Bundle Path", + "description": "Path to a custom CA bundle 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.", + "default": {}, + "type": "object" + }, + "api_key": { + "title": "Api Key", + "description": "The API key for the LLM provider.", "type": "string" } - } + }, + "required": ["model"] }, "src__continuedev__core__models__ContinueSDK": { "title": "ContinueSDK", @@ -143,13 +181,19 @@ "chat": { "$ref": "#/definitions/LLM" }, + "unused": { + "title": "Unused", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/LLM" + } + }, "sdk": { "$ref": "#/definitions/src__continuedev__core__models__ContinueSDK" } }, - "required": [ - "default" - ] + "required": ["default"] }, "CustomCommand": { "title": "CustomCommand", @@ -168,11 +212,7 @@ "type": "string" } }, - "required": [ - "name", - "prompt", - "description" - ] + "required": ["name", "prompt", "description"] }, "SlashCommand": { "title": "SlashCommand", @@ -195,11 +235,7 @@ "type": "object" } }, - "required": [ - "name", - "description", - "step" - ] + "required": ["name", "description", "step"] }, "Policy": { "title": "Policy", @@ -227,10 +263,7 @@ "type": "string" } }, - "required": [ - "provider_title", - "item_id" - ] + "required": ["provider_title", "item_id"] }, "ContextItemDescription": { "title": "ContextItemDescription", @@ -249,11 +282,7 @@ "$ref": "#/definitions/ContextItemId" } }, - "required": [ - "name", - "description", - "id" - ] + "required": ["name", "description", "id"] }, "ContextItem": { "title": "ContextItem", @@ -278,10 +307,7 @@ "type": "boolean" } }, - "required": [ - "description", - "content" - ] + "required": ["description", "content"] }, "ContextProvider": { "title": "ContextProvider", @@ -290,13 +316,42 @@ "properties": { "title": { "title": "Title", + "description": "The title of the ContextProvider. This is what must be typed in the input to trigger the ContextProvider.", "type": "string" }, "sdk": { - "$ref": "#/definitions/src__continuedev__core__context__ContinueSDK" + "title": "Sdk", + "description": "The ContinueSDK instance accessible by the ContextProvider", + "allOf": [ + { + "$ref": "#/definitions/src__continuedev__core__context__ContinueSDK" + } + ] + }, + "display_title": { + "title": "Display Title", + "description": "The display title of the ContextProvider shown in the dropdown menu", + "type": "string" + }, + "description": { + "title": "Description", + "description": "A description of the ContextProvider displayed in the dropdown menu", + "type": "string" + }, + "dynamic": { + "title": "Dynamic", + "description": "Indicates whether the ContextProvider is dynamic", + "type": "boolean" + }, + "requires_query": { + "title": "Requires Query", + "description": "Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.", + "default": false, + "type": "boolean" }, "selected_items": { "title": "Selected Items", + "description": "List of selected items in the ContextProvider", "default": [], "type": "array", "items": { @@ -304,17 +359,16 @@ } } }, - "required": [ - "title" - ] + "required": ["title", "display_title", "description", "dynamic"] }, "src__continuedev__core__config__ContinueConfig": { "title": "ContinueConfig", - "description": "A pydantic class for the continue config file.", + "description": "Continue can be deeply customized by editing the `ContinueConfig` object in `~/.continue/config.py` (`%userprofile%\\.continue\\config.py` for Windows) on your machine. This class is instantiated from the config file for every new session.", "type": "object", "properties": { "steps_on_startup": { "title": "Steps On Startup", + "description": "Steps that will be automatically run at the beginning of a new session", "default": [], "type": "array", "items": { @@ -323,6 +377,7 @@ }, "disallowed_steps": { "title": "Disallowed Steps", + "description": "Steps that are not allowed to be run, and will be skipped if attempted", "default": [], "type": "array", "items": { @@ -331,38 +386,47 @@ }, "allow_anonymous_telemetry": { "title": "Allow Anonymous Telemetry", + "description": "If this field is set to True, we will collect anonymous telemetry as described in the documentation page on telemetry. If set to False, we will not collect any data.", "default": true, "type": "boolean" }, "models": { "title": "Models", + "description": "Configuration for the models used by Continue. Read more about how to configure models in the documentation.", "default": { "default": { - "requires_api_key": null, - "requires_unique_id": true, - "requires_write_log": true, + "title": null, "system_message": null, + "context_length": 2048, "model": "gpt-4", + "stop_tokens": null, + "timeout": 300, + "verify_ssl": null, + "ca_bundle_path": null, + "prompt_templates": {}, "api_key": null, "llm": null, - "name": null, - "class_name": "MaybeProxyOpenAI" + "class_name": "OpenAIFreeTrial" }, "small": null, "medium": { - "requires_api_key": null, - "requires_unique_id": true, - "requires_write_log": true, + "title": null, "system_message": null, + "context_length": 2048, "model": "gpt-3.5-turbo", + "stop_tokens": null, + "timeout": 300, + "verify_ssl": null, + "ca_bundle_path": null, + "prompt_templates": {}, "api_key": null, "llm": null, - "name": null, - "class_name": "MaybeProxyOpenAI" + "class_name": "OpenAIFreeTrial" }, "large": null, "edit": null, - "chat": null + "chat": null, + "unused": [] }, "allOf": [ { @@ -372,11 +436,13 @@ }, "temperature": { "title": "Temperature", + "description": "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.", "default": 0.5, "type": "number" }, "custom_commands": { "title": "Custom Commands", + "description": "An array of custom commands that allow you to reuse prompts. Each has name, description, and prompt properties. When you enter /<name> in the text input, it will act as a shortcut to the prompt.", "default": [ { "name": "test", @@ -391,6 +457,7 @@ }, "slash_commands": { "title": "Slash Commands", + "description": "An array of slash commands that let you map custom Steps to a shortcut.", "default": [], "type": "array", "items": { @@ -398,24 +465,55 @@ } }, "on_traceback": { - "$ref": "#/definitions/Step" + "title": "On Traceback", + "description": "The step that will be run when a traceback is detected (when you use the shortcut cmd+shift+R)", + "allOf": [ + { + "$ref": "#/definitions/Step" + } + ] }, "system_message": { "title": "System Message", + "description": "A system message that will always be followed by the LLM", "type": "string" }, "policy_override": { - "$ref": "#/definitions/Policy" + "title": "Policy Override", + "description": "A Policy object that can be used to override the default behavior of Continue, for example in order to build custom agents that take multiple steps at a time.", + "allOf": [ + { + "$ref": "#/definitions/Policy" + } + ] }, "context_providers": { "title": "Context Providers", + "description": "A list of ContextProvider objects that can be used to provide context to the LLM by typing '@'. Read more about ContextProviders in the documentation.", "default": [], "type": "array", "items": { "$ref": "#/definitions/ContextProvider" } + }, + "user_token": { + "title": "User Token", + "description": "An optional token to identify the user.", + "type": "string" + }, + "data_server_url": { + "title": "Data Server Url", + "description": "The URL of the server where development data is sent. No data is sent unless a valid user token is provided.", + "default": "https://us-west1-autodebug.cloudfunctions.net", + "type": "string" + }, + "disable_summaries": { + "title": "Disable Summaries", + "description": "If set to `True`, Continue will not generate summaries for each Step. This can be useful if you want to save on compute.", + "default": false, + "type": "boolean" } } } } -}
\ No newline at end of file +} diff --git a/schema/json/FullState.json b/schema/json/FullState.json index 1db72ad9..ae52cf5d 100644 --- a/schema/json/FullState.json +++ b/schema/json/FullState.json @@ -72,6 +72,11 @@ "title": "Description", "type": "string" }, + "class_name": { + "title": "Class Name", + "default": "Step", + "type": "string" + }, "system_message": { "title": "System Message", "type": "string" @@ -286,6 +291,39 @@ } } }, + "ContextProviderDescription": { + "title": "ContextProviderDescription", + "type": "object", + "properties": { + "title": { + "title": "Title", + "type": "string" + }, + "display_title": { + "title": "Display Title", + "type": "string" + }, + "description": { + "title": "Description", + "type": "string" + }, + "dynamic": { + "title": "Dynamic", + "type": "boolean" + }, + "requires_query": { + "title": "Requires Query", + "type": "boolean" + } + }, + "required": [ + "title", + "display_title", + "description", + "dynamic", + "requires_query" + ] + }, "src__continuedev__core__main__FullState": { "title": "FullState", "description": "A full state of the program, including the history", @@ -339,6 +377,14 @@ "$ref": "#/definitions/ContextItem" } } + }, + "context_providers": { + "title": "Context Providers", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ContextProviderDescription" + } } }, "required": [ diff --git a/schema/json/History.json b/schema/json/History.json index 56415520..c8b8208b 100644 --- a/schema/json/History.json +++ b/schema/json/History.json @@ -72,6 +72,11 @@ "title": "Description", "type": "string" }, + "class_name": { + "title": "Class Name", + "default": "Step", + "type": "string" + }, "system_message": { "title": "System Message", "type": "string" diff --git a/schema/json/HistoryNode.json b/schema/json/HistoryNode.json index 81e239b3..3ca9e394 100644 --- a/schema/json/HistoryNode.json +++ b/schema/json/HistoryNode.json @@ -72,6 +72,11 @@ "title": "Description", "type": "string" }, + "class_name": { + "title": "Class Name", + "default": "Step", + "type": "string" + }, "system_message": { "title": "System Message", "type": "string" diff --git a/schema/json/LLM.json b/schema/json/LLM.json index 57d78928..acfc4dc2 100644 --- a/schema/json/LLM.json +++ b/schema/json/LLM.json @@ -6,25 +6,71 @@ "title": "LLM", "type": "object", "properties": { - "requires_api_key": { - "title": "Requires Api Key", + "title": { + "title": "Title", + "description": "A title that will identify this model in the model selection dropdown", "type": "string" }, - "requires_unique_id": { - "title": "Requires Unique Id", - "default": false, - "type": "boolean" + "system_message": { + "title": "System Message", + "description": "A system message that will always be followed by the LLM", + "type": "string" + }, + "context_length": { + "title": "Context Length", + "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", + "default": 2048, + "type": "integer" + }, + "unique_id": { + "title": "Unique Id", + "description": "The unique ID of the user.", + "type": "string" + }, + "model": { + "title": "Model", + "description": "The name of the model to be used (e.g. gpt-4, codellama)", + "type": "string" + }, + "stop_tokens": { + "title": "Stop Tokens", + "description": "Tokens that will stop the completion.", + "type": "array", + "items": { + "type": "string" + } }, - "requires_write_log": { - "title": "Requires Write Log", - "default": false, + "timeout": { + "title": "Timeout", + "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", + "default": 300, + "type": "integer" + }, + "verify_ssl": { + "title": "Verify Ssl", + "description": "Whether to verify SSL certificates for requests.", "type": "boolean" }, - "system_message": { - "title": "System Message", + "ca_bundle_path": { + "title": "Ca Bundle Path", + "description": "Path to a custom CA bundle 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.", + "default": {}, + "type": "object" + }, + "api_key": { + "title": "Api Key", + "description": "The API key for the LLM provider.", "type": "string" } - } + }, + "required": [ + "model" + ] } } }
\ No newline at end of file diff --git a/schema/json/Models.json b/schema/json/Models.json index 0b3b9c21..de2f32c5 100644 --- a/schema/json/Models.json +++ b/schema/json/Models.json @@ -6,25 +6,71 @@ "title": "LLM", "type": "object", "properties": { - "requires_api_key": { - "title": "Requires Api Key", + "title": { + "title": "Title", + "description": "A title that will identify this model in the model selection dropdown", "type": "string" }, - "requires_unique_id": { - "title": "Requires Unique Id", - "default": false, - "type": "boolean" + "system_message": { + "title": "System Message", + "description": "A system message that will always be followed by the LLM", + "type": "string" + }, + "context_length": { + "title": "Context Length", + "description": "The maximum context length of the LLM in tokens, as counted by count_tokens.", + "default": 2048, + "type": "integer" + }, + "unique_id": { + "title": "Unique Id", + "description": "The unique ID of the user.", + "type": "string" + }, + "model": { + "title": "Model", + "description": "The name of the model to be used (e.g. gpt-4, codellama)", + "type": "string" + }, + "stop_tokens": { + "title": "Stop Tokens", + "description": "Tokens that will stop the completion.", + "type": "array", + "items": { + "type": "string" + } }, - "requires_write_log": { - "title": "Requires Write Log", - "default": false, + "timeout": { + "title": "Timeout", + "description": "Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.", + "default": 300, + "type": "integer" + }, + "verify_ssl": { + "title": "Verify Ssl", + "description": "Whether to verify SSL certificates for requests.", "type": "boolean" }, - "system_message": { - "title": "System Message", + "ca_bundle_path": { + "title": "Ca Bundle Path", + "description": "Path to a custom CA bundle 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.", + "default": {}, + "type": "object" + }, + "api_key": { + "title": "Api Key", + "description": "The API key for the LLM provider.", "type": "string" } - } + }, + "required": [ + "model" + ] }, "ContinueSDK": { "title": "ContinueSDK", @@ -54,6 +100,14 @@ "chat": { "$ref": "#/definitions/LLM" }, + "unused": { + "title": "Unused", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/LLM" + } + }, "sdk": { "$ref": "#/definitions/ContinueSDK" } |