summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--continuedev/poetry.lock12
-rw-r--r--continuedev/pyproject.toml1
-rw-r--r--continuedev/requirements.txt3
-rw-r--r--continuedev/src/continuedev/core/autopilot.py31
-rw-r--r--continuedev/src/continuedev/core/main.py9
-rw-r--r--continuedev/src/continuedev/libs/constants/default_config.py2
-rw-r--r--continuedev/src/continuedev/libs/util/paths.py9
-rw-r--r--continuedev/src/continuedev/libs/util/telemetry.py1
-rw-r--r--continuedev/src/continuedev/models/generate_json_schema.py4
-rw-r--r--continuedev/src/continuedev/plugins/context_providers/search.py92
-rw-r--r--continuedev/src/continuedev/server/gui.py12
-rw-r--r--continuedev/src/continuedev/server/main.py3
-rw-r--r--continuedev/src/continuedev/server/session_manager.py69
-rw-r--r--extension/package-lock.json2
-rw-r--r--extension/package.json2
-rw-r--r--extension/react-app/package-lock.json49
-rw-r--r--extension/react-app/package.json2
-rw-r--r--extension/react-app/src/App.tsx62
-rw-r--r--extension/react-app/src/components/DebugPanel.tsx99
-rw-r--r--extension/react-app/src/components/Layout.tsx219
-rw-r--r--extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts4
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts66
-rw-r--r--extension/react-app/src/hooks/messenger.ts6
-rw-r--r--extension/react-app/src/hooks/vscodeMessenger.ts4
-rw-r--r--extension/react-app/src/index.css4
-rw-r--r--extension/react-app/src/pages/error.tsx17
-rw-r--r--extension/react-app/src/pages/gui.tsx610
-rw-r--r--extension/react-app/src/pages/history.tsx100
-rw-r--r--extension/react-app/src/redux/hooks.ts21
-rw-r--r--extension/react-app/src/redux/selectors/debugContextSelectors.ts29
-rw-r--r--extension/react-app/src/redux/slices/configSlice.ts4
-rw-r--r--extension/react-app/src/redux/slices/debugContexSlice.ts149
-rw-r--r--extension/react-app/src/redux/slices/serverStateReducer.ts53
-rw-r--r--extension/react-app/src/redux/slices/uiStateSlice.ts26
-rw-r--r--extension/react-app/src/redux/store.ts16
-rw-r--r--extension/schema/FullState.d.ts2
-rw-r--r--extension/schema/SessionInfo.d.ts18
-rw-r--r--extension/src/activation/environmentSetup.ts1
-rw-r--r--extension/src/commands.ts9
-rw-r--r--extension/src/debugPanel.ts9
-rw-r--r--extension/src/util/messenger.ts6
-rw-r--r--schema/json/FullState.json5
-rw-r--r--schema/json/SessionInfo.json29
43 files changed, 1105 insertions, 766 deletions
diff --git a/continuedev/poetry.lock b/continuedev/poetry.lock
index 9569f19e..7c347488 100644
--- a/continuedev/poetry.lock
+++ b/continuedev/poetry.lock
@@ -1265,6 +1265,16 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
+name = "ripgrepy"
+version = "2.0.0"
+description = ""
+optional = false
+python-versions = "*"
+files = [
+ {file = "ripgrepy-2.0.0.tar.gz", hash = "sha256:6dd871bafe859301097354d1f171540fbc9bd38d3f8f52f8a196dc28522085da"},
+]
+
+[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
@@ -1767,4 +1777,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8.1"
-content-hash = "482e5ed42e46d4b422302ab5718a52c6765f5471155ee8ba0885f9436a5be565"
+content-hash = "aacc9ed670fbe067caccc88bf35f1a36541ed3b9446f64fdff7f36fe9b7b558a"
diff --git a/continuedev/pyproject.toml b/continuedev/pyproject.toml
index e2011081..f640c749 100644
--- a/continuedev/pyproject.toml
+++ b/continuedev/pyproject.toml
@@ -28,6 +28,7 @@ psutil = "^5.9.5"
pygithub = "^1.59.0"
meilisearch-python-async = "^1.4.8"
socksio = "^1.0.0"
+ripgrepy = "^2.0.0"
[tool.poetry.scripts]
typegen = "src.continuedev.models.generate_json_schema:main"
diff --git a/continuedev/requirements.txt b/continuedev/requirements.txt
index ca0b0f14..21a7400a 100644
--- a/continuedev/requirements.txt
+++ b/continuedev/requirements.txt
@@ -18,4 +18,5 @@ chevron==0.14.0
psutil==5.9.5
pygithub==1.59.0
meilisearch-python-async==1.4.8
-socksio==1.0.0 \ No newline at end of file
+socksio==1.0.0
+ripgrepy==2.0.0 \ No newline at end of file
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index f3a17d47..256f3439 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -1,10 +1,11 @@
from functools import cached_property
import traceback
import time
-from typing import Callable, Coroutine, Dict, List, Union
+from typing import Callable, Coroutine, Dict, List, Optional, Union
from aiohttp import ClientPayloadError
from pydantic import root_validator
+from ..libs.util.strings import remove_quotes_and_escapes
from ..models.filesystem import RangeInFileWithContents
from ..models.filesystem_edit import FileEditWithFullContents
from .observation import Observation, InternalErrorObservation
@@ -15,7 +16,7 @@ from ..plugins.context_providers.highlighted_code import HighlightedCodeContextP
from ..server.ide_protocol import AbstractIdeProtocolServer
from ..libs.util.queue import AsyncSubscriptionQueue
from ..models.main import ContinueBaseModel
-from .main import Context, ContinueCustomException, Policy, History, FullState, Step, HistoryNode
+from .main import Context, ContinueCustomException, Policy, History, FullState, SessionInfo, Step, HistoryNode
from ..plugins.steps.core.core import DisplayErrorStep, ReversibleStep, ManualEditStep, UserInputStep
from .sdk import ContinueSDK
from ..libs.util.traceback_parsers import get_python_traceback, get_javascript_traceback
@@ -53,7 +54,8 @@ class Autopilot(ContinueBaseModel):
policy: Policy = DefaultPolicy()
history: History = History.from_empty()
context: Context = Context()
- full_state: Union[FullState, None] = None
+ full_state: Optional[FullState] = None
+ session_info: Optional[SessionInfo] = None
context_manager: ContextManager = ContextManager()
continue_sdk: ContinueSDK = None
@@ -68,7 +70,7 @@ class Autopilot(ContinueBaseModel):
started: bool = False
- async def start(self):
+ async def start(self, full_state: Optional[FullState] = None):
self.continue_sdk = await ContinueSDK.create(self)
if override_policy := self.continue_sdk.config.policy_override:
self.policy = override_policy
@@ -84,6 +86,12 @@ class Autopilot(ContinueBaseModel):
logger.debug("Loading index")
create_async_task(self.context_manager.load_index(
self.ide.workspace_directory))
+
+ if full_state is not None:
+ self.history = full_state.history
+ self.context_manager.context_providers["code"].adding_highlighted_code = full_state.adding_highlighted_code
+ self.session_info = full_state.session_info
+
self.started = True
class Config:
@@ -106,6 +114,7 @@ class Autopilot(ContinueBaseModel):
adding_highlighted_code=self.context_manager.context_providers[
"code"].adding_highlighted_code if "code" in self.context_manager.context_providers else False,
selected_context_items=await self.context_manager.get_selected_items() if self.context_manager is not None else [],
+ session_info=self.session_info
)
self.full_state = full_state
return full_state
@@ -369,6 +378,20 @@ class Autopilot(ContinueBaseModel):
self._main_user_input_queue.append(user_input)
await self.update_subscribers()
+ # 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():
+ 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}\". The title is: ")
+ title = remove_quotes_and_escapes(title)
+ self.session_info = SessionInfo(
+ title=title,
+ session_id=self.ide.session_id,
+ date_created=str(time.time())
+ )
+
+ create_async_task(create_title(), on_error=lambda e: self.continue_sdk.run_step(
+ DisplayErrorStep(e=e)))
+
if len(self._main_user_input_queue) > 1:
return
diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py
index 2553850f..a33d777e 100644
--- a/continuedev/src/continuedev/core/main.py
+++ b/continuedev/src/continuedev/core/main.py
@@ -1,5 +1,5 @@
import json
-from typing import Coroutine, Dict, List, Literal, Union
+from typing import Coroutine, Dict, List, Literal, Optional, Union
from pydantic.schema import schema
@@ -253,6 +253,12 @@ class ContextItem(BaseModel):
editable: bool = False
+class SessionInfo(ContinueBaseModel):
+ session_id: str
+ title: str
+ date_created: str
+
+
class FullState(ContinueBaseModel):
"""A full state of the program, including the history"""
history: History
@@ -261,6 +267,7 @@ class FullState(ContinueBaseModel):
slash_commands: List[SlashCommandDescription]
adding_highlighted_code: bool
selected_context_items: List[ContextItem]
+ session_info: Optional[SessionInfo] = None
class ContinueSDK:
diff --git a/continuedev/src/continuedev/libs/constants/default_config.py b/continuedev/src/continuedev/libs/constants/default_config.py
index d3922091..f3b19f89 100644
--- a/continuedev/src/continuedev/libs/constants/default_config.py
+++ b/continuedev/src/continuedev/libs/constants/default_config.py
@@ -21,6 +21,7 @@ from continuedev.src.continuedev.plugins.steps.clear_history import ClearHistory
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.main import EditHighlightedCodeStep
+from continuedev.src.continuedev.plugins.context_providers.search import SearchContextProvider
class CommitMessageStep(Step):
@@ -122,6 +123,7 @@ config = ContinueConfig(
# GoogleContextProvider(
# serper_api_key="<your serper.dev api key>"
# )
+ SearchContextProvider()
],
# Policies hold the main logic that decides which Step to take next
diff --git a/continuedev/src/continuedev/libs/util/paths.py b/continuedev/src/continuedev/libs/util/paths.py
index 83a472ad..01b594cf 100644
--- a/continuedev/src/continuedev/libs/util/paths.py
+++ b/continuedev/src/continuedev/libs/util/paths.py
@@ -32,6 +32,15 @@ def getSessionFilePath(session_id: str):
return path
+def getSessionsListFilePath():
+ path = os.path.join(getSessionsFolderPath(), "sessions.json")
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ if not os.path.exists(path):
+ with open(path, 'w') as f:
+ f.write("[]")
+ return path
+
+
def getConfigFilePath() -> str:
path = os.path.join(getGlobalFolderPath(), "config.py")
os.makedirs(os.path.dirname(path), exist_ok=True)
diff --git a/continuedev/src/continuedev/libs/util/telemetry.py b/continuedev/src/continuedev/libs/util/telemetry.py
index 60c910bb..0f66ad8d 100644
--- a/continuedev/src/continuedev/libs/util/telemetry.py
+++ b/continuedev/src/continuedev/libs/util/telemetry.py
@@ -23,7 +23,6 @@ class PostHogLogger:
self.posthog = Posthog(self.api_key, host='https://app.posthog.com')
def setup(self, unique_id: str, allow_anonymous_telemetry: bool):
- logger.debug(f"Setting unique_id as {unique_id}")
self.unique_id = unique_id or "NO_UNIQUE_ID"
self.allow_anonymous_telemetry = allow_anonymous_telemetry or True
diff --git a/continuedev/src/continuedev/models/generate_json_schema.py b/continuedev/src/continuedev/models/generate_json_schema.py
index 2166bc37..4262ac55 100644
--- a/continuedev/src/continuedev/models/generate_json_schema.py
+++ b/continuedev/src/continuedev/models/generate_json_schema.py
@@ -1,7 +1,7 @@
from .main import *
from .filesystem import RangeInFile, FileEdit
from .filesystem_edit import FileEditWithFullContents
-from ..core.main import History, HistoryNode, FullState
+from ..core.main import History, HistoryNode, FullState, SessionInfo
from ..core.context import ContextItem
from pydantic import schema_json_of
import os
@@ -13,7 +13,7 @@ MODELS_TO_GENERATE = [
] + [
FileEditWithFullContents
] + [
- History, HistoryNode, FullState
+ History, HistoryNode, FullState, SessionInfo
] + [
ContextItem
]
diff --git a/continuedev/src/continuedev/plugins/context_providers/search.py b/continuedev/src/continuedev/plugins/context_providers/search.py
new file mode 100644
index 00000000..17f2660c
--- /dev/null
+++ b/continuedev/src/continuedev/plugins/context_providers/search.py
@@ -0,0 +1,92 @@
+import os
+from typing import List
+from ripgrepy import Ripgrepy
+
+from .util import remove_meilisearch_disallowed_chars
+from ...core.main import ContextItem, ContextItemDescription, ContextItemId
+from ...core.context import ContextProvider
+
+
+class SearchContextProvider(ContextProvider):
+ title = "search"
+
+ SEARCH_CONTEXT_ITEM_ID = "search"
+
+ workspace_dir: str = None
+
+ @property
+ def BASE_CONTEXT_ITEM(self):
+ return ContextItem(
+ content="",
+ description=ContextItemDescription(
+ name="Search",
+ description="Search the workspace for all matches of an exact string (e.g. '@search console.log')",
+ id=ContextItemId(
+ provider_title=self.title,
+ item_id=self.SEARCH_CONTEXT_ITEM_ID
+ )
+ )
+ )
+
+ def _get_rg_path(self):
+ if os.name == 'nt':
+ rg_path = f"C:\\Users\\{os.getlogin()}\\AppData\\Local\\Programs\\Microsoft VS Code\\resources\\app\\node_modules.asar.unpacked\\vscode-ripgrep\\bin\\rg.exe"
+ elif os.name == 'posix':
+ if 'darwin' in os.sys.platform:
+ rg_path = "/Applications/Visual Studio Code.app/Contents/Resources/app/node_modules.asar.unpacked/vscode-ripgrep/bin/rg"
+ else:
+ rg_path = "/usr/share/code/resources/app/node_modules.asar.unpacked/vscode-ripgrep/bin/rg"
+ else:
+ rg_path = "rg"
+
+ if not os.path.exists(rg_path):
+ rg_path = "rg"
+
+ return rg_path
+
+ async def _search(self, query: str) -> str:
+ rg = Ripgrepy(query, self.workspace_dir, rg_path=self._get_rg_path())
+ results = rg.I().context(2).run()
+ return f"Search results in workspace for '{query}':\n\n{results}"
+
+ # Custom display below - TODO
+
+ # Gather results per file
+ file_to_matches = {}
+ for result in results:
+ if result["type"] == "match":
+ data = result["data"]
+ filepath = data["path"]["text"]
+ if filepath not in file_to_matches:
+ file_to_matches[filepath] = []
+
+ line_num_and_line = f"{data['line_number']}: {data['lines']['text']}"
+ file_to_matches[filepath].append(line_num_and_line)
+
+ # Format results
+ content = f"Search results in workspace for '{query}':\n\n"
+ for filepath, matches in file_to_matches.items():
+ content += f"{filepath}\n"
+ for match in matches:
+ content += f"{match}\n"
+ content += "\n"
+
+ return content
+
+ async def provide_context_items(self, workspace_dir: str) -> List[ContextItem]:
+ self.workspace_dir = workspace_dir
+ return [self.BASE_CONTEXT_ITEM]
+
+ async def get_item(self, id: ContextItemId, query: str) -> ContextItem:
+ if not id.item_id == self.SEARCH_CONTEXT_ITEM_ID:
+ raise Exception("Invalid item id")
+
+ query = query.lstrip("search ")
+ results = await self._search(query)
+
+ ctx_item = self.BASE_CONTEXT_ITEM.copy()
+ ctx_item.content = results
+ ctx_item.description.name = f"Search: '{query}'"
+ ctx_item.description.id.item_id = remove_meilisearch_disallowed_chars(
+ query)
+ return ctx_item
diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py
index 7c89c5c2..4470999a 100644
--- a/continuedev/src/continuedev/server/gui.py
+++ b/continuedev/src/continuedev/server/gui.py
@@ -2,7 +2,7 @@ import asyncio
import json
from fastapi import Depends, Header, WebSocket, APIRouter
from starlette.websockets import WebSocketState, WebSocketDisconnect
-from typing import Any, List, Type, TypeVar
+from typing import Any, List, Optional, Type, TypeVar
from pydantic import BaseModel
import traceback
from uvicorn.main import Server
@@ -99,6 +99,8 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
self.on_show_logs_at_index(data["index"])
elif message_type == "select_context_item":
self.select_context_item(data["id"], data["query"])
+ elif message_type == "load_session":
+ self.load_session(data.get("session_id", None))
def on_main_input(self, input: str):
# Do something with user input
@@ -154,6 +156,14 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
create_async_task(
self.session.autopilot.select_context_item(id, query), self.on_error)
+ def load_session(self, session_id: Optional[str] = None):
+ async def load_and_tell_to_reconnect():
+ new_session_id = await session_manager.load_session(self.session.session_id, session_id)
+ await self._send_json("reconnect_at_session", {"session_id": new_session_id})
+
+ create_async_task(
+ load_and_tell_to_reconnect(), self.on_error)
+
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(websocket_session)):
diff --git a/continuedev/src/continuedev/server/main.py b/continuedev/src/continuedev/server/main.py
index f8dfb009..f0a3f094 100644
--- a/continuedev/src/continuedev/server/main.py
+++ b/continuedev/src/continuedev/server/main.py
@@ -11,13 +11,14 @@ import argparse
from .ide import router as ide_router
from .gui import router as gui_router
-from .session_manager import session_manager
+from .session_manager import session_manager, router as sessions_router
from ..libs.util.logging import logger
app = FastAPI()
app.include_router(ide_router)
app.include_router(gui_router)
+app.include_router(sessions_router)
# Add CORS support
app.add_middleware(
diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py
index 56c92307..cde0344e 100644
--- a/continuedev/src/continuedev/server/session_manager.py
+++ b/continuedev/src/continuedev/server/session_manager.py
@@ -1,21 +1,23 @@
import os
import traceback
-from fastapi import WebSocket
-from typing import Any, Coroutine, Dict, Union
+from fastapi import WebSocket, APIRouter
+from typing import Any, Coroutine, Dict, Optional, Union
from uuid import uuid4
import json
from fastapi.websockets import WebSocketState
from ..plugins.steps.core.core import MessageStep
-from ..libs.util.paths import getSessionFilePath, getSessionsFolderPath
-from ..core.main import FullState, HistoryNode
+from ..libs.util.paths import getSessionFilePath, getSessionsFolderPath, getSessionsListFilePath
+from ..core.main import FullState, HistoryNode, SessionInfo
from ..core.autopilot import Autopilot
from .ide_protocol import AbstractIdeProtocolServer
from ..libs.util.create_async_task import create_async_task
from ..libs.util.errors import SessionNotFound
from ..libs.util.logging import logger
+router = APIRouter(prefix="/sessions", tags=["sessions"])
+
class Session:
session_id: str
@@ -47,7 +49,7 @@ class SessionManager:
raise KeyError("Session ID not recognized", session_id)
return self.sessions[session_id]
- async def new_session(self, ide: AbstractIdeProtocolServer, session_id: Union[str, None] = None) -> Session:
+ async def new_session(self, ide: AbstractIdeProtocolServer, session_id: Optional[str] = None) -> Session:
logger.debug(f"New session: {session_id}")
# Load the persisted state (not being used right now)
@@ -74,20 +76,9 @@ class SessionManager:
# Start the autopilot (must be after session is added to sessions) and the policy
try:
- await autopilot.start()
+ await autopilot.start(full_state=full_state)
except Exception as e:
- # Have to manually add to history because autopilot isn't started
- formatted_err = '\n'.join(traceback.format_exception(e))
- msg_step = MessageStep(
- name="Error loading context manager", message=formatted_err)
- msg_step.description = f"```\n{formatted_err}\n```"
- autopilot.history.add_node(HistoryNode(
- step=msg_step,
- observation=None,
- depth=0,
- active=False
- ))
- logger.warning(f"Error loading context manager: {e}")
+ await self.on_error(e)
def on_error(e: Exception) -> Coroutine:
err_msg = '\n'.join(traceback.format_exception(e))
@@ -99,7 +90,7 @@ class SessionManager:
async def remove_session(self, session_id: str):
logger.debug(f"Removing session: {session_id}")
if session_id in self.sessions:
- if session_id in self.registered_ides:
+ if session_id in self.registered_ides and self.registered_ides[session_id] is not None:
ws_to_close = self.registered_ides[session_id].websocket
if ws_to_close is not None and ws_to_close.client_state != WebSocketState.DISCONNECTED:
await self.sessions[session_id].autopilot.ide.websocket.close()
@@ -109,9 +100,37 @@ class SessionManager:
async def persist_session(self, session_id: str):
"""Save the session's FullState as a json file"""
full_state = await self.sessions[session_id].autopilot.get_full_state()
+ if full_state.session_info is None:
+ return
+
with open(getSessionFilePath(session_id), "w") as f:
json.dump(full_state.dict(), f)
+ # Read and update the sessions list
+ with open(getSessionsListFilePath(), "r") as f:
+ sessions_list = json.load(f)
+
+ session_ids = [s["session_id"] for s in sessions_list]
+ if session_id not in session_ids:
+ sessions_list.append(full_state.session_info.dict())
+
+ with open(getSessionsListFilePath(), "w") as f:
+ json.dump(sessions_list, f)
+
+ async def load_session(self, old_session_id: str, new_session_id: Optional[str] = None) -> str:
+ """Load the session's FullState from a json file"""
+
+ # First persist the current state
+ await self.persist_session(old_session_id)
+
+ # Delete the old session, but keep the IDE
+ ide = self.registered_ides[old_session_id]
+ del self.registered_ides[old_session_id]
+
+ # Start the new session
+ new_session = await self.new_session(ide, session_id=new_session_id)
+ return new_session.session_id
+
def register_websocket(self, session_id: str, ws: WebSocket):
self.sessions[session_id].ws = ws
logger.debug(f"Registered websocket for session {session_id}")
@@ -130,3 +149,15 @@ class SessionManager:
session_manager = SessionManager()
+
+
+@router.get("/list")
+async def list_sessions():
+ """List all sessions"""
+ sessions_list_file = getSessionsListFilePath()
+ if not os.path.exists(sessions_list_file):
+ print("Returning empty sessions list")
+ return []
+ sessions = json.load(open(sessions_list_file, "r"))
+ print("Returning sessions list: ", sessions)
+ return sessions
diff --git a/extension/package-lock.json b/extension/package-lock.json
index 8d558976..f7d94b68 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "continue",
- "version": "0.0.266",
+ "version": "0.0.271",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/extension/package.json b/extension/package.json
index c30b3aa9..a1be66e1 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -14,7 +14,7 @@
"displayName": "Continue",
"pricing": "Free",
"description": "The open-source coding autopilot",
- "version": "0.0.266",
+ "version": "0.0.271",
"publisher": "Continue",
"engines": {
"vscode": "^1.67.0"
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index 0d348ffb..50f04d1a 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -429,6 +429,11 @@
"fastq": "^1.6.0"
}
},
+ "@remix-run/router": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz",
+ "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A=="
+ },
"@swc/core": {
"version": "1.3.73",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.73.tgz",
@@ -533,6 +538,12 @@
"@types/unist": "^2"
}
},
+ "@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "dev": true
+ },
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
@@ -589,6 +600,27 @@
"@types/react": "*"
}
},
+ "@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "dev": true,
+ "requires": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "dev": true,
+ "requires": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"@types/react-syntax-highlighter": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.7.tgz",
@@ -2346,6 +2378,23 @@
"use-sync-external-store": "^1.0.0"
}
},
+ "react-router": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz",
+ "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==",
+ "requires": {
+ "@remix-run/router": "1.7.2"
+ }
+ },
+ "react-router-dom": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz",
+ "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==",
+ "requires": {
+ "@remix-run/router": "1.7.2",
+ "react-router": "6.14.2"
+ }
+ },
"react-switch": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index 4c5ab26c..b4762990 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -19,6 +19,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
+ "react-router-dom": "^6.14.2",
"react-switch": "^7.0.0",
"react-syntax-highlighter": "^15.5.0",
"react-tooltip": "^5.18.0",
@@ -28,6 +29,7 @@
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
+ "@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.7",
"@types/styled-components": "^5.1.26",
"@vitejs/plugin-react-swc": "^3.0.0",
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx
index aa462171..879373a0 100644
--- a/extension/react-app/src/App.tsx
+++ b/extension/react-app/src/App.tsx
@@ -1,8 +1,43 @@
-import DebugPanel from "./components/DebugPanel";
import GUI from "./pages/gui";
-import { createContext } from "react";
+import History from "./pages/history";
+import Layout from "./components/Layout";
+import { createContext, useEffect } from "react";
import useContinueGUIProtocol from "./hooks/useWebsocket";
import ContinueGUIClientProtocol from "./hooks/ContinueGUIClientProtocol";
+import { useDispatch } from "react-redux";
+import {
+ setApiUrl,
+ setVscMachineId,
+ setSessionId,
+ setVscMediaUrl,
+ setDataSwitchOn,
+} from "./redux/slices/configSlice";
+import { setHighlightedCode } from "./redux/slices/miscSlice";
+import { postVscMessage } from "./vscode";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import ErrorPage from "./pages/error";
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: <Layout />,
+ errorElement: <ErrorPage />,
+ children: [
+ {
+ path: "/index.html",
+ element: <GUI />,
+ },
+ {
+ path: "/",
+ element: <GUI />,
+ },
+ {
+ path: "/history",
+ element: <History />,
+ },
+ ],
+ },
+]);
export const GUIClientContext = createContext<
ContinueGUIClientProtocol | undefined
@@ -11,9 +46,30 @@ export const GUIClientContext = createContext<
function App() {
const client = useContinueGUIProtocol();
+ const dispatch = useDispatch();
+ useEffect(() => {
+ const eventListener = (event: any) => {
+ switch (event.data.type) {
+ case "onLoad":
+ dispatch(setApiUrl(event.data.apiUrl));
+ dispatch(setVscMachineId(event.data.vscMachineId));
+ dispatch(setSessionId(event.data.sessionId));
+ dispatch(setVscMediaUrl(event.data.vscMediaUrl));
+ dispatch(setDataSwitchOn(event.data.dataSwitchOn));
+ break;
+ case "highlightedCode":
+ dispatch(setHighlightedCode(event.data.rangeInFile));
+ break;
+ }
+ };
+ window.addEventListener("message", eventListener);
+ postVscMessage("onLoad", {});
+ return () => window.removeEventListener("message", eventListener);
+ }, []);
+
return (
<GUIClientContext.Provider value={client}>
- <DebugPanel tabs={[{ element: <GUI />, title: "GUI" }]} />
+ <RouterProvider router={router} />
</GUIClientContext.Provider>
);
}
diff --git a/extension/react-app/src/components/DebugPanel.tsx b/extension/react-app/src/components/DebugPanel.tsx
deleted file mode 100644
index fffb6c6e..00000000
--- a/extension/react-app/src/components/DebugPanel.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-import { postVscMessage } from "../vscode";
-import { useDispatch } from "react-redux";
-import {
- setApiUrl,
- setVscMachineId,
- setSessionId,
- setVscMediaUrl,
- setDataSwitchOn,
-} from "../redux/slices/configSlice";
-import { setHighlightedCode } from "../redux/slices/miscSlice";
-import { updateFileSystem } from "../redux/slices/debugContexSlice";
-import { defaultBorderRadius, secondaryDark, vscBackground } from ".";
-interface DebugPanelProps {
- tabs: {
- element: React.ReactElement;
- title: string;
- }[];
-}
-
-const TabBar = styled.div<{ numTabs: number }>`
- display: grid;
- grid-template-columns: repeat(${(props) => props.numTabs}, 1fr);
-`;
-
-const TabsAndBodyDiv = styled.div`
- height: 100%;
- border-radius: ${defaultBorderRadius};
- scrollbar-base-color: transparent;
-`;
-
-function DebugPanel(props: DebugPanelProps) {
- const dispatch = useDispatch();
- useEffect(() => {
- const eventListener = (event: any) => {
- switch (event.data.type) {
- case "onLoad":
- dispatch(setApiUrl(event.data.apiUrl));
- dispatch(setVscMachineId(event.data.vscMachineId));
- dispatch(setSessionId(event.data.sessionId));
- dispatch(setVscMediaUrl(event.data.vscMediaUrl));
- dispatch(setDataSwitchOn(event.data.dataSwitchOn));
- break;
- case "highlightedCode":
- dispatch(setHighlightedCode(event.data.rangeInFile));
- dispatch(updateFileSystem(event.data.filesystem));
- break;
- }
- };
- window.addEventListener("message", eventListener);
- postVscMessage("onLoad", {});
- return () => window.removeEventListener("message", eventListener);
- }, []);
-
- const [currentTab, setCurrentTab] = useState(0);
-
- return (
- <TabsAndBodyDiv>
- {props.tabs.length > 1 && (
- <TabBar numTabs={props.tabs.length}>
- {props.tabs.map((tab, index) => {
- return (
- <div
- key={index}
- className={`p-2 cursor-pointer text-center ${
- index === currentTab
- ? "bg-secondary-dark"
- : "bg-vsc-background"
- }`}
- onClick={() => setCurrentTab(index)}
- >
- {tab.title}
- </div>
- );
- })}
- </TabBar>
- )}
- {props.tabs.map((tab, index) => {
- return (
- <div
- key={index}
- hidden={index !== currentTab}
- style={{
- scrollbarGutter: "stable both-edges",
- minHeight: "100%",
- display: "grid",
- gridTemplateRows: "1fr auto",
- }}
- >
- {tab.element}
- </div>
- );
- })}
- </TabsAndBodyDiv>
- );
-}
-
-export default DebugPanel;
diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx
new file mode 100644
index 00000000..a2e54173
--- /dev/null
+++ b/extension/react-app/src/components/Layout.tsx
@@ -0,0 +1,219 @@
+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 } from "react";
+import { GUIClientContext } from "../App";
+import { useDispatch, useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+ setDialogEntryOn,
+ setDialogMessage,
+ setShowDialog,
+} from "../redux/slices/uiStateSlice";
+import {
+ PlusIcon,
+ FolderIcon,
+ BookOpenIcon,
+ ChatBubbleOvalLeftEllipsisIcon,
+} from "@heroicons/react/24/outline";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+import { useNavigate } from "react-router-dom";
+
+// #region Styled Components
+
+const LayoutTopDiv = styled.div`
+ height: 100%;
+ border-radius: ${defaultBorderRadius};
+ scrollbar-base-color: transparent;
+ scrollbar-width: thin;
+`;
+
+const BottomMessageDiv = styled.div<{ displayOnBottom: boolean }>`
+ position: fixed;
+ bottom: ${(props) => (props.displayOnBottom ? "50px" : undefined)};
+ top: ${(props) => (props.displayOnBottom ? undefined : "50px")};
+ left: 0;
+ right: 0;
+ margin: 8px;
+ margin-top: 0;
+ background-color: ${secondaryDark};
+ color: ${vscForeground};
+ border-radius: ${defaultBorderRadius};
+ padding: 12px;
+ z-index: 100;
+ box-shadow: 0px 0px 2px 0px ${vscForeground};
+ max-height: 50vh;
+ overflow: scroll;
+`;
+
+const Footer = styled.footer`
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ justify-content: right;
+ padding: 8px;
+ align-items: center;
+ margin-top: 8px;
+ border-top: 0.1px solid gray;
+`;
+
+// #endregion
+
+const Layout = () => {
+ const navigate = useNavigate();
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+ const dialogMessage = useSelector(
+ (state: RootStore) => state.uiState.dialogMessage
+ );
+ const showDialog = useSelector(
+ (state: RootStore) => state.uiState.showDialog
+ );
+ const dialogEntryOn = useSelector(
+ (state: RootStore) => state.uiState.dialogEntryOn
+ );
+
+ // #region Selectors
+ const vscMediaUrl = useSelector(
+ (state: RootStore) => state.config.vscMediaUrl
+ );
+
+ const bottomMessage = useSelector(
+ (state: RootStore) => state.uiState.bottomMessage
+ );
+ const displayBottomMessageOnBottom = useSelector(
+ (state: RootStore) => state.uiState.displayBottomMessageOnBottom
+ );
+
+ // #endregion
+
+ return (
+ <LayoutTopDiv>
+ <div
+ style={{
+ scrollbarGutter: "stable both-edges",
+ minHeight: "100%",
+ display: "grid",
+ gridTemplateRows: "1fr auto",
+ }}
+ >
+ <Onboarding />
+ <TextDialog
+ showDialog={showDialog}
+ onEnter={(text) => {
+ client?.sendMainInput(`/feedback ${text}`);
+ dispatch(setShowDialog(false));
+ }}
+ onClose={() => {
+ dispatch(setShowDialog(false));
+ }}
+ message={dialogMessage}
+ entryOn={dialogEntryOn}
+ />
+ <Outlet />
+
+ <BottomMessageDiv
+ displayOnBottom={displayBottomMessageOnBottom}
+ onMouseEnter={() => {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ }}
+ onMouseLeave={(e) => {
+ if (!e.buttons) {
+ dispatch(setBottomMessage(undefined));
+ }
+ }}
+ hidden={!bottomMessage}
+ >
+ {bottomMessage}
+ </BottomMessageDiv>
+ <Footer>
+ {vscMediaUrl && (
+ <a
+ href="https://github.com/continuedev/continue"
+ style={{ marginRight: "auto" }}
+ >
+ <img
+ src={`${vscMediaUrl}/continue-dev-square.png`}
+ width="22px"
+ style={{ backgroundColor: "black", color: "red" }}
+ />
+ </a>
+ )}
+ <HeaderButtonWithText
+ onClick={() => {
+ // Show the dialog
+ dispatch(
+ setDialogMessage(`Continue uses GPT-4 by default, but works with any model. If you'd like to keep your code completely private, there are few options:
+
+ Run a local model with ggml: [5 minute quickstart](https://github.com/continuedev/ggml-server-example)
+
+ Use Azure OpenAI service, which is GDPR and HIPAA compliant: [Tutorial](https://continue.dev/docs/customization#azure-openai-service)
+
+ If you already have an LLM deployed on your own infrastructure, or would like to do so, please contact us at hi@continue.dev.
+ `)
+ );
+ dispatch(setDialogEntryOn(false));
+ dispatch(setShowDialog(true));
+ }}
+ text={"Use Private Model"}
+ >
+ <div
+ style={{
+ fontSize: "18px",
+ marginLeft: "2px",
+ marginRight: "2px",
+ }}
+ >
+ 🔒
+ </div>
+ </HeaderButtonWithText>
+ <HeaderButtonWithText
+ onClick={() => {
+ client?.loadSession(undefined);
+ }}
+ text="New Session"
+ >
+ <PlusIcon 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>
+ <HeaderButtonWithText
+ onClick={() => {
+ // Set dialog open
+ dispatch(
+ setDialogMessage(
+ "Having trouble using Continue? Want a new feature? Let us know! This box is anonymous, but we will promptly address your feedback."
+ )
+ );
+ dispatch(setDialogEntryOn(true));
+ dispatch(setShowDialog(true));
+ }}
+ text="Feedback"
+ >
+ <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ </Footer>
+ </div>
+ </LayoutTopDiv>
+ );
+};
+
+export default Layout;
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 8d8b7b7e..e018c03c 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -30,6 +30,10 @@ abstract class AbstractContinueGUIClientProtocol {
abstract showLogsAtIndex(index: number): void;
abstract selectContextItem(id: string, query: string): void;
+
+ abstract loadSession(session_id?: string): void;
+
+ abstract onReconnectAtSession(session_id: string): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index b6dd43d9..c2285f6d 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -4,16 +4,21 @@ import { Messenger, WebsocketMessenger } from "./messenger";
import { VscodeMessenger } from "./vscodeMessenger";
class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
- messenger: Messenger;
+ messenger?: Messenger;
// Server URL must contain the session ID param
serverUrlWithSessionId: string;
+ useVscodeMessagePassing: boolean;
- constructor(
+ private connectMessenger(
serverUrlWithSessionId: string,
useVscodeMessagePassing: boolean
) {
- super();
+ if (this.messenger) {
+ console.log("Closing session: ", this.serverUrlWithSessionId);
+ this.messenger.close();
+ }
this.serverUrlWithSessionId = serverUrlWithSessionId;
+ this.useVscodeMessagePassing = useVscodeMessagePassing;
this.messenger = useVscodeMessagePassing
? new VscodeMessenger(serverUrlWithSessionId)
: new WebsocketMessenger(serverUrlWithSessionId);
@@ -24,26 +29,53 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger.onError((error) => {
console.log("GUI -> IDE websocket error", error);
});
+
+ this.messenger.onMessageType("reconnect_at_session", (data: any) => {
+ if (data.session_id) {
+ this.onReconnectAtSession(data.session_id);
+ }
+ });
+ }
+
+ constructor(
+ serverUrlWithSessionId: string,
+ useVscodeMessagePassing: boolean
+ ) {
+ super();
+ this.serverUrlWithSessionId = serverUrlWithSessionId;
+ this.useVscodeMessagePassing = useVscodeMessagePassing;
+ this.connectMessenger(serverUrlWithSessionId, useVscodeMessagePassing);
+ }
+
+ loadSession(session_id?: string): void {
+ this.messenger?.send("load_session", { session_id });
+ }
+
+ onReconnectAtSession(session_id: string): void {
+ this.connectMessenger(
+ `${this.serverUrlWithSessionId.split("?")[0]}?session_id=${session_id}`,
+ this.useVscodeMessagePassing
+ );
}
sendMainInput(input: string) {
- this.messenger.send("main_input", { input });
+ this.messenger?.send("main_input", { input });
}
reverseToIndex(index: number) {
- this.messenger.send("reverse_to_index", { index });
+ this.messenger?.send("reverse_to_index", { index });
}
sendRefinementInput(input: string, index: number) {
- this.messenger.send("refinement_input", { input, index });
+ this.messenger?.send("refinement_input", { input, index });
}
sendStepUserInput(input: string, index: number) {
- this.messenger.send("step_user_input", { input, index });
+ this.messenger?.send("step_user_input", { input, index });
}
onStateUpdate(callback: (state: any) => void) {
- this.messenger.onMessageType("state_update", (data: any) => {
+ this.messenger?.onMessageType("state_update", (data: any) => {
if (data.state) {
callback(data.state);
}
@@ -53,7 +85,7 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
onAvailableSlashCommands(
callback: (commands: { name: string; description: string }[]) => void
) {
- this.messenger.onMessageType("available_slash_commands", (data: any) => {
+ this.messenger?.onMessageType("available_slash_commands", (data: any) => {
if (data.commands) {
callback(data.commands);
}
@@ -61,37 +93,37 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
}
sendClear() {
- this.messenger.send("clear_history", {});
+ this.messenger?.send("clear_history", {});
}
retryAtIndex(index: number) {
- this.messenger.send("retry_at_index", { index });
+ this.messenger?.send("retry_at_index", { index });
}
deleteAtIndex(index: number) {
- this.messenger.send("delete_at_index", { index });
+ this.messenger?.send("delete_at_index", { index });
}
deleteContextWithIds(ids: ContextItemId[]) {
- this.messenger.send("delete_context_with_ids", {
+ this.messenger?.send("delete_context_with_ids", {
ids: ids.map((id) => `${id.provider_title}-${id.item_id}`),
});
}
setEditingAtIds(ids: string[]) {
- this.messenger.send("set_editing_at_ids", { ids });
+ this.messenger?.send("set_editing_at_ids", { ids });
}
toggleAddingHighlightedCode(): void {
- this.messenger.send("toggle_adding_highlighted_code", {});
+ this.messenger?.send("toggle_adding_highlighted_code", {});
}
showLogsAtIndex(index: number): void {
- this.messenger.send("show_logs_at_index", { index });
+ this.messenger?.send("show_logs_at_index", { index });
}
selectContextItem(id: string, query: string): void {
- this.messenger.send("select_context_item", { id, query });
+ this.messenger?.send("select_context_item", { id, query });
}
}
diff --git a/extension/react-app/src/hooks/messenger.ts b/extension/react-app/src/hooks/messenger.ts
index ecf646c7..0bfbe00c 100644
--- a/extension/react-app/src/hooks/messenger.ts
+++ b/extension/react-app/src/hooks/messenger.ts
@@ -15,6 +15,8 @@ export abstract class Messenger {
abstract sendAndReceive(messageType: string, data: any): Promise<any>;
abstract onError(callback: (error: any) => void): void;
+
+ abstract close(): void;
}
export class WebsocketMessenger extends Messenger {
@@ -105,4 +107,8 @@ export class WebsocketMessenger extends Messenger {
onError(callback: (error: any) => void): void {
this.websocket.addEventListener("error", callback);
}
+
+ close(): void {
+ this.websocket.close();
+ }
}
diff --git a/extension/react-app/src/hooks/vscodeMessenger.ts b/extension/react-app/src/hooks/vscodeMessenger.ts
index 13f5092b..cf626721 100644
--- a/extension/react-app/src/hooks/vscodeMessenger.ts
+++ b/extension/react-app/src/hooks/vscodeMessenger.ts
@@ -76,4 +76,8 @@ export class VscodeMessenger extends Messenger {
}
});
}
+
+ close(): void {
+ postVscMessage("websocketForwardingClose", { url: this.serverUrl });
+ }
}
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index 6a46800e..269da69a 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -9,9 +9,9 @@
--button-color-hover: rgba(113, 28, 59, 0.667);
--def-border-radius: 5px;
- /* --vscode-editor-background: rgb(30, 30, 30);
+ --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, 0.05);
}
html,
diff --git a/extension/react-app/src/pages/error.tsx b/extension/react-app/src/pages/error.tsx
new file mode 100644
index 00000000..5267c405
--- /dev/null
+++ b/extension/react-app/src/pages/error.tsx
@@ -0,0 +1,17 @@
+import { useRouteError } from "react-router-dom";
+
+export default function ErrorPage() {
+ const error: any = useRouteError();
+ console.error(error);
+
+ return (
+ <div id="error-page" className="text-center">
+ <h1>Error in Continue React App</h1>
+ <p>
+ <i>{error.statusText || error.message}</i>
+ </p>
+ <br />
+ <pre className="text-left m-4">{error.stack}</pre>
+ </div>
+ );
+}
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index d69da57e..02b903f7 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,60 +1,31 @@
import styled from "styled-components";
-import {
- defaultBorderRadius,
- secondaryDark,
- vscForeground,
-} from "../components";
+import { defaultBorderRadius } from "../components";
import Loader from "../components/Loader";
import ContinueButton from "../components/ContinueButton";
-import { ContextItem, FullState } from "../../../schema/FullState";
+import { FullState } from "../../../schema/FullState";
import { useCallback, useEffect, useRef, useState, useContext } from "react";
-import { History } from "../../../schema/History";
import { HistoryNode } from "../../../schema/HistoryNode";
import StepContainer from "../components/StepContainer";
import { GUIClientContext } from "../App";
-import {
- BookOpenIcon,
- ChatBubbleOvalLeftEllipsisIcon,
- TrashIcon,
-} from "@heroicons/react/24/outline";
import ComboBox from "../components/ComboBox";
-import TextDialog from "../components/TextDialog";
-import HeaderButtonWithText from "../components/HeaderButtonWithText";
-import ReactSwitch from "react-switch";
import { usePostHog } from "posthog-js/react";
import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { postVscMessage } from "../vscode";
import UserInputContainer from "../components/UserInputContainer";
-import Onboarding from "../components/Onboarding";
import { isMetaEquivalentKeyPressed } from "../util";
import {
setBottomMessage,
- setBottomMessageCloseTimeout,
+ setDialogEntryOn,
+ setDialogMessage,
+ setDisplayBottomMessageOnBottom,
+ setShowDialog,
} from "../redux/slices/uiStateSlice";
import RingLoader from "../components/RingLoader";
-
-const TopGUIDiv = styled.div`
- overflow: hidden;
-`;
-
-const BottomMessageDiv = styled.div<{ displayOnBottom: boolean }>`
- position: fixed;
- bottom: ${(props) => (props.displayOnBottom ? "50px" : undefined)};
- top: ${(props) => (props.displayOnBottom ? undefined : "50px")};
- left: 0;
- right: 0;
- margin: 8px;
- margin-top: 0;
- background-color: ${secondaryDark};
- color: ${vscForeground};
- border-radius: ${defaultBorderRadius};
- padding: 12px;
- z-index: 100;
- box-shadow: 0px 0px 2px 0px ${vscForeground};
- max-height: 50vh;
- overflow: scroll;
-`;
+import {
+ setServerState,
+ temporarilySetUserInputQueue,
+} from "../redux/slices/serverStateReducer";
const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
@@ -64,47 +35,34 @@ const UserInputQueueItem = styled.div`
text-align: center;
`;
-const Footer = styled.footer<{ dataSwitchChecked: boolean }>`
- display: flex;
- flex-direction: row;
- gap: 8px;
- justify-content: right;
- padding: 8px;
- align-items: center;
- margin-top: 8px;
- border-top: 0.1px solid gray;
- background-color: ${(props) =>
- props.dataSwitchChecked ? "#12887a33" : "transparent"};
-`;
-
interface GUIProps {
firstObservation?: any;
}
function GUI(props: GUIProps) {
+ // #region Hooks
const client = useContext(GUIClientContext);
const posthog = usePostHog();
+ const dispatch = useDispatch();
- const vscMachineId = useSelector(
- (state: RootStore) => state.config.vscMachineId
+ // #endregion
+
+ // #region Selectors
+ const history = useSelector((state: RootStore) => state.serverState.history);
+ 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 [dataSwitchChecked, setDataSwitchChecked] = useState(false);
- const dataSwitchOn = useSelector(
- (state: RootStore) => state.config.dataSwitchOn
+ const selected_context_items = useSelector(
+ (state: RootStore) => state.serverState.selected_context_items
);
- useEffect(() => {
- if (typeof dataSwitchOn !== "undefined") {
- setDataSwitchChecked(dataSwitchOn);
- }
- }, [dataSwitchOn]);
+ // #endregion
+ // #region State
const [waitingForSteps, setWaitingForSteps] = useState(false);
- const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
- const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
- const [selectedContextItems, setSelectedContextItems] = useState<
- ContextItem[]
- >([]);
const [availableSlashCommands, setAvailableSlashCommands] = useState<
{ name: string; description: string }[]
>([]);
@@ -114,62 +72,33 @@ function GUI(props: GUIProps) {
true,
true,
]);
- const [history, setHistory] = useState<History | undefined>({
- timeline: [
- {
- step: {
- name: "Welcome to Continue",
- hide: false,
- description: `- Highlight code section and ask a question or give instructions
-- Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue
-- Use \`/help\` to ask questions about how to use Continue`,
- system_message: null,
- chat_context: [],
- manage_own_chat_context: false,
- message: "",
- },
- depth: 0,
- deleted: false,
- active: false,
- },
- ],
- current_index: 3,
- } as any);
-
- const vscMediaUrl = useSelector(
- (state: RootStore) => state.config.vscMediaUrl
- );
- const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
- const [feedbackDialogMessage, setFeedbackDialogMessage] = useState<
- string | JSX.Element
- >("");
- const [feedbackEntryOn, setFeedbackEntryOn] = useState(true);
+ const [waitingForClient, setWaitingForClient] = useState(true);
+ const [showLoading, setShowLoading] = useState(false);
- const dispatch = useDispatch();
- const bottomMessage = useSelector(
- (state: RootStore) => state.uiState.bottomMessage
- );
+ // #endregion
- const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] =
- useState<boolean>(true);
+ // #region Refs
const mainTextInputRef = useRef<HTMLInputElement>(null);
+ const topGuiDivRef = useRef<HTMLDivElement>(null);
+ // #endregion
- const aboveComboBoxDivRef = useRef<HTMLDivElement>(null);
+ // #region Effects
+ // Set displayBottomMessageOnBottom
+ const aboveComboBoxDivRef = useRef<HTMLDivElement>(null);
+ const bottomMessage = useSelector(
+ (state: RootStore) => state.uiState.bottomMessage
+ );
useEffect(() => {
if (!aboveComboBoxDivRef.current) return;
- if (
- aboveComboBoxDivRef.current.getBoundingClientRect().top >
- window.innerHeight / 2
- ) {
- setDisplayBottomMessageOnBottom(false);
- } else {
- setDisplayBottomMessageOnBottom(true);
- }
+ dispatch(
+ setDisplayBottomMessageOnBottom(
+ aboveComboBoxDivRef.current.getBoundingClientRect().top <
+ window.innerHeight / 2
+ )
+ );
}, [bottomMessage, aboveComboBoxDivRef.current]);
- const topGuiDivRef = useRef<HTMLDivElement>(null);
-
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
null
);
@@ -225,11 +154,9 @@ function GUI(props: GUIProps) {
state.history.current_index
].step.description?.trim() === "";
+ dispatch(setServerState(state));
+
setWaitingForSteps(waitingForSteps);
- setHistory(state.history);
- setSelectedContextItems(state.selected_context_items || []);
- setUserInputQueue(state.user_input_queue);
- setAddingHighlightedCode(state.adding_highlighted_code);
setAvailableSlashCommands(
state.slash_commands.map((c: any) => {
return {
@@ -260,15 +187,17 @@ function GUI(props: GUIProps) {
scrollToBottom();
}, [waitingForSteps]);
- const [waitingForClient, setWaitingForClient] = useState(true);
+ // #endregion
+
useEffect(() => {
if (client && waitingForClient) {
+ console.log("sending user input queue, ", user_input_queue);
setWaitingForClient(false);
- for (const input of userInputQueue) {
+ for (const input of user_input_queue) {
client.sendMainInput(input);
}
}
- }, [client, userInputQueue, waitingForClient]);
+ }, [client, user_input_queue, waitingForClient]);
const onMainTextInput = (event?: any) => {
if (mainTextInputRef.current) {
@@ -279,9 +208,11 @@ function GUI(props: GUIProps) {
}
(mainTextInputRef.current as any).setInputValue("");
if (!client) {
- setUserInputQueue((queue) => {
- return [...queue, input];
- });
+ dispatch(
+ temporarilySetUserInputQueue((queue: string[]) => {
+ return [...queue, input];
+ })
+ );
return;
}
@@ -310,9 +241,11 @@ function GUI(props: GUIProps) {
if (input.trim() === "") return;
client.sendMainInput(input);
- setUserInputQueue((queue) => {
- return [...queue, input];
- });
+ dispatch(
+ temporarilySetUserInputQueue((queue: string[]) => {
+ return [...queue, input];
+ })
+ );
// Increment localstorage counter
const counter = localStorage.getItem("mainTextEntryCounter");
@@ -323,62 +256,66 @@ function GUI(props: GUIProps) {
(currentCount + 1).toString()
);
if (currentCount === 25) {
- setFeedbackDialogMessage(
- <div className="text-center">
- 👋 Thanks for using Continue. We are a beta product and love
- working closely with our first users. If you're interested in
- speaking, enter your name and email. We won't use this information
- for anything other than reaching out.
- <br />
- <br />
- <form
- onSubmit={(e: any) => {
- e.preventDefault();
- posthog?.capture("user_interest_form", {
- name: e.target.elements[0].value,
- email: e.target.elements[1].value,
- });
- setFeedbackDialogMessage(
- <div className="text-center">
- Thanks! We'll be in touch soon.
- </div>
- );
- }}
- style={{
- display: "flex",
- flexDirection: "column",
- gap: "10px",
- }}
- >
- <input
- style={{ padding: "10px", borderRadius: "5px" }}
- type="text"
- name="name"
- placeholder="Name"
- required
- />
- <input
- style={{ padding: "10px", borderRadius: "5px" }}
- type="email"
- name="email"
- placeholder="Email"
- required
- />
- <button
+ dispatch(
+ setDialogMessage(
+ <div className="text-center">
+ 👋 Thanks for using Continue. We are a beta product and love
+ working closely with our first users. If you're interested in
+ speaking, enter your name and email. We won't use this
+ information for anything other than reaching out.
+ <br />
+ <br />
+ <form
+ onSubmit={(e: any) => {
+ e.preventDefault();
+ posthog?.capture("user_interest_form", {
+ name: e.target.elements[0].value,
+ email: e.target.elements[1].value,
+ });
+ dispatch(
+ setDialogMessage(
+ <div className="text-center">
+ Thanks! We'll be in touch soon.
+ </div>
+ )
+ );
+ }}
style={{
- padding: "10px",
- borderRadius: "5px",
- cursor: "pointer",
+ display: "flex",
+ flexDirection: "column",
+ gap: "10px",
}}
- type="submit"
>
- Submit
- </button>
- </form>
- </div>
+ <input
+ style={{ padding: "10px", borderRadius: "5px" }}
+ type="text"
+ name="name"
+ placeholder="Name"
+ required
+ />
+ <input
+ style={{ padding: "10px", borderRadius: "5px" }}
+ type="email"
+ name="email"
+ placeholder="Email"
+ required
+ />
+ <button
+ style={{
+ padding: "10px",
+ borderRadius: "5px",
+ cursor: "pointer",
+ }}
+ type="submit"
+ >
+ Submit
+ </button>
+ </form>
+ </div>
+ )
);
- setFeedbackEntryOn(false);
- setShowFeedbackDialog(true);
+ dispatch(setDialogEntryOn(false));
+ dispatch(setShowDialog(true));
}
} else {
localStorage.setItem("mainTextEntryCounter", "1");
@@ -391,7 +328,6 @@ function GUI(props: GUIProps) {
client.sendStepUserInput(input, index);
};
- const [showLoading, setShowLoading] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoading(true);
@@ -401,219 +337,121 @@ function GUI(props: GUIProps) {
clearTimeout(timeout);
};
}, []);
-
- // const iterations = useSelector(selectIterations);
return (
- <>
- <Onboarding />
- <TextDialog
- showDialog={showFeedbackDialog}
- onEnter={(text) => {
- client?.sendMainInput(`/feedback ${text}`);
- setShowFeedbackDialog(false);
- }}
- onClose={() => {
- setShowFeedbackDialog(false);
- }}
- message={feedbackDialogMessage}
- entryOn={feedbackEntryOn}
- />
-
- <TopGUIDiv
- ref={topGuiDivRef}
- onKeyDown={(e) => {
- if (e.key === "Enter" && e.ctrlKey) {
- onMainTextInput();
- }
- }}
- >
- {showLoading && typeof client === "undefined" && (
- <>
- <RingLoader />
- <p
- style={{
- textAlign: "center",
- margin: "0px",
- fontSize: "14px",
- }}
- >
- Continue Server Starting
- </p>
- <p
- style={{
- margin: "auto",
- textAlign: "center",
- marginTop: "4px",
- fontSize: "12px",
- cursor: "pointer",
- opacity: 0.7,
- }}
- onClick={() => {
- postVscMessage("toggleDevTools", {});
- }}
- >
- <u>Click to view logs</u>
- </p>
- <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.
- </div>
- </>
- )}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return node.step.name === "User Input" ? (
- node.step.hide || (
- <UserInputContainer
- 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);
- }}
+ <div
+ className="overflow-hidden"
+ ref={topGuiDivRef}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && e.ctrlKey) {
+ onMainTextInput();
+ }
+ }}
+ >
+ {showLoading && typeof client === "undefined" && (
+ <>
+ <RingLoader />
+ <p
+ style={{
+ textAlign: "center",
+ margin: "0px",
+ fontSize: "14px",
+ }}
+ >
+ Continue Server Starting
+ </p>
+ <p
+ style={{
+ margin: "auto",
+ textAlign: "center",
+ marginTop: "4px",
+ fontSize: "12px",
+ cursor: "pointer",
+ opacity: 0.7,
+ }}
+ onClick={() => {
+ postVscMessage("toggleDevTools", {});
+ }}
+ >
+ <u>Click to view logs</u>
+ </p>
+ <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.
+ </div>
+ </>
+ )}
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ return node.step.name === "User Input" ? (
+ node.step.hide || (
+ <UserInputContainer
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 />}
+
+ <div>
+ {user_input_queue?.map?.((input) => {
+ return <UserInputQueueItem>{input}</UserInputQueueItem>;
})}
- {waitingForSteps && <Loader></Loader>}
-
- <div>
- {userInputQueue.map((input) => {
- return <UserInputQueueItem>{input}</UserInputQueueItem>;
- })}
- </div>
-
- <div ref={aboveComboBoxDivRef} />
- <ComboBox
- ref={mainTextInputRef}
- onEnter={(e) => {
- onMainTextInput(e);
- e.stopPropagation();
- e.preventDefault();
- }}
- onInputValueChange={() => {}}
- items={availableSlashCommands}
- selectedContextItems={selectedContextItems}
- onToggleAddContext={() => {
- client?.toggleAddingHighlightedCode();
- }}
- addingHighlightedCode={addingHighlightedCode}
- />
- <ContinueButton onClick={onMainTextInput} />
- </TopGUIDiv>
- <BottomMessageDiv
- displayOnBottom={displayBottomMessageOnBottom}
- onMouseEnter={() => {
- dispatch(setBottomMessageCloseTimeout(undefined));
+ </div>
+
+ <div ref={aboveComboBoxDivRef} />
+ <ComboBox
+ ref={mainTextInputRef}
+ onEnter={(e) => {
+ onMainTextInput(e);
+ e.stopPropagation();
+ e.preventDefault();
}}
- onMouseLeave={(e) => {
- if (!e.buttons) {
- dispatch(setBottomMessage(undefined));
- }
+ onInputValueChange={() => {}}
+ items={availableSlashCommands}
+ selectedContextItems={selected_context_items}
+ onToggleAddContext={() => {
+ client?.toggleAddingHighlightedCode();
}}
- hidden={!bottomMessage}
- >
- {bottomMessage}
- </BottomMessageDiv>
- <Footer dataSwitchChecked={dataSwitchChecked}>
- {vscMediaUrl && (
- <a
- href="https://github.com/continuedev/continue"
- style={{ marginRight: "auto" }}
- >
- <img
- src={`${vscMediaUrl}/continue-dev-square.png`}
- width="22px"
- style={{ backgroundColor: "black", color: "red" }}
- />
- </a>
- )}
- <HeaderButtonWithText
- onClick={() => {
- // Show the dialog
- setFeedbackDialogMessage(
- `Continue uses GPT-4 by default, but works with any model. If you'd like to keep your code completely private, there are few options:
-
-Run a local model with ggml: [5 minute quickstart](https://github.com/continuedev/ggml-server-example)
-
-Use Azure OpenAI service, which is GDPR and HIPAA compliant: [Tutorial](https://continue.dev/docs/customization#azure-openai-service)
-
-If you already have an LLM deployed on your own infrastructure, or would like to do so, please contact us at hi@continue.dev.
- `
- );
- setFeedbackEntryOn(false);
- setShowFeedbackDialog(true);
- }}
- text={"Use Private Model"}
- >
- <div
- style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }}
- >
- 🔒
- </div>
- </HeaderButtonWithText>
- <HeaderButtonWithText
- onClick={() => {
- client?.sendClear();
- }}
- text="Clear"
- >
- <TrashIcon 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>
- <HeaderButtonWithText
- onClick={() => {
- // Set dialog open
- setFeedbackDialogMessage(
- "Having trouble using Continue? Want a new feature? Let us know! This box is anonymous, but we will promptly address your feedback."
- );
- setFeedbackEntryOn(true);
- setShowFeedbackDialog(true);
- }}
- text="Feedback"
- >
- <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </Footer>
- </>
+ addingHighlightedCode={adding_highlighted_code}
+ />
+ <ContinueButton onClick={onMainTextInput} />
+ </div>
);
}
diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx
new file mode 100644
index 00000000..edf4d39f
--- /dev/null
+++ b/extension/react-app/src/pages/history.tsx
@@ -0,0 +1,100 @@
+import React, { useContext, useEffect, useState } from "react";
+import { SessionInfo } from "../../../schema/SessionInfo";
+import { GUIClientContext } from "../App";
+import { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import { useNavigate } from "react-router-dom";
+import { secondaryDark, vscBackground } from "../components";
+import styled from "styled-components";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+
+const Tr = styled.tr`
+ &:hover {
+ background-color: ${secondaryDark};
+ }
+`;
+
+const TdDiv = styled.div`
+ cursor: pointer;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid ${secondaryDark};
+`;
+
+function History() {
+ const navigate = useNavigate();
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
+ const client = useContext(GUIClientContext);
+ const apiUrl = useSelector((state: RootStore) => state.config.apiUrl);
+
+ useEffect(() => {
+ const fetchSessions = async () => {
+ console.log("fetching sessions from: ", apiUrl);
+ if (!apiUrl) {
+ return;
+ }
+ const response = await fetch(`${apiUrl}/sessions/list`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const json = await response.json();
+ console.log(json);
+ setSessions(json);
+ };
+ fetchSessions();
+ }, [client]);
+
+ 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-2xl font-bold m-4 inline-block">History</h1>
+ </div>
+ <table className="w-full">
+ <tbody>
+ {sessions
+ .sort((a, b) => parseInt(b.date_created) - parseInt(a.date_created))
+ .map((session, index) => (
+ <Tr key={index}>
+ <td>
+ <TdDiv
+ onClick={() => {
+ client?.loadSession(session.session_id);
+ navigate("/");
+ }}
+ >
+ <div className="text-lg">{session.title}</div>
+ <div className="text-gray-400">
+ {new Date(
+ parseInt(session.date_created) * 1000
+ ).toLocaleString("en-US", {
+ weekday: "short",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ })}
+ </div>
+ </TdDiv>
+ </td>
+ </Tr>
+ ))}
+ </tbody>
+ </table>
+ <br />
+ <i className="text-sm ml-4">
+ All session data is saved in ~/.continue/sessions
+ </i>
+ </div>
+ );
+}
+
+export default History;
diff --git a/extension/react-app/src/redux/hooks.ts b/extension/react-app/src/redux/hooks.ts
deleted file mode 100644
index a6aef869..00000000
--- a/extension/react-app/src/redux/hooks.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useCallback } from "react";
-import { useDispatch, useSelector } from "react-redux";
-import { RootStore } from "./store";
-import { selectDebugContextValue } from "./selectors/debugContextSelectors";
-import { updateValue } from "./slices/debugContexSlice";
-import { SerializedDebugContext } from "../../../src/client";
-
-export function useDebugContextValue(
- key: keyof SerializedDebugContext,
- defaultValue: any
-): [any, (value: any) => void] {
- const dispatch = useDispatch();
- const state =
- useSelector((state: RootStore) => selectDebugContextValue(state, key)) ||
- defaultValue;
- const boundAction = useCallback(
- (value: any) => dispatch(updateValue({ key, value })),
- [dispatch, key]
- );
- return [state, boundAction];
-}
diff --git a/extension/react-app/src/redux/selectors/debugContextSelectors.ts b/extension/react-app/src/redux/selectors/debugContextSelectors.ts
deleted file mode 100644
index 89201bb7..00000000
--- a/extension/react-app/src/redux/selectors/debugContextSelectors.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { RootStore } from "../store";
-
-const selectDebugContext = (state: RootStore) => {
- return {
- ...state.debugState.debugContext,
- rangesInFiles: state.debugState.debugContext.rangesInFiles.filter(
- (_, index) => state.debugState.rangesMask[index]
- ),
- };
-};
-
-const selectAllRangesInFiles = (state: RootStore) => {
- return state.debugState.debugContext.rangesInFiles;
-};
-
-const selectRangesMask = (state: RootStore) => {
- return state.debugState.rangesMask;
-};
-
-const selectDebugContextValue = (state: RootStore, key: string) => {
- return (state.debugState.debugContext as any)[key];
-};
-
-export {
- selectDebugContext,
- selectDebugContextValue,
- selectAllRangesInFiles,
- selectRangesMask,
-};
diff --git a/extension/react-app/src/redux/slices/configSlice.ts b/extension/react-app/src/redux/slices/configSlice.ts
index 57c4f860..59c76066 100644
--- a/extension/react-app/src/redux/slices/configSlice.ts
+++ b/extension/react-app/src/redux/slices/configSlice.ts
@@ -50,7 +50,7 @@ export const configSlice = createSlice({
) => ({
...state,
dataSwitchOn: action.payload,
- })
+ }),
},
});
@@ -60,6 +60,6 @@ export const {
setWorkspacePath,
setSessionId,
setVscMediaUrl,
- setDataSwitchOn
+ setDataSwitchOn,
} = configSlice.actions;
export default configSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/debugContexSlice.ts b/extension/react-app/src/redux/slices/debugContexSlice.ts
deleted file mode 100644
index 647440d5..00000000
--- a/extension/react-app/src/redux/slices/debugContexSlice.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-import { RangeInFile, SerializedDebugContext } from "../../../../src/client";
-import { RootStore } from "../store";
-
-export const debugStateSlice = createSlice({
- name: "debugState",
- initialState: {
- debugContext: {
- rangesInFiles: [],
- filesystem: {},
- traceback: undefined,
- description: undefined,
- },
- rangesMask: [],
- } as RootStore["debugState"],
- reducers: {
- updateValue: (
- state: RootStore["debugState"],
- action: {
- type: string;
- payload: { key: keyof SerializedDebugContext; value: any };
- }
- ) => {
- return {
- ...state,
- debugContext: {
- ...state.debugContext,
- [action.payload.key]: action.payload.value,
- },
- };
- },
- addRangeInFile: (
- state: RootStore["debugState"],
- action: {
- type: string;
- payload: {
- rangeInFile: RangeInFile;
- canUpdateLast: boolean;
- };
- }
- ) => {
- let rangesInFiles = state.debugContext.rangesInFiles;
- // If identical to existing range, don't add. Ideally you check for overlap of ranges.
- for (let range of rangesInFiles) {
- if (
- range.filepath === action.payload.rangeInFile.filepath &&
- range.range.start.line ===
- action.payload.rangeInFile.range.start.line &&
- range.range.end.line === action.payload.rangeInFile.range.end.line
- ) {
- return state;
- }
- }
-
- if (
- action.payload.canUpdateLast &&
- rangesInFiles.length > 0 &&
- rangesInFiles[rangesInFiles.length - 1].filepath ===
- action.payload.rangeInFile.filepath
- ) {
- return {
- ...state,
- debugContext: {
- ...state.debugContext,
- rangesInFiles: [
- ...rangesInFiles.slice(0, rangesInFiles.length - 1),
- action.payload.rangeInFile,
- ],
- },
- };
- } else {
- return {
- ...state,
- debugContext: {
- ...state.debugContext,
- rangesInFiles: [
- ...state.debugContext.rangesInFiles,
- action.payload.rangeInFile,
- ],
- },
- rangesMask: [...state.rangesMask, true],
- };
- }
- },
- deleteRangeInFileAt: (
- state: RootStore["debugState"],
- action: {
- type: string;
- payload: number;
- }
- ) => {
- return {
- ...state,
- debugContext: {
- ...state.debugContext,
- rangesInFiles: state.debugContext.rangesInFiles.filter(
- (_, index) => index !== action.payload
- ),
- },
- rangesMask: state.rangesMask.filter(
- (_, index) => index !== action.payload
- ),
- };
- },
- toggleSelectionAt: (
- state: RootStore["debugState"],
- action: {
- type: string;
- payload: number;
- }
- ) => {
- return {
- ...state,
- rangesMask: state.rangesMask.map((_, index) =>
- index === action.payload
- ? !state.rangesMask[index]
- : state.rangesMask[index]
- ),
- };
- },
- updateFileSystem: (
- state: RootStore["debugState"],
- action: {
- type: string;
- payload: { [filepath: string]: string };
- }
- ) => {
- return {
- ...state,
- debugContext: {
- ...state.debugContext,
- filesystem: {
- ...state.debugContext.filesystem,
- ...action.payload,
- },
- },
- };
- },
- },
-});
-
-export const {
- updateValue,
- updateFileSystem,
- addRangeInFile,
- deleteRangeInFileAt,
- toggleSelectionAt,
-} = debugStateSlice.actions;
-export default debugStateSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts
new file mode 100644
index 00000000..4d9dc326
--- /dev/null
+++ b/extension/react-app/src/redux/slices/serverStateReducer.ts
@@ -0,0 +1,53 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { FullState } from "../../../../schema/FullState";
+
+const initialState: FullState = {
+ history: {
+ timeline: [
+ {
+ step: {
+ name: "Welcome to Continue",
+ hide: false,
+ description: `- Highlight code section and ask a question or give instructions
+ - Use \`cmd+m\` (Mac) / \`ctrl+m\` (Windows) to open Continue
+ - Use \`/help\` to ask questions about how to use Continue`,
+ system_message: null,
+ chat_context: [],
+ manage_own_chat_context: false,
+ message: "",
+ },
+ depth: 0,
+ deleted: false,
+ active: false,
+ },
+ ],
+ current_index: 3,
+ } as any,
+ user_input_queue: [],
+ active: false,
+ slash_commands: [],
+ adding_highlighted_code: false,
+ selected_context_items: [],
+};
+
+export const serverStateSlice = createSlice({
+ name: "serverState",
+ initialState,
+ reducers: {
+ setServerState: (state, action) => {
+ return {
+ ...action.payload,
+ selected_context_items: action.payload.selected_context_items || [],
+ user_input_queue: action.payload.user_input_queue || [],
+ slash_commands: action.payload.slash_commands || [],
+ };
+ },
+ temporarilySetUserInputQueue: (state, action) => {
+ state.user_input_queue = action.payload;
+ },
+ },
+});
+
+export const { setServerState, temporarilySetUserInputQueue } =
+ serverStateSlice.actions;
+export default serverStateSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts
index 837d19e9..d34596c9 100644
--- a/extension/react-app/src/redux/slices/uiStateSlice.ts
+++ b/extension/react-app/src/redux/slices/uiStateSlice.ts
@@ -5,6 +5,10 @@ export const uiStateSlice = createSlice({
initialState: {
bottomMessage: undefined,
bottomMessageCloseTimeout: undefined,
+ showDialog: false,
+ dialogMessage: "",
+ dialogEntryOn: false,
+ displayBottomMessageOnBottom: true,
},
reducers: {
setBottomMessage: (state, action) => {
@@ -16,9 +20,27 @@ export const uiStateSlice = createSlice({
}
state.bottomMessageCloseTimeout = action.payload;
},
+ setDialogMessage: (state, action) => {
+ state.dialogMessage = action.payload;
+ },
+ setDialogEntryOn: (state, action) => {
+ state.dialogEntryOn = action.payload;
+ },
+ setShowDialog: (state, action) => {
+ state.showDialog = action.payload;
+ },
+ setDisplayBottomMessageOnBottom: (state, action) => {
+ state.displayBottomMessageOnBottom = action.payload;
+ },
},
});
-export const { setBottomMessage, setBottomMessageCloseTimeout } =
- uiStateSlice.actions;
+export const {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+ setDialogMessage,
+ setDialogEntryOn,
+ setShowDialog,
+ setDisplayBottomMessageOnBottom,
+} = uiStateSlice.actions;
export default uiStateSlice.reducer;
diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts
index d49513e5..59339060 100644
--- a/extension/react-app/src/redux/store.ts
+++ b/extension/react-app/src/redux/store.ts
@@ -1,10 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
-import debugStateReducer from "./slices/debugContexSlice";
import chatReducer from "./slices/chatSlice";
import configReducer from "./slices/configSlice";
import miscReducer from "./slices/miscSlice";
import uiStateReducer from "./slices/uiStateSlice";
-import { RangeInFile, SerializedDebugContext } from "../../../src/client";
+import { RangeInFile } from "../../../src/client";
+import { FullState } from "../../../schema/FullState";
+import serverStateReducer from "./slices/serverStateReducer";
export interface ChatMessage {
role: "system" | "user" | "assistant";
@@ -12,10 +13,6 @@ export interface ChatMessage {
}
export interface RootStore {
- debugState: {
- debugContext: SerializedDebugContext;
- rangesMask: boolean[];
- };
config: {
workspacePath: string | undefined;
apiUrl: string;
@@ -35,16 +32,21 @@ export interface RootStore {
uiState: {
bottomMessage: JSX.Element | undefined;
bottomMessageCloseTimeout: NodeJS.Timeout | undefined;
+ displayBottomMessageOnBottom: boolean;
+ showDialog: boolean;
+ dialogMessage: string | JSX.Element;
+ dialogEntryOn: boolean;
};
+ serverState: FullState;
}
const store = configureStore({
reducer: {
- debugState: debugStateReducer,
chat: chatReducer,
config: configReducer,
misc: miscReducer,
uiState: uiStateReducer,
+ serverState: serverStateReducer,
},
});
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
index 0095f41b..2938e012 100644
--- a/extension/schema/FullState.d.ts
+++ b/extension/schema/FullState.d.ts
@@ -26,7 +26,6 @@ export type Timeline = HistoryNode[];
export type CurrentIndex = number;
export type Active1 = boolean;
export type UserInputQueue = string[];
-export type DefaultModel = string;
export type Name3 = string;
export type Description1 = string;
export type SlashCommands = SlashCommandDescription[];
@@ -47,7 +46,6 @@ export interface FullState1 {
history: History;
active: Active1;
user_input_queue: UserInputQueue;
- default_model: DefaultModel;
slash_commands: SlashCommands;
adding_highlighted_code: AddingHighlightedCode;
selected_context_items: SelectedContextItems;
diff --git a/extension/schema/SessionInfo.d.ts b/extension/schema/SessionInfo.d.ts
new file mode 100644
index 00000000..ac94e76d
--- /dev/null
+++ b/extension/schema/SessionInfo.d.ts
@@ -0,0 +1,18 @@
+/* eslint-disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type SessionInfo = SessionInfo1;
+export type SessionId = string;
+export type Title = string;
+export type DateCreated = string;
+
+export interface SessionInfo1 {
+ session_id: SessionId;
+ title: Title;
+ date_created: DateCreated;
+ [k: string]: unknown;
+}
diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts
index c8e8b85f..81d58afe 100644
--- a/extension/src/activation/environmentSetup.ts
+++ b/extension/src/activation/environmentSetup.ts
@@ -249,6 +249,7 @@ export async function startContinuePythonServer() {
});
child.unref();
} catch (e: any) {
+ console.log("Error starting server:", e);
retry();
}
};
diff --git a/extension/src/commands.ts b/extension/src/commands.ts
index cf6892f2..ea12699e 100644
--- a/extension/src/commands.ts
+++ b/extension/src/commands.ts
@@ -1,17 +1,8 @@
import * as vscode from "vscode";
import * as path from "path";
import * as os from "os";
-import {
- acceptSuggestionCommand,
- rejectSuggestionCommand,
- suggestionDownCommand,
- suggestionUpCommand,
- acceptAllSuggestionsCommand,
- rejectAllSuggestionsCommand,
-} from "./suggestions";
import { acceptDiffCommand, rejectDiffCommand } from "./diffs";
-import * as bridge from "./bridge";
import { debugPanelWebview } from "./debugPanel";
import { ideProtocolClient } from "./activation/activate";
diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts
index b687c3e4..d133080b 100644
--- a/extension/src/debugPanel.ts
+++ b/extension/src/debugPanel.ts
@@ -221,6 +221,15 @@ export function setupDebugPanel(
}
break;
}
+ case "websocketForwardingClose": {
+ let url = data.url;
+ let connection = websocketConnections[url];
+ if (typeof connection !== "undefined") {
+ connection.close();
+ websocketConnections[url] = undefined;
+ }
+ break;
+ }
case "websocketForwardingMessage": {
let url = data.url;
let connection = websocketConnections[url];
diff --git a/extension/src/util/messenger.ts b/extension/src/util/messenger.ts
index bcc88fe1..152d4a1f 100644
--- a/extension/src/util/messenger.ts
+++ b/extension/src/util/messenger.ts
@@ -18,6 +18,8 @@ export abstract class Messenger {
abstract onError(callback: () => void): void;
abstract sendAndReceive(messageType: string, data: any): Promise<any>;
+
+ abstract close(): void;
}
export class WebsocketMessenger extends Messenger {
@@ -160,4 +162,8 @@ export class WebsocketMessenger extends Messenger {
onError(callback: () => void): void {
this.websocket.addEventListener("error", callback);
}
+
+ close(): void {
+ this.websocket.close();
+ }
}
diff --git a/schema/json/FullState.json b/schema/json/FullState.json
index 7b6dfd6b..21db438a 100644
--- a/schema/json/FullState.json
+++ b/schema/json/FullState.json
@@ -264,10 +264,6 @@
"type": "string"
}
},
- "default_model": {
- "title": "Default Model",
- "type": "string"
- },
"slash_commands": {
"title": "Slash Commands",
"type": "array",
@@ -291,7 +287,6 @@
"history",
"active",
"user_input_queue",
- "default_model",
"slash_commands",
"adding_highlighted_code",
"selected_context_items"
diff --git a/schema/json/SessionInfo.json b/schema/json/SessionInfo.json
new file mode 100644
index 00000000..5857a724
--- /dev/null
+++ b/schema/json/SessionInfo.json
@@ -0,0 +1,29 @@
+{
+ "title": "SessionInfo",
+ "$ref": "#/definitions/src__continuedev__core__main__SessionInfo",
+ "definitions": {
+ "src__continuedev__core__main__SessionInfo": {
+ "title": "SessionInfo",
+ "type": "object",
+ "properties": {
+ "session_id": {
+ "title": "Session Id",
+ "type": "string"
+ },
+ "title": {
+ "title": "Title",
+ "type": "string"
+ },
+ "date_created": {
+ "title": "Date Created",
+ "type": "string"
+ }
+ },
+ "required": [
+ "session_id",
+ "title",
+ "date_created"
+ ]
+ }
+ }
+} \ No newline at end of file