From 122b63daeec6cb35d401eccd660759e1745f6778 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Tue, 13 Jun 2023 22:56:29 -0700
Subject: feedback dialog

---
 extension/react-app/src/components/TextDialog.tsx | 56 +++++++++++++++++++++++
 1 file changed, 56 insertions(+)
 create mode 100644 extension/react-app/src/components/TextDialog.tsx

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
new file mode 100644
index 00000000..6b335e00
--- /dev/null
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -0,0 +1,56 @@
+// Write a component that displays a dialog box with a text field and a button.
+import React, { useState } from "react";
+import styled from "styled-components";
+import { Button, buttonColor, secondaryDark, vscBackground } from ".";
+
+const DialogContainer = styled.div`
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+`;
+
+const Dialog = styled.div`
+  background-color: white;
+  border-radius: 8px;
+  padding: 8px;
+  display: flex;
+  flex-direction: column;
+  /* box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.5); */
+  border: 2px solid ${buttonColor};
+  width: fit-content;
+  margin: auto;
+`;
+
+const TextArea = styled.textarea`
+  border: 1px solid #ccc;
+  border-radius: 8px;
+  padding: 8px;
+  outline: 1px solid black;
+  font-family: Arial, Helvetica, sans-serif;
+`;
+
+const P = styled.p`
+  color: black;
+  margin: 8px auto;
+`;
+
+const TextDialog = (props: {
+  showDialog: boolean;
+  onEnter: (text: string) => void;
+}) => {
+  const [text, setText] = useState("");
+  const textAreaRef = React.createRef<HTMLTextAreaElement>();
+
+  return (
+    <DialogContainer hidden={!props.showDialog}>
+      <Dialog>
+        <P>Thanks for your feedback. We'll get back to you soon!</P>
+        <TextArea cols={50} rows={10} ref={textAreaRef}></TextArea>
+        <Button onClick={() => props.onEnter(text)}>Enter</Button>
+      </Dialog>
+    </DialogContainer>
+  );
+};
+
+export default TextDialog;
-- 
cgit v1.2.3-70-g09d2


From 9100f116d0c28b231c0b746f60b8d59e00546456 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Tue, 13 Jun 2023 23:45:49 -0700
Subject: Small fixes

---
 .../src/continuedev/recipes/CreatePipelineRecipe/steps.py      |  2 +-
 continuedev/src/continuedev/steps/feedback.py                  |  6 ++++++
 extension/react-app/src/components/TextDialog.tsx              | 10 +++++++++-
 extension/src/continueIdeClient.ts                             |  6 +++++-
 4 files changed, 21 insertions(+), 3 deletions(-)

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
index 88e27d2a..096b41c6 100644
--- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
+++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py
@@ -50,7 +50,7 @@ class SetupPipelineStep(Step):
 
         # editing the resource function to call the requested API
         resource_function_range = Range.from_shorthand(15, 0, 29, 0)
-        await sdk.ide.highlightCode(RangeInFile(filepath=os.path.join(await sdk.ide.getWorkspaceDirectory(), filename), range=resource_function_range))
+        await sdk.ide.highlightCode(RangeInFile(filepath=os.path.join(await sdk.ide.getWorkspaceDirectory(), filename), range=resource_function_range), "#ffa50033")
 
         # sdk.set_loading_message("Writing code to call the API...")
         await sdk.edit_file(
diff --git a/continuedev/src/continuedev/steps/feedback.py b/continuedev/src/continuedev/steps/feedback.py
index bf119ab0..96081756 100644
--- a/continuedev/src/continuedev/steps/feedback.py
+++ b/continuedev/src/continuedev/steps/feedback.py
@@ -1,3 +1,5 @@
+from typing import Coroutine
+from ..core.main import Models
 from ..core.main import Step
 from ..core.sdk import ContinueSDK
 from ..libs.util.telemetry import capture_event
@@ -5,6 +7,10 @@ from ..libs.util.telemetry import capture_event
 
 class FeedbackStep(Step):
     user_input: str
+    name = "Thanks for your feedback!"
+
+    async def describe(self, models: Models):
+        return f"`{self.user_input}`\n\nWe'll see your feedback and make improvements as soon as possible. If you'd like to directly email us, you can send an email to [nate@continue.dev](mailto:nate@continue.dev?subject=Feedback%20On%20Continue)."
 
     async def run(self, sdk: ContinueSDK):
         capture_event("feedback", {"feedback": self.user_input})
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 6b335e00..167e12cf 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -47,7 +47,15 @@ const TextDialog = (props: {
       <Dialog>
         <P>Thanks for your feedback. We'll get back to you soon!</P>
         <TextArea cols={50} rows={10} ref={textAreaRef}></TextArea>
-        <Button onClick={() => props.onEnter(text)}>Enter</Button>
+        <Button
+          onClick={() => {
+            if (textAreaRef.current) {
+              props.onEnter(textAreaRef.current.value);
+            }
+          }}
+        >
+          Enter
+        </Button>
       </Dialog>
     </DialogContainer>
   );
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index 782219dc..3308068a 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -121,7 +121,11 @@ class IdeProtocolClient {
     }
   }
   getWorkspaceDirectory() {
-    return vscode.workspace.workspaceFolders![0].uri.fsPath;
+    if (!vscode.workspace.workspaceFolders) {
+      // Return the home directory
+      return process.env.HOME || process.env.USERPROFILE || "/";
+    }
+    return vscode.workspace.workspaceFolders[0].uri.fsPath;
   }
 
   // ------------------------------------ //
-- 
cgit v1.2.3-70-g09d2


From 5a6fa9ae784c5942f1e37c7a0ae3acdd6337c459 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Fri, 16 Jun 2023 23:42:10 -0700
Subject: polishing

---
 continuedev/src/continuedev/core/config.py         | 15 ++++++++++--
 continuedev/src/continuedev/core/sdk.py            |  3 +++
 .../src/continuedev/libs/llm/proxy_server.py       |  2 +-
 .../continuedev/libs/util/step_name_to_steps.py    |  2 ++
 continuedev/src/continuedev/steps/clear_history.py | 10 ++++++++
 continuedev/src/continuedev/steps/core/core.py     |  2 ++
 extension/package-lock.json                        |  4 ++--
 extension/package.json                             |  8 +++----
 extension/react-app/src/components/ComboBox.tsx    | 21 ++++++++++++++++
 .../src/components/HeaderButtonWithText.tsx        |  4 +---
 .../react-app/src/components/StepContainer.tsx     |  1 -
 extension/react-app/src/components/TextDialog.tsx  | 28 ++++++++++++++++++++--
 extension/react-app/src/tabs/gui.tsx               |  3 +++
 extension/src/commands.ts                          |  5 +---
 extension/src/continueIdeClient.ts                 |  2 +-
 15 files changed, 90 insertions(+), 20 deletions(-)
 create mode 100644 continuedev/src/continuedev/steps/clear_history.py

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/core/config.py b/continuedev/src/continuedev/core/config.py
index 8f703758..1ee3a7f8 100644
--- a/continuedev/src/continuedev/core/config.py
+++ b/continuedev/src/continuedev/core/config.py
@@ -52,6 +52,11 @@ class ContinueConfig(BaseModel):
             name="feedback",
             description="Send feedback to improve Continue",
             step_name="FeedbackStep",
+        ),
+        SlashCommand(
+            name="clear",
+            description="Clear step history",
+            step_name="ClearHistoryStep",
         )
     ]
     on_traceback: Optional[List[OnTracebackSteps]] = [
@@ -68,10 +73,16 @@ def load_config(config_file: str) -> ContinueConfig:
     _, ext = os.path.splitext(config_file)
     if ext == '.yaml':
         with open(config_file, 'r') as f:
-            config_dict = yaml.safe_load(f)
+            try:
+                config_dict = yaml.safe_load(f)
+            except:
+                return ContinueConfig()
     elif ext == '.json':
         with open(config_file, 'r') as f:
-            config_dict = json.load(f)
+            try:
+                config_dict = json.load(f)
+            except:
+                return ContinueConfig()
     else:
         raise ValueError(f'Unknown config file extension: {ext}')
     return ContinueConfig(**config_dict)
diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py
index 7639d010..4ab2f027 100644
--- a/continuedev/src/continuedev/core/sdk.py
+++ b/continuedev/src/continuedev/core/sdk.py
@@ -192,3 +192,6 @@ class ContinueSDK(AbstractContinueSDK):
 
     async def update_ui(self):
         await self.__autopilot.update_subscribers()
+
+    async def clear_history(self):
+        await self.__autopilot.clear_history()
diff --git a/continuedev/src/continuedev/libs/llm/proxy_server.py b/continuedev/src/continuedev/libs/llm/proxy_server.py
index 93f2d48a..dd0d3aca 100644
--- a/continuedev/src/continuedev/libs/llm/proxy_server.py
+++ b/continuedev/src/continuedev/libs/llm/proxy_server.py
@@ -18,7 +18,7 @@ CHAT_MODELS = {
 }
 
 # SERVER_URL = "http://127.0.0.1:8080"
-SERVER_URL = "https://proxy-server-l6vsfbzhba-uc.a.run.app"
+SERVER_URL = "https://proxy-server-l6vsfbzhba-uw.a.run.app"
 
 
 class ProxyServer(LLM):
diff --git a/continuedev/src/continuedev/libs/util/step_name_to_steps.py b/continuedev/src/continuedev/libs/util/step_name_to_steps.py
index 2c4474af..f431f317 100644
--- a/continuedev/src/continuedev/libs/util/step_name_to_steps.py
+++ b/continuedev/src/continuedev/libs/util/step_name_to_steps.py
@@ -11,6 +11,7 @@ from ...recipes.CreatePipelineRecipe.main import CreatePipelineRecipe
 from ...recipes.DDtoBQRecipe.main import DDtoBQRecipe
 from ...recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowRecipe
 from ...steps.on_traceback import DefaultOnTracebackStep
+from ...steps.clear_history import ClearHistoryStep
 
 # This mapping is used to convert from string in ContinueConfig json to corresponding Step class.
 # Used for example in slash_commands and steps_on_startup
@@ -25,6 +26,7 @@ step_name_to_step_class = {
     "DDtoBQRecipe": DDtoBQRecipe,
     "DeployPipelineAirflowRecipe": DeployPipelineAirflowRecipe,
     "DefaultOnTracebackStep": DefaultOnTracebackStep,
+    "ClearHistoryStep": ClearHistoryStep,
 }
 
 
diff --git a/continuedev/src/continuedev/steps/clear_history.py b/continuedev/src/continuedev/steps/clear_history.py
new file mode 100644
index 00000000..a875c6d3
--- /dev/null
+++ b/continuedev/src/continuedev/steps/clear_history.py
@@ -0,0 +1,10 @@
+from ..core.main import Step
+from ..core.sdk import ContinueSDK
+
+
+class ClearHistoryStep(Step):
+    name: str = "Clear History"
+    hide: bool = True
+
+    async def run(self, sdk: ContinueSDK):
+        await sdk.clear_history()
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index 59af5f38..330f60ed 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -186,6 +186,8 @@ class DefaultModelEditCodeStep(Step):
             completion = completion.removesuffix(eot_token)
 
             # Remove tags and If it accidentally includes prefix or suffix, remove it
+            if completion.strip().startswith("```"):
+                completion = completion.strip().removeprefix("```").removesuffix("```")
             completion = completion.replace("<file_prefix>", "").replace("<file_suffix>", "").replace(
                 "<commit_before>", "").replace("<commit_msg>", "").replace("<commit_after>", "")
             completion = completion.removeprefix(segs[0])
diff --git a/extension/package-lock.json b/extension/package-lock.json
index 86c816e0..92df6a4f 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "continue",
-  "version": "0.0.47",
+  "version": "0.0.50",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "continue",
-      "version": "0.0.47",
+      "version": "0.0.50",
       "license": "Apache-2.0",
       "dependencies": {
         "@electron/rebuild": "^3.2.10",
diff --git a/extension/package.json b/extension/package.json
index 3a3927f3..a875c82d 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -14,7 +14,7 @@
   "displayName": "Continue",
   "pricing": "Free",
   "description": "Refine code 10x faster",
-  "version": "0.0.47",
+  "version": "0.0.50",
   "publisher": "Continue",
   "engines": {
     "vscode": "^1.74.0"
@@ -86,8 +86,8 @@
       "activitybar": [
         {
           "id": "continue",
-          "title": "Continue",
-          "icon": "media/continue-gradient.png"
+          "title": "Continue  ",
+          "icon": "react-app/dist/continue_arrow.png"
         }
       ]
     },
@@ -96,7 +96,7 @@
         {
           "type": "webview",
           "id": "continue.continueGUIView",
-          "name": ")",
+          "name": "  )",
           "visibility": "visible"
         }
       ]
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 2b140567..29f8a53b 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -68,6 +68,9 @@ interface ComboBoxProps {
 }
 
 const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+  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 {
     isOpen,
@@ -113,9 +116,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
               (event.nativeEvent as any).preventDownshiftDefault = true;
               if (props.onEnter) props.onEnter(event);
               setInputValue("");
+              const value = event.currentTarget.value;
+              if (value !== "") {
+                setPositionInHistory(history.length + 1);
+                setHistory([...history, value]);
+              }
             } else if (event.key === "Tab" && items.length > 0) {
               setInputValue(items[0].name);
               event.preventDefault();
+            } else if (event.key === "ArrowUp" && !isOpen) {
+              if (positionInHistory == 0) return;
+              setInputValue(history[positionInHistory - 1]);
+              setPositionInHistory((prev) => prev - 1);
+            } else if (event.key === "ArrowDown" && !isOpen) {
+              if (positionInHistory >= history.length - 1) {
+                setInputValue("");
+              } else {
+                setInputValue(history[positionInHistory + 1]);
+              }
+              setPositionInHistory((prev) =>
+                Math.min(prev + 1, history.length)
+              );
             }
           },
           ref: ref as any,
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index c4f22211..f9483f0f 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -15,9 +15,7 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
       style={{ padding: "3px" }}
       onMouseEnter={() => setHover(true)}
       onMouseLeave={() => {
-        setTimeout(() => {
-          setHover(false);
-        }, 100);
+        setHover(false);
       }}
       onClick={props.onClick}
     >
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 480f517f..7adbd7c2 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -63,7 +63,6 @@ const HeaderDiv = styled.div<{ error: boolean }>`
 const ContentDiv = styled.div`
   padding: 8px;
   padding-left: 16px;
-  background-color: ${vscBackground};
 `;
 
 const OnHoverDiv = styled.div`
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 167e12cf..31a385d5 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -1,5 +1,5 @@
 // Write a component that displays a dialog box with a text field and a button.
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import { Button, buttonColor, secondaryDark, vscBackground } from ".";
 
@@ -28,6 +28,10 @@ const TextArea = styled.textarea`
   padding: 8px;
   outline: 1px solid black;
   font-family: Arial, Helvetica, sans-serif;
+
+  &:focus {
+    outline: 1px solid orange;
+  }
 `;
 
 const P = styled.p`
@@ -38,19 +42,39 @@ const P = styled.p`
 const TextDialog = (props: {
   showDialog: boolean;
   onEnter: (text: string) => void;
+  onClose: () => void;
 }) => {
   const [text, setText] = useState("");
   const textAreaRef = React.createRef<HTMLTextAreaElement>();
 
+  useEffect(() => {
+    if (textAreaRef.current) {
+      textAreaRef.current.focus();
+    }
+  }, [props.showDialog]);
+
   return (
     <DialogContainer hidden={!props.showDialog}>
       <Dialog>
         <P>Thanks for your feedback. We'll get back to you soon!</P>
-        <TextArea cols={50} rows={10} ref={textAreaRef}></TextArea>
+        <TextArea
+          cols={50}
+          rows={10}
+          ref={textAreaRef}
+          onKeyDown={(e) => {
+            if (e.key === "Enter" && e.metaKey && textAreaRef.current) {
+              props.onEnter(textAreaRef.current.value);
+              setText("");
+            } else if (e.key === "Escape") {
+              props.onClose();
+            }
+          }}
+        />
         <Button
           onClick={() => {
             if (textAreaRef.current) {
               props.onEnter(textAreaRef.current.value);
+              setText("");
             }
           }}
         >
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index 994cb896..63da595f 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -297,6 +297,9 @@ function GUI(props: GUIProps) {
           client?.sendMainInput(`/feedback ${text}`);
           setShowFeedbackDialog(false);
         }}
+        onClose={() => {
+          setShowFeedbackDialog(false);
+        }}
       ></TextDialog>
 
       <TopGUIDiv
diff --git a/extension/src/commands.ts b/extension/src/commands.ts
index c98cd3c3..5392a7a3 100644
--- a/extension/src/commands.ts
+++ b/extension/src/commands.ts
@@ -61,12 +61,9 @@ const commandsMap: { [command: string]: (...args: any) => any } = {
   "continue.suggestionUp": suggestionUpCommand,
   "continue.acceptSuggestion": acceptSuggestionCommand,
   "continue.rejectSuggestion": rejectSuggestionCommand,
-  "continue.openContinueGUI": () => {
-    ideProtocolClient.openGUI();
-  },
   "continue.focusContinueInput": async () => {
     if (!debugPanelWebview) {
-      await ideProtocolClient.openGUI();
+      vscode.commands.executeCommand("continue.continueGUIView.focus");
     }
     debugPanelWebview?.postMessage({
       type: "focusContinueInput",
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index 9a93a4ef..b4937ac4 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -192,7 +192,7 @@ class IdeProtocolClient {
   async getUserSecret(key: string) {
     // Check if secret already exists in VS Code settings (global)
     let secret = vscode.workspace.getConfiguration("continue").get(key);
-    if (typeof secret !== "undefined") return secret;
+    if (typeof secret !== "undefined" && secret !== null) return secret;
 
     // If not, ask user for secret
     secret = await vscode.window.showInputBox({
-- 
cgit v1.2.3-70-g09d2


From f197e701a21e40f65b17ba2a5a25bb0cda7003f2 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 17 Jun 2023 12:06:25 -0700
Subject: more ui details

---
 extension/react-app/src/components/CodeBlock.tsx   |  63 +++++++----
 extension/react-app/src/components/ComboBox.tsx    |  22 +++-
 .../react-app/src/components/StepContainer.tsx     | 118 +++++++++------------
 extension/react-app/src/components/TextDialog.tsx  |  77 +++++++++-----
 extension/react-app/src/components/index.ts        |  20 ----
 extension/react-app/src/index.css                  |   5 +-
 extension/react-app/src/tabs/gui.tsx               |   2 +
 7 files changed, 164 insertions(+), 143 deletions(-)

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx
index eedae3fb..c4524a51 100644
--- a/extension/react-app/src/components/CodeBlock.tsx
+++ b/extension/react-app/src/components/CodeBlock.tsx
@@ -1,44 +1,62 @@
 import hljs from "highlight.js";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import styled from "styled-components";
-import { defaultBorderRadius, vscBackground } from ".";
+import { defaultBorderRadius, secondaryDark, vscBackground } from ".";
 
-import { Clipboard } from "@styled-icons/heroicons-outline";
+import { Clipboard, CheckCircle } from "@styled-icons/heroicons-outline";
 
 const StyledPre = styled.pre`
   overflow-y: scroll;
   word-wrap: normal;
-  border: 1px solid gray;
+  border: 0.5px solid gray;
   border-radius: ${defaultBorderRadius};
-  background-color: ${vscBackground};
+  background-color: ${secondaryDark};
   padding: 8px;
+  padding-top: 14px;
+  padding-bottom: 16px;
 `;
 
-const StyledCode = styled.code`
-  background-color: ${vscBackground};
-`;
+const StyledCode = styled.code``;
 
-const StyledCopyButton = styled.button`
+const StyledCopyButton = styled.button<{ visible: boolean }>`
+  /* position: relative; */
   float: right;
   border: none;
-  background-color: ${vscBackground};
+  background-color: transparent;
   cursor: pointer;
   padding: 0;
-  margin: 4px;
-  &:hover {
-    color: #fff;
-  }
+  /* margin: 4px; */
+  margin-top: -6px;
+
+  visibility: ${(props) => (props.visible ? "visible" : "hidden")};
 `;
 
-function CopyButton(props: { textToCopy: string }) {
+function CopyButton(props: { textToCopy: string; visible: boolean }) {
+  const [hovered, setHovered] = useState<boolean>(false);
+  const [clicked, setClicked] = useState<boolean>(false);
   return (
     <>
       <StyledCopyButton
+        onMouseEnter={() => {
+          setHovered(true);
+        }}
+        onMouseLeave={() => {
+          setHovered(false);
+        }}
+        visible={clicked || props.visible}
         onClick={() => {
           navigator.clipboard.writeText(props.textToCopy);
+          setClicked(true);
+          setTimeout(() => {
+            setClicked(false);
+          }, 2000);
         }}
       >
-        <Clipboard color="white" size="1.4em" />
+        {clicked ? (
+          <CheckCircle color="#00ff00" size="1.4em" />
+        ) : (
+          <Clipboard color={hovered ? "#00ff00" : "white"} size="1.4em" />
+        )}
       </StyledCopyButton>
     </>
   );
@@ -48,9 +66,18 @@ function CodeBlock(props: { language?: string; children: string }) {
   useEffect(() => {
     hljs.highlightAll();
   }, [props.children]);
+  const [hovered, setHovered] = useState<boolean>(false);
+
   return (
-    <StyledPre>
-      <CopyButton textToCopy={props.children} />
+    <StyledPre
+      onMouseEnter={() => {
+        setHovered(true);
+      }}
+      onMouseLeave={() => {
+        setHovered(false);
+      }}
+    >
+      <CopyButton visible={hovered} textToCopy={props.children} />
       <StyledCode>{props.children}</StyledCode>
     </StyledPre>
   );
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index e855ebe5..ddc9d5dc 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -9,16 +9,22 @@ import {
 } from ".";
 
 const mainInputFontSize = 16;
-const MainTextInput = styled.input`
+const MainTextInput = styled.textarea`
+  resize: none;
+
   padding: 8px;
   font-size: ${mainInputFontSize}px;
   border-radius: ${defaultBorderRadius};
-  border: 1px solid #ccc;
+  border: 1px solid white;
   margin: 8px auto;
   width: 100%;
   background-color: ${vscBackground};
   color: white;
-  outline: 1px solid orange;
+
+  &:focus {
+    border: 1px solid transparent;
+    outline: 1px solid orange;
+  }
 `;
 
 const UlMaxHeight = 200;
@@ -108,10 +114,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
     <div className="flex px-2" ref={divRef} hidden={!isOpen}>
       <MainTextInput
         disabled={props.disabled}
-        placeholder="Type '/' to see the list of available slash commands..."
+        placeholder="Type '/' to see available slash commands."
         {...getInputProps({
           onKeyDown: (event) => {
-            if (event.key === "Enter" && (!isOpen || items.length === 0)) {
+            if (event.key === "Enter" && event.shiftKey) {
+              // Prevent Downshift's default 'Enter' behavior.
+              (event.nativeEvent as any).preventDownshiftDefault = true;
+            } else if (
+              event.key === "Enter" &&
+              (!isOpen || items.length === 0)
+            ) {
               // Prevent Downshift's default 'Enter' behavior.
               (event.nativeEvent as any).preventDownshiftDefault = true;
               if (props.onEnter) props.onEnter(event);
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 4c98b04d..1eb1d1fd 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -5,14 +5,8 @@ import {
   defaultBorderRadius,
   secondaryDark,
   vscBackground,
-  GradientBorder,
   vscBackgroundTransparent,
-  HeaderButton,
 } from ".";
-import { RangeInFile, FileEdit } from "../../../src/client";
-import CodeBlock from "./CodeBlock";
-import SubContainer from "./SubContainer";
-
 import {
   ChevronDown,
   ChevronRight,
@@ -22,6 +16,7 @@ import {
 import { HistoryNode } from "../../../schema/HistoryNode";
 import ReactMarkdown from "react-markdown";
 import HeaderButtonWithText from "./HeaderButtonWithText";
+import CodeBlock from "./CodeBlock";
 
 interface StepContainerProps {
   historyNode: HistoryNode;
@@ -34,6 +29,8 @@ interface StepContainerProps {
   open: boolean;
   onToggleAll: () => void;
   onToggle: () => void;
+  isFirst: boolean;
+  isLast: boolean;
 }
 
 const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>`
@@ -67,12 +64,6 @@ const ContentDiv = styled.div`
   padding-left: 16px;
 `;
 
-const OnHoverDiv = styled.div`
-  text-align: center;
-  padding: 10px;
-  animation: ${appear} 0.3s ease-in-out;
-`;
-
 const MarkdownPre = styled.pre`
   background-color: ${secondaryDark};
   padding: 10px;
@@ -80,7 +71,34 @@ const MarkdownPre = styled.pre`
   border: 0.5px solid white;
 `;
 
-const MarkdownCode = styled.code``;
+const StyledCode = styled.code`
+  word-wrap: break-word;
+  color: lightgray;
+`;
+
+const GradientBorder = styled.div<{
+  borderWidth?: number;
+  borderRadius?: string;
+  borderColor?: string;
+  isFirst: boolean;
+  isLast: 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
+      : `linear-gradient(
+    101.79deg,
+    #12887a 0%,
+    #87245c 37.64%,
+    #e12637 65.98%,
+    #ffb215 110.45%
+  )`};
+`;
 
 function StepContainer(props: StepContainerProps) {
   const [isHovered, setIsHovered] = useState(false);
@@ -120,12 +138,14 @@ function StepContainer(props: StepContainerProps) {
     >
       <StepContainerDiv open={props.open}>
         <GradientBorder
+          isFirst={props.isFirst}
+          isLast={props.isLast}
           borderColor={
             props.historyNode.observation?.error ? "#f00" : undefined
           }
           className="overflow-hidden cursor-pointer"
           onClick={(e) => {
-            if (props.open && e.metaKey) {
+            if (e.metaKey) {
               props.onToggleAll();
             } else {
               props.onToggle();
@@ -198,66 +218,28 @@ function StepContainer(props: StepContainerProps) {
             <ReactMarkdown
               key={1}
               className="overflow-x-scroll"
-              components={
-                {
-                  // pre: ({ node, ...props }) => {
-                  //   return (
-                  //     <CodeBlock
-                  //       children={props.children[0] as string}
-                  //     ></CodeBlock>
-                  //   );
-                  // },
-                }
-              }
+              components={{
+                pre: ({ node, ...props }) => {
+                  return (
+                    <CodeBlock
+                      children={props.children[0] as string}
+                    ></CodeBlock>
+                  );
+                },
+                code: ({ node, ...props }) => {
+                  return (
+                    <StyledCode
+                      children={props.children[0] as string}
+                    ></StyledCode>
+                  );
+                },
+              }}
             >
               {props.historyNode.step.description as any}
             </ReactMarkdown>
           )}
-
-          {/* {props.historyNode.step.name === "Waiting for user input" && (
-            <InputAndButton
-              onUserInput={(value) => {
-                props.onUserInput(value);
-              }}
-            ></InputAndButton>
-          )}
-          {props.historyNode.step.name === "Waiting for user confirmation" && (
-            <>
-              <input
-                type="button"
-                value="Cancel"
-                className="m-4 p-2 rounded-md border border-solid text-white border-gray-200 bg-vsc-background cursor-pointer hover:bg-white hover:text-black"
-              ></input>
-              <input
-                className="m-4 p-2 rounded-md border border-solid text-white border-gray-200 bg-vsc-background cursor-pointer hover:bg-white hover:text-black"
-                onClick={(e) => {
-                  props.onUserInput("ok");
-                  e.preventDefault();
-                  e.stopPropagation();
-                }}
-                type="button"
-                value="Confirm"
-              />
-            </>
-          )} */}
         </ContentDiv>
       </StepContainerDiv>
-
-      {/* <OnHoverDiv hidden={!open}>
-        <NaturalLanguageInput
-          onKeyDown={(e) => {
-            if (e.key === "Enter") {
-              onTextInput();
-            }
-          }}
-          ref={naturalLanguageInputRef}
-          onClick={(e) => {
-            e.stopPropagation();
-            e.preventDefault();
-          }}
-        ></NaturalLanguageInput>
-        <ContinueButton onClick={onTextInput}></ContinueButton>
-      </OnHoverDiv> */}
     </MainDiv>
   );
 }
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 31a385d5..e50a7686 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -3,6 +3,13 @@ import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import { Button, buttonColor, secondaryDark, vscBackground } from ".";
 
+const ScreenCover = styled.div`
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(168, 168, 168, 0.5);
+`;
+
 const DialogContainer = styled.div`
   position: absolute;
   top: 50%;
@@ -28,9 +35,10 @@ const TextArea = styled.textarea`
   padding: 8px;
   outline: 1px solid black;
   font-family: Arial, Helvetica, sans-serif;
+  resize: none;
 
   &:focus {
-    outline: 1px solid orange;
+    outline: 1px solid ${buttonColor};
   }
 `;
 
@@ -54,34 +62,45 @@ const TextDialog = (props: {
   }, [props.showDialog]);
 
   return (
-    <DialogContainer hidden={!props.showDialog}>
-      <Dialog>
-        <P>Thanks for your feedback. We'll get back to you soon!</P>
-        <TextArea
-          cols={50}
-          rows={10}
-          ref={textAreaRef}
-          onKeyDown={(e) => {
-            if (e.key === "Enter" && e.metaKey && textAreaRef.current) {
-              props.onEnter(textAreaRef.current.value);
-              setText("");
-            } else if (e.key === "Escape") {
-              props.onClose();
-            }
-          }}
-        />
-        <Button
-          onClick={() => {
-            if (textAreaRef.current) {
-              props.onEnter(textAreaRef.current.value);
-              setText("");
-            }
-          }}
-        >
-          Enter
-        </Button>
-      </Dialog>
-    </DialogContainer>
+    <ScreenCover
+      onClick={() => {
+        props.onClose();
+      }}
+      hidden={!props.showDialog}
+    >
+      <DialogContainer
+        onClick={(e) => {
+          e.stopPropagation();
+        }}
+      >
+        <Dialog>
+          <P>Thanks for your feedback. We'll get back to you soon!</P>
+          <TextArea
+            cols={50}
+            rows={10}
+            ref={textAreaRef}
+            onKeyDown={(e) => {
+              if (e.key === "Enter" && e.metaKey && textAreaRef.current) {
+                props.onEnter(textAreaRef.current.value);
+                setText("");
+              } else if (e.key === "Escape") {
+                props.onClose();
+              }
+            }}
+          />
+          <Button
+            onClick={() => {
+              if (textAreaRef.current) {
+                props.onEnter(textAreaRef.current.value);
+                setText("");
+              }
+            }}
+          >
+            Enter
+          </Button>
+        </Dialog>
+      </DialogContainer>
+    </ScreenCover>
   );
 };
 
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index d99b4d96..fc94c51f 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -95,26 +95,6 @@ export const Loader = styled.div`
   margin: auto;
 `;
 
-export const GradientBorder = styled.div<{
-  borderWidth?: string;
-  borderRadius?: string;
-  borderColor?: string;
-}>`
-  border-radius: ${(props) => props.borderRadius || "0"};
-  padding-top: ${(props) => props.borderWidth || "1px"};
-  padding-bottom: ${(props) => props.borderWidth || "1px"};
-  background: ${(props) =>
-    props.borderColor
-      ? props.borderColor
-      : `linear-gradient(
-    101.79deg,
-    #12887a 0%,
-    #87245c 37.64%,
-    #e12637 65.98%,
-    #ffb215 110.45%
-  )`};
-`;
-
 export const MainContainerWithBorder = styled.div<{ borderWidth?: string }>`
   border-radius: ${defaultBorderRadius};
   padding: ${(props) => props.borderWidth || "1px"};
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index db8afab9..6dc514ec 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -3,8 +3,8 @@
 @tailwind utilities;
 
 :root {
-  --secondary-dark: 37 37 38;
-  --vsc-background: 30 30 30;
+  --secondary-dark: rgb(37, 37, 38);
+  --vsc-background: rgb(30, 30, 30);
   --button-color: rgb(113, 28, 59);
   --button-color-hover: rgba(113, 28, 59, 0.667);
   --def-border-radius: 5px;
@@ -26,7 +26,6 @@ body,
 }
 
 body {
-  background-color: var(--vsc-background);
   padding: 0;
   color: white;
   font-family: "Mona Sans", "Arial", sans-serif;
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index 6aa20e0e..624d22d5 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -331,6 +331,8 @@ function GUI(props: GUIProps) {
         {history?.timeline.map((node: HistoryNode, index: number) => {
           return (
             <StepContainer
+              isLast={index === history.timeline.length - 1}
+              isFirst={index === 0}
               open={stepsOpen[index]}
               onToggle={() => {
                 const nextStepsOpen = [...stepsOpen];
-- 
cgit v1.2.3-70-g09d2


From 0f4dc5f28c567e8ae5f2e9e5feedb72ac35265ea Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sun, 25 Jun 2023 22:35:37 -0700
Subject: context overflow fix

---
 continuedev/src/continuedev/libs/llm/openai.py     |   4 ++--
 .../src/continuedev/libs/util/count_tokens.py      |  14 +++++++++++--
 continuedev/src/continuedev/steps/core/core.py     |  22 ++++++---------------
 extension/package-lock.json                        |   4 ++--
 extension/package.json                             |   2 +-
 extension/react-app/src/components/TextDialog.tsx  |   2 +-
 .../scripts/continuedev-0.1.1-py3-none-any.whl     | Bin 89177 -> 89291 bytes
 7 files changed, 24 insertions(+), 24 deletions(-)

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/libs/llm/openai.py b/continuedev/src/continuedev/libs/llm/openai.py
index f0b2e6d8..3024ae61 100644
--- a/continuedev/src/continuedev/libs/llm/openai.py
+++ b/continuedev/src/continuedev/libs/llm/openai.py
@@ -4,7 +4,7 @@ from typing import Any, Coroutine, Dict, Generator, List, Union
 from ...core.main import ChatMessage
 import openai
 from ..llm import LLM
-from ..util.count_tokens import DEFAULT_MAX_TOKENS, compile_chat_messages, CHAT_MODELS, DEFAULT_ARGS, count_tokens
+from ..util.count_tokens import DEFAULT_MAX_TOKENS, compile_chat_messages, CHAT_MODELS, DEFAULT_ARGS, count_tokens, prune_raw_prompt_from_top
 
 
 class OpenAI(LLM):
@@ -72,7 +72,7 @@ class OpenAI(LLM):
             )).choices[0].message.content
         else:
             resp = (await openai.Completion.acreate(
-                prompt=prompt,
+                prompt=prune_raw_prompt_from_top(args["model"], prompt),
                 **args,
             )).choices[0].text
 
diff --git a/continuedev/src/continuedev/libs/util/count_tokens.py b/continuedev/src/continuedev/libs/util/count_tokens.py
index 6038b68d..addafcff 100644
--- a/continuedev/src/continuedev/libs/util/count_tokens.py
+++ b/continuedev/src/continuedev/libs/util/count_tokens.py
@@ -28,6 +28,16 @@ def count_tokens(model: str, text: str | None):
     return len(encoding.encode(text, disallowed_special=()))
 
 
+def prune_raw_prompt_from_top(model: str, prompt: str):
+    max_tokens = MAX_TOKENS_FOR_MODEL.get(model, DEFAULT_MAX_TOKENS)
+    encoding = encoding_for_model(model)
+    tokens = encoding.encode(prompt, disallowed_special=())
+    if len(tokens) <= max_tokens:
+        return prompt
+    else:
+        return encoding.decode(tokens[-max_tokens:])
+
+
 def prune_chat_history(model: str, chat_history: List[ChatMessage], max_tokens: int, tokens_for_completion: int):
     total_tokens = tokens_for_completion + \
         sum(count_tokens(model, message.content)
@@ -43,13 +53,13 @@ def prune_chat_history(model: str, chat_history: List[ChatMessage], max_tokens:
         i += 1
 
     # 2. Remove entire messages until the last 5
-    while len(chat_history) > 5 and total_tokens > max_tokens:
+    while len(chat_history) > 5 and total_tokens > max_tokens and len(chat_history) > 0:
         message = chat_history.pop(0)
         total_tokens -= count_tokens(model, message.content)
 
     # 3. Truncate message in the last 5
     i = 0
-    while total_tokens > max_tokens:
+    while total_tokens > max_tokens and len(chat_history) > 0:
         message = chat_history[0]
         total_tokens -= count_tokens(model, message.content)
         total_tokens += count_tokens(model, message.summary)
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index f146c94a..24f00d36 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -211,26 +211,16 @@ class DefaultModelEditCodeStep(Step):
 
                 return cur_start_line, cur_end_line
 
-            if model_to_use.name == "gpt-4":
-
-                total_tokens = model_to_use.count_tokens(
-                    full_file_contents + self._prompt)
-                cur_start_line, cur_end_line = cut_context(
-                    model_to_use, total_tokens, cur_start_line, cur_end_line)
-
-            elif model_to_use.name == "gpt-3.5-turbo" or model_to_use.name == "gpt-3.5-turbo-16k":
-
+            model_to_use = sdk.models.default
+            if model_to_use.name == "gpt-3.5-turbo":
                 if sdk.models.gpt35.count_tokens(full_file_contents) > MAX_TOKENS_FOR_MODEL["gpt-3.5-turbo"]:
-
                     model_to_use = sdk.models.gpt3516k
-                    total_tokens = model_to_use.count_tokens(
-                        full_file_contents + self._prompt)
-                    cur_start_line, cur_end_line = cut_context(
-                        model_to_use, total_tokens, cur_start_line, cur_end_line)
 
-            else:
+            total_tokens = model_to_use.count_tokens(
+                full_file_contents + self._prompt + self.user_input)
 
-                raise Exception("Unknown default model")
+            cur_start_line, cur_end_line = cut_context(
+                model_to_use, total_tokens, cur_start_line, cur_end_line)
 
             code_before = "\n".join(
                 full_file_contents_lst[cur_start_line:max_start_line])
diff --git a/extension/package-lock.json b/extension/package-lock.json
index 71edfe8c..c2f1cf18 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "continue",
-  "version": "0.0.62",
+  "version": "0.0.63",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "continue",
-      "version": "0.0.62",
+      "version": "0.0.63",
       "license": "Apache-2.0",
       "dependencies": {
         "@electron/rebuild": "^3.2.10",
diff --git a/extension/package.json b/extension/package.json
index 9679b159..ba5fd5b0 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -14,7 +14,7 @@
   "displayName": "Continue",
   "pricing": "Free",
   "description": "Accelerating software development with language models",
-  "version": "0.0.62",
+  "version": "0.0.63",
   "publisher": "Continue",
   "engines": {
     "vscode": "^1.74.0"
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index e50a7686..2632e572 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -15,6 +15,7 @@ const DialogContainer = styled.div`
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
+  width: 75%;
 `;
 
 const Dialog = styled.div`
@@ -76,7 +77,6 @@ const TextDialog = (props: {
         <Dialog>
           <P>Thanks for your feedback. We'll get back to you soon!</P>
           <TextArea
-            cols={50}
             rows={10}
             ref={textAreaRef}
             onKeyDown={(e) => {
diff --git a/extension/scripts/continuedev-0.1.1-py3-none-any.whl b/extension/scripts/continuedev-0.1.1-py3-none-any.whl
index 692bdfb7..a683cedd 100644
Binary files a/extension/scripts/continuedev-0.1.1-py3-none-any.whl and b/extension/scripts/continuedev-0.1.1-py3-none-any.whl differ
-- 
cgit v1.2.3-70-g09d2


From 4657f30167d18c42aa66cdd841965c9661669d3f Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sun, 2 Jul 2023 15:47:14 -0700
Subject: no more gpt-3.5-turbo

---
 continuedev/src/continuedev/steps/chat.py         |  2 +-
 continuedev/src/continuedev/steps/core/core.py    |  2 +-
 extension/react-app/src/components/TextDialog.tsx |  3 ++-
 extension/react-app/src/tabs/gui.tsx              | 22 +++++++++++++++++-----
 4 files changed, 21 insertions(+), 8 deletions(-)

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py
index 9bb75ab4..2efef37d 100644
--- a/continuedev/src/continuedev/steps/chat.py
+++ b/continuedev/src/continuedev/steps/chat.py
@@ -36,7 +36,7 @@ class SimpleChatStep(Step):
             summary=self.user_input
         ))
 
-        async for chunk in sdk.models.default.stream_chat(messages):
+        async for chunk in sdk.models.gpt4.stream_chat(messages):
             if sdk.current_step_was_deleted():
                 return
 
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index a84263cc..345d99bc 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -172,7 +172,7 @@ class DefaultModelEditCodeStep(Step):
         # We don't know here all of the functions being passed in.
         # We care because if this prompt itself goes over the limit, then the entire message will have to be cut from the completion.
         # Overflow won't happen, but prune_chat_messages in count_tokens.py will cut out this whole thing, instead of us cutting out only as many lines as we need.
-        model_to_use = sdk.models.default
+        model_to_use = sdk.models.gpt4
 
         BUFFER_FOR_FUNCTIONS = 400
         total_tokens = model_to_use.count_tokens(
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 2632e572..a564f884 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -52,6 +52,7 @@ const TextDialog = (props: {
   showDialog: boolean;
   onEnter: (text: string) => void;
   onClose: () => void;
+  message?: string;
 }) => {
   const [text, setText] = useState("");
   const textAreaRef = React.createRef<HTMLTextAreaElement>();
@@ -75,7 +76,7 @@ const TextDialog = (props: {
         }}
       >
         <Dialog>
-          <P>Thanks for your feedback. We'll get back to you soon!</P>
+          <P>{props.message || ""}</P>
           <TextArea
             rows={10}
             ref={textAreaRef}
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index 39925fc5..bc0e5365 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -310,6 +310,7 @@ function GUI(props: GUIProps) {
   // } as any);
 
   const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
+  const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
 
   const topGuiDivRef = useRef<HTMLDivElement>(null);
   const client = useContinueGUIProtocol();
@@ -458,6 +459,7 @@ function GUI(props: GUIProps) {
         onClose={() => {
           setShowFeedbackDialog(false);
         }}
+        message={feedbackDialogMessage}
       ></TextDialog>
 
       <TopGUIDiv
@@ -593,17 +595,24 @@ function GUI(props: GUIProps) {
         </div>
         <HeaderButtonWithText
           onClick={() => {
-            client?.changeDefaultModel(
-              usingFastModel ? "gpt-4" : "gpt-3.5-turbo"
-            );
+            // client?.changeDefaultModel(
+            //   usingFastModel ? "gpt-4" : "gpt-3.5-turbo"
+            // );
+            if (!usingFastModel) {
+              // Show the dialog
+              setFeedbackDialogMessage(
+                "We don't yet support local models, but we're working on it! If privacy is a concern of yours, please use the feedback button in the bottom right to let us know."
+              );
+              setShowFeedbackDialog(true);
+            }
             setUsingFastModel((prev) => !prev);
           }}
-          text={usingFastModel ? "gpt-3.5-turbo" : "gpt-4"}
+          text={usingFastModel ? "local" : "gpt-4"}
         >
           <div
             style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }}
           >
-            {usingFastModel ? "⚡" : "🧠"}
+            {usingFastModel ? "🔒" : "🧠"}
           </div>
         </HeaderButtonWithText>
         <HeaderButtonWithText
@@ -625,6 +634,9 @@ function GUI(props: GUIProps) {
         <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."
+            );
             setShowFeedbackDialog(true);
           }}
           text="Feedback"
-- 
cgit v1.2.3-70-g09d2


From 5c03dd4775845c559a1f6298f7577609ba907bb1 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Wed, 5 Jul 2023 23:49:37 -0700
Subject: ui overhaul

---
 continuedev/src/continuedev/core/autopilot.py      |  47 +++-
 continuedev/src/continuedev/core/main.py           |   9 +-
 .../src/continuedev/models/generate_json_schema.py |   4 +-
 continuedev/src/continuedev/server/gui.py          |  14 +
 .../media/Lexend/Lexend-VariableFont_wght.ttf      | Bin 0 -> 176220 bytes
 extension/media/Lexend/OFL.txt                     |  93 +++++++
 extension/media/Lexend/README.txt                  |  71 +++++
 extension/media/Lexend/static/Lexend-Black.ttf     | Bin 0 -> 78496 bytes
 extension/media/Lexend/static/Lexend-Bold.ttf      | Bin 0 -> 78632 bytes
 extension/media/Lexend/static/Lexend-ExtraBold.ttf | Bin 0 -> 78716 bytes
 .../media/Lexend/static/Lexend-ExtraLight.ttf      | Bin 0 -> 78508 bytes
 extension/media/Lexend/static/Lexend-Light.ttf     | Bin 0 -> 78668 bytes
 extension/media/Lexend/static/Lexend-Medium.ttf    | Bin 0 -> 78696 bytes
 extension/media/Lexend/static/Lexend-Regular.ttf   | Bin 0 -> 78360 bytes
 extension/media/Lexend/static/Lexend-SemiBold.ttf  | Bin 0 -> 78744 bytes
 extension/media/Lexend/static/Lexend-Thin.ttf      | Bin 0 -> 78380 bytes
 extension/react-app/package-lock.json              |  59 ++++
 extension/react-app/package.json                   |   1 +
 extension/react-app/src/App.tsx                    |  39 +--
 extension/react-app/src/components/CodeBlock.tsx   |   4 +-
 extension/react-app/src/components/ComboBox.tsx    | 191 +++++++------
 .../react-app/src/components/ContinueButton.tsx    |   2 +-
 .../src/components/HeaderButtonWithText.tsx        |  44 +--
 extension/react-app/src/components/Loader.tsx      |  40 +++
 extension/react-app/src/components/PillButton.tsx  | 144 +++++++---
 .../react-app/src/components/StepContainer.tsx     |  13 +-
 extension/react-app/src/components/TextDialog.tsx  |   2 +-
 .../src/components/UserInputContainer.tsx          |   6 +-
 extension/react-app/src/components/index.ts        |  22 +-
 .../src/hooks/ContinueGUIClientProtocol.ts         |   4 +
 .../react-app/src/hooks/useContinueGUIProtocol.ts  |   8 +
 extension/react-app/src/index.css                  |   9 +-
 extension/react-app/src/main.tsx                   |   6 +-
 extension/react-app/src/tabs/gui.tsx               |  22 +-
 extension/schema/FullState.d.ts                    | 133 +++++++++
 extension/src/debugPanel.ts                        |   4 +
 schema/json/FullState.json                         | 304 +++++++++++++++++++++
 37 files changed, 1088 insertions(+), 207 deletions(-)
 create mode 100644 extension/media/Lexend/Lexend-VariableFont_wght.ttf
 create mode 100644 extension/media/Lexend/OFL.txt
 create mode 100644 extension/media/Lexend/README.txt
 create mode 100644 extension/media/Lexend/static/Lexend-Black.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-Bold.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-ExtraBold.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-ExtraLight.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-Light.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-Medium.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-Regular.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-SemiBold.ttf
 create mode 100644 extension/media/Lexend/static/Lexend-Thin.ttf
 create mode 100644 extension/react-app/src/components/Loader.tsx
 create mode 100644 extension/schema/FullState.d.ts
 create mode 100644 schema/json/FullState.json

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index b1c4f471..acdc1f0d 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -12,7 +12,7 @@ from .observation import Observation, InternalErrorObservation
 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, HighlightedRangeContext, Policy, History, FullState, Step, HistoryNode
 from ..steps.core.core import ReversibleStep, ManualEditStep, UserInputStep
 from ..libs.util.telemetry import capture_event
 from .sdk import ContinueSDK
@@ -140,11 +140,24 @@ class Autopilot(ContinueBaseModel):
                         tb_step.step_name, {"output": output, **tb_step.params})
                     await self._run_singular_step(step)
 
-    _highlighted_ranges: List[RangeInFileWithContents] = []
+    _highlighted_ranges: List[HighlightedRangeContext] = []
     _adding_highlighted_code: bool = False
 
+    def _make_sure_is_editing_range(self):
+        """If none of the highlighted ranges are currently being edited, the first should be selected"""
+        if len(self._highlighted_ranges) == 0:
+            return
+        if not any(map(lambda x: x.editing, self._highlighted_ranges)):
+            self._highlighted_ranges[0].editing = True
+
     async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]):
-        if not self._adding_highlighted_code:
+        if not self._adding_highlighted_code and len(self._highlighted_ranges) > 0:
+            return
+
+        # If un-highlighting, then remove the range
+        if len(self._highlighted_ranges) == 1 and len(range_in_files) == 1 and range_in_files[0].range.start == range_in_files[0].range.end:
+            self._highlighted_ranges = []
+            await self.update_subscribers()
             return
 
         # Filter out rifs from ~/.continue/diffs folder
@@ -160,20 +173,25 @@ class Autopilot(ContinueBaseModel):
         for i, rif in enumerate(self._highlighted_ranges):
             found_overlap = False
             for new_rif in range_in_files:
-                if rif.filepath == new_rif.filepath and rif.range.overlaps_with(new_rif.range):
+                if rif.range.filepath == new_rif.filepath and rif.range.range.overlaps_with(new_rif.range):
                     found_overlap = True
                     break
 
                 # 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 rif.filepath == new_rif.filepath and rif.contents == new_rif.contents:
+                if rif.range.filepath == new_rif.filepath and rif.range.contents == new_rif.contents:
                     found_overlap = True
                     break
 
             if not found_overlap:
                 new_ranges.append(rif)
 
-        self._highlighted_ranges = new_ranges + range_in_files
+        self._highlighted_ranges = new_ranges + [HighlightedRangeContext(
+            range=rif, editing=False, pinned=False
+        ) for rif in range_in_files]
+
+        self._make_sure_is_editing_range()
+
         await self.update_subscribers()
 
     _step_depth: int = 0
@@ -193,12 +211,25 @@ class Autopilot(ContinueBaseModel):
             if i not in indices:
                 kept_ranges.append(rif)
         self._highlighted_ranges = kept_ranges
+
+        self._make_sure_is_editing_range()
+
         await self.update_subscribers()
 
     async def toggle_adding_highlighted_code(self):
         self._adding_highlighted_code = not self._adding_highlighted_code
         await self.update_subscribers()
 
+    async def set_editing_at_indices(self, indices: List[int]):
+        for i in range(len(self._highlighted_ranges)):
+            self._highlighted_ranges[i].editing = i in indices
+        await self.update_subscribers()
+
+    async def set_pinned_at_indices(self, indices: List[int]):
+        for i in range(len(self._highlighted_ranges)):
+            self._highlighted_ranges[i].pinned = i in indices
+        await self.update_subscribers()
+
     async def _run_singular_step(self, step: "Step", is_future_step: bool = False) -> Coroutine[Observation, None, None]:
         # Allow config to set disallowed steps
         if step.__class__.__name__ in self.continue_sdk.config.disallowed_steps:
@@ -359,6 +390,10 @@ class Autopilot(ContinueBaseModel):
         if len(self._main_user_input_queue) > 1:
             return
 
+        # Remove context unless pinned
+        self._highlighted_ranges = [
+            hr for hr in self._highlighted_ranges if hr.pinned]
+
         # await self._request_halt()
         # Just run the step that takes user input, and
         # then up to the policy to decide how to deal with it.
diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py
index 28fd964e..62cc4936 100644
--- a/continuedev/src/continuedev/core/main.py
+++ b/continuedev/src/continuedev/core/main.py
@@ -199,13 +199,20 @@ class SlashCommandDescription(ContinueBaseModel):
     description: str
 
 
+class HighlightedRangeContext(ContinueBaseModel):
+    """Context for a highlighted range"""
+    range: RangeInFileWithContents
+    editing: bool
+    pinned: bool
+
+
 class FullState(ContinueBaseModel):
     """A full state of the program, including the history"""
     history: History
     active: bool
     user_input_queue: List[str]
     default_model: str
-    highlighted_ranges: List[RangeInFileWithContents]
+    highlighted_ranges: List[HighlightedRangeContext]
     slash_commands: List[SlashCommandDescription]
     adding_highlighted_code: bool
 
diff --git a/continuedev/src/continuedev/models/generate_json_schema.py b/continuedev/src/continuedev/models/generate_json_schema.py
index 080787a5..6cebf429 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
+from ..core.main import History, HistoryNode, FullState
 from pydantic import schema_json_of
 import os
 
@@ -12,7 +12,7 @@ MODELS_TO_GENERATE = [
 ] + [
     FileEditWithFullContents
 ] + [
-    History, HistoryNode
+    History, HistoryNode, FullState
 ]
 
 RENAMES = {
diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py
index fa573b37..8e9b1fb9 100644
--- a/continuedev/src/continuedev/server/gui.py
+++ b/continuedev/src/continuedev/server/gui.py
@@ -87,6 +87,10 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
                 self.on_delete_context_at_indices(data["indices"])
             elif message_type == "toggle_adding_highlighted_code":
                 self.on_toggle_adding_highlighted_code()
+            elif message_type == "set_editing_at_indices":
+                self.on_set_editing_at_indices(data["indices"])
+            elif message_type == "set_pinned_at_indices":
+                self.on_set_pinned_at_indices(data["indices"])
         except Exception as e:
             print(e)
 
@@ -135,6 +139,16 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
             self.session.autopilot.toggle_adding_highlighted_code()
         )
 
+    def on_set_editing_at_indices(self, indices: List[int]):
+        asyncio.create_task(
+            self.session.autopilot.set_editing_at_indices(indices)
+        )
+
+    def on_set_pinned_at_indices(self, indices: List[int]):
+        asyncio.create_task(
+            self.session.autopilot.set_pinned_at_indices(indices)
+        )
+
 
 @router.websocket("/ws")
 async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(websocket_session)):
diff --git a/extension/media/Lexend/Lexend-VariableFont_wght.ttf b/extension/media/Lexend/Lexend-VariableFont_wght.ttf
new file mode 100644
index 00000000..b294dc84
Binary files /dev/null and b/extension/media/Lexend/Lexend-VariableFont_wght.ttf differ
diff --git a/extension/media/Lexend/OFL.txt b/extension/media/Lexend/OFL.txt
new file mode 100644
index 00000000..6b679248
--- /dev/null
+++ b/extension/media/Lexend/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/extension/media/Lexend/README.txt b/extension/media/Lexend/README.txt
new file mode 100644
index 00000000..f2966dfe
--- /dev/null
+++ b/extension/media/Lexend/README.txt
@@ -0,0 +1,71 @@
+Lexend Variable Font
+====================
+
+This download contains Lexend as both a variable font and static fonts.
+
+Lexend is a variable font with this axis:
+  wght
+
+This means all the styles are contained in a single file:
+  Lexend-VariableFont_wght.ttf
+
+If your app fully supports variable fonts, you can now pick intermediate styles
+that aren’t available as static fonts. Not all apps support variable fonts, and
+in those cases you can use the static font files for Lexend:
+  static/Lexend-Thin.ttf
+  static/Lexend-ExtraLight.ttf
+  static/Lexend-Light.ttf
+  static/Lexend-Regular.ttf
+  static/Lexend-Medium.ttf
+  static/Lexend-SemiBold.ttf
+  static/Lexend-Bold.ttf
+  static/Lexend-ExtraBold.ttf
+  static/Lexend-Black.ttf
+
+Get started
+-----------
+
+1. Install the font files you want to use
+
+2. Use your app's font picker to view the font family and all the
+available styles
+
+Learn more about variable fonts
+-------------------------------
+
+  https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
+  https://variablefonts.typenetwork.com
+  https://medium.com/variable-fonts
+
+In desktop apps
+
+  https://theblog.adobe.com/can-variable-fonts-illustrator-cc
+  https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
+
+Online
+
+  https://developers.google.com/fonts/docs/getting_started
+  https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
+  https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
+
+Installing fonts
+
+  MacOS: https://support.apple.com/en-us/HT201749
+  Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
+  Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
+
+Android Apps
+
+  https://developers.google.com/fonts/docs/android
+  https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
+
+License
+-------
+Please read the full license text (OFL.txt) to understand the permissions,
+restrictions and requirements for usage, redistribution, and modification.
+
+You can use them in your products & projects – print or digital,
+commercial or otherwise.
+
+This isn't legal advice, please consider consulting a lawyer and see the full
+license for all details.
diff --git a/extension/media/Lexend/static/Lexend-Black.ttf b/extension/media/Lexend/static/Lexend-Black.ttf
new file mode 100644
index 00000000..2fea087c
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Black.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-Bold.ttf b/extension/media/Lexend/static/Lexend-Bold.ttf
new file mode 100644
index 00000000..95884f6e
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Bold.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-ExtraBold.ttf b/extension/media/Lexend/static/Lexend-ExtraBold.ttf
new file mode 100644
index 00000000..02f84ed3
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-ExtraBold.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-ExtraLight.ttf b/extension/media/Lexend/static/Lexend-ExtraLight.ttf
new file mode 100644
index 00000000..20e7068d
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-ExtraLight.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-Light.ttf b/extension/media/Lexend/static/Lexend-Light.ttf
new file mode 100644
index 00000000..fb6d097c
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Light.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-Medium.ttf b/extension/media/Lexend/static/Lexend-Medium.ttf
new file mode 100644
index 00000000..d91a8673
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Medium.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-Regular.ttf b/extension/media/Lexend/static/Lexend-Regular.ttf
new file mode 100644
index 00000000..b423d3ab
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Regular.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-SemiBold.ttf b/extension/media/Lexend/static/Lexend-SemiBold.ttf
new file mode 100644
index 00000000..9dcb8214
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-SemiBold.ttf differ
diff --git a/extension/media/Lexend/static/Lexend-Thin.ttf b/extension/media/Lexend/static/Lexend-Thin.ttf
new file mode 100644
index 00000000..0d7df881
Binary files /dev/null and b/extension/media/Lexend/static/Lexend-Thin.ttf differ
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index fb13dffd..7316581d 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -20,6 +20,7 @@
         "react-redux": "^8.0.5",
         "react-switch": "^7.0.0",
         "react-syntax-highlighter": "^15.5.0",
+        "react-tooltip": "^5.18.0",
         "styled-components": "^5.3.6",
         "vscode-webview": "^1.0.1-beta.1"
       },
@@ -597,6 +598,19 @@
         "node": ">=12"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
+      "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.4.tgz",
+      "integrity": "sha512-21hhDEPOiWkGp0Ys4Wi6Neriah7HweToKra626CIK712B5m9qkdz54OP9gVldUg+URnBTpv/j/bi/skmGdstXQ==",
+      "dependencies": {
+        "@floating-ui/core": "^1.3.1"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -1310,6 +1324,11 @@
         "node": ">= 6"
       }
     },
+    "node_modules/classnames": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+      "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+    },
     "node_modules/color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2967,6 +2986,19 @@
         "react": ">= 0.14.0"
       }
     },
+    "node_modules/react-tooltip": {
+      "version": "5.18.0",
+      "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.18.0.tgz",
+      "integrity": "sha512-qjDK/skUJJ27sc9lTWeNxp2rLzmenBTskSsRiDOCPnupGSz2GhL5IZxDizK/sOsk0hn5iSCywt+3jKxUJ3Y4Sw==",
+      "dependencies": {
+        "@floating-ui/dom": "^1.0.0",
+        "classnames": "^2.3.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.14.0",
+        "react-dom": ">=16.14.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -3886,6 +3918,19 @@
       "dev": true,
       "optional": true
     },
+    "@floating-ui/core": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
+      "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
+    },
+    "@floating-ui/dom": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.4.tgz",
+      "integrity": "sha512-21hhDEPOiWkGp0Ys4Wi6Neriah7HweToKra626CIK712B5m9qkdz54OP9gVldUg+URnBTpv/j/bi/skmGdstXQ==",
+      "requires": {
+        "@floating-ui/core": "^1.3.1"
+      }
+    },
     "@jridgewell/gen-mapping": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
@@ -4350,6 +4395,11 @@
         }
       }
     },
+    "classnames": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+      "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+    },
     "color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -5411,6 +5461,15 @@
         "refractor": "^3.6.0"
       }
     },
+    "react-tooltip": {
+      "version": "5.18.0",
+      "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.18.0.tgz",
+      "integrity": "sha512-qjDK/skUJJ27sc9lTWeNxp2rLzmenBTskSsRiDOCPnupGSz2GhL5IZxDizK/sOsk0hn5iSCywt+3jKxUJ3Y4Sw==",
+      "requires": {
+        "@floating-ui/dom": "^1.0.0",
+        "classnames": "^2.3.0"
+      }
+    },
     "read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index 12701906..4bedb813 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -21,6 +21,7 @@
     "react-redux": "^8.0.5",
     "react-switch": "^7.0.0",
     "react-syntax-highlighter": "^15.5.0",
+    "react-tooltip": "^5.18.0",
     "styled-components": "^5.3.6",
     "vscode-webview": "^1.0.1-beta.1"
   },
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx
index a51541d0..8785f88f 100644
--- a/extension/react-app/src/App.tsx
+++ b/extension/react-app/src/App.tsx
@@ -1,28 +1,33 @@
 import DebugPanel from "./components/DebugPanel";
 import MainTab from "./tabs/main";
-import { Provider } from "react-redux";
-import store from "./redux/store";
 import WelcomeTab from "./tabs/welcome";
 import ChatTab from "./tabs/chat";
 import GUI from "./tabs/gui";
+import { createContext } from "react";
+import useContinueGUIProtocol from "./hooks/useWebsocket";
+import ContinueGUIClientProtocol from "./hooks/useContinueGUIProtocol";
+
+export const GUIClientContext = createContext<
+  ContinueGUIClientProtocol | undefined
+>(undefined);
 
 function App() {
+  const client = useContinueGUIProtocol();
+
   return (
-    <>
-      <Provider store={store}>
-        <DebugPanel
-          tabs={[
-            {
-              element: <GUI />,
-              title: "GUI",
-            },
-            // { element: <MainTab />, title: "Debug Panel" },
-            // { element: <WelcomeTab />, title: "Welcome" },
-            // { element: <ChatTab />, title: "Chat" },
-          ]}
-        ></DebugPanel>
-      </Provider>
-    </>
+    <GUIClientContext.Provider value={client}>
+      <DebugPanel
+        tabs={[
+          {
+            element: <GUI />,
+            title: "GUI",
+          },
+          // { element: <MainTab />, title: "Debug Panel" },
+          // { element: <WelcomeTab />, title: "Welcome" },
+          // { element: <ChatTab />, title: "Chat" },
+        ]}
+      />
+    </GUIClientContext.Provider>
   );
 }
 
diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx
index 17f5626b..fe9b3a95 100644
--- a/extension/react-app/src/components/CodeBlock.tsx
+++ b/extension/react-app/src/components/CodeBlock.tsx
@@ -52,9 +52,9 @@ function CopyButton(props: { textToCopy: string; visible: boolean }) {
         }}
       >
         {clicked ? (
-          <CheckCircle color="#00ff00" size="1.4em" />
+          <CheckCircle color="#00ff00" size="1.5em" />
         ) : (
-          <Clipboard color={hovered ? "#00ff00" : "white"} size="1.4em" />
+          <Clipboard color={hovered ? "#00ff00" : "white"} size="1.5em" />
         )}
       </StyledCopyButton>
     </>
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 81b148b9..7ee5dc24 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -4,6 +4,7 @@ import styled from "styled-components";
 import {
   buttonColor,
   defaultBorderRadius,
+  lightGray,
   secondaryDark,
   vscBackground,
 } from ".";
@@ -16,11 +17,32 @@ import {
   LockClosed,
   LockOpen,
   Plus,
+  DocumentPlus,
 } from "@styled-icons/heroicons-outline";
+import { HighlightedRangeContext } from "../../../schema/FullState";
 
 // #region styled components
 const mainInputFontSize = 16;
 
+const EmptyPillDiv = styled.div`
+  padding: 8px;
+  border-radius: ${defaultBorderRadius};
+  border: 1px dashed ${lightGray};
+  color: ${lightGray};
+  background-color: ${vscBackground};
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  text-align: center;
+  cursor: pointer;
+  font-size: 13px;
+
+  &:hover {
+    background-color: ${lightGray};
+    color: ${vscBackground};
+  }
+`;
+
 const ContextDropdown = styled.div`
   position: absolute;
   padding: 4px;
@@ -41,17 +63,19 @@ const MainTextInput = styled.textarea`
 
   padding: 8px;
   font-size: ${mainInputFontSize}px;
+  font-family: inherit;
+  border: 1px solid transparent;
   border-radius: ${defaultBorderRadius};
-  border: 1px solid white;
   margin: 8px auto;
+  height: auto;
   width: 100%;
-  background-color: ${vscBackground};
+  background-color: ${secondaryDark};
   color: white;
   z-index: 1;
 
   &:focus {
+    outline: 1px solid #ff000066;
     border: 1px solid transparent;
-    outline: 1px solid orange;
   }
 `;
 
@@ -69,7 +93,6 @@ const Ul = styled.ul<{
   background: ${vscBackground};
   background-color: ${secondaryDark};
   color: white;
-  font-family: "Fira Code", monospace;
   max-height: ${UlMaxHeight}px;
   overflow: scroll;
   padding: 0;
@@ -102,7 +125,7 @@ interface ComboBoxProps {
   onInputValueChange: (inputValue: string) => void;
   disabled?: boolean;
   onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
-  highlightedCodeSections: (RangeInFile & { contents: string })[];
+  highlightedCodeSections: HighlightedRangeContext[];
   deleteContextItems: (indices: number[]) => void;
   onTogglePin: () => void;
   onToggleAddContext: () => void;
@@ -119,16 +142,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
     React.useState(false);
   const [pinned, setPinned] = useState(false);
   const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
-    props.highlightedCodeSections || [
-      {
-        filepath: "test.ts",
-        range: {
-          start: { line: 0, character: 0 },
-          end: { line: 0, character: 0 },
-        },
-        contents: "import * as a from 'a';",
-      },
-    ]
+    props.highlightedCodeSections || []
   );
 
   useEffect(() => {
@@ -169,6 +183,71 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
 
   return (
     <>
+      <div className="px-2 flex gap-2 items-center flex-wrap mt-2">
+        {highlightedCodeSections.length > 1 && (
+          <>
+            <HeaderButtonWithText
+              text="Clear Context"
+              onClick={() => {
+                props.deleteContextItems(
+                  highlightedCodeSections.map((_, idx) => idx)
+                );
+              }}
+            >
+              <Trash size="1.6em" />
+            </HeaderButtonWithText>
+          </>
+        )}
+        {highlightedCodeSections.map((section, idx) => (
+          <PillButton
+            editing={section.editing}
+            pinned={section.pinned}
+            index={idx}
+            key={`${section.filepath}${idx}`}
+            title={`${section.range.filepath} (${
+              section.range.range.start.line + 1
+            }-${section.range.range.end.line + 1})`}
+            onDelete={() => {
+              if (props.deleteContextItems) {
+                props.deleteContextItems([idx]);
+              }
+              setHighlightedCodeSections((prev) => {
+                const newSections = [...prev];
+                newSections.splice(idx, 1);
+                return newSections;
+              });
+            }}
+            onHover={(val: boolean) => {
+              if (val) {
+                setHoveringButton(val);
+              } else {
+                setTimeout(() => {
+                  setHoveringButton(val);
+                }, 100);
+              }
+            }}
+          />
+        ))}
+        {props.highlightedCodeSections.length > 0 &&
+          (props.addingHighlightedCode ? (
+            <EmptyPillDiv
+              onClick={() => {
+                props.onToggleAddContext();
+              }}
+            >
+              Highlight to Add Context
+            </EmptyPillDiv>
+          ) : (
+            <HeaderButtonWithText
+              text="Add to Context"
+              onClick={() => {
+                props.onToggleAddContext();
+              }}
+            >
+              <DocumentPlus width="1.6em"></DocumentPlus>
+            </HeaderButtonWithText>
+          ))}
+      </div>
       <div className="flex px-2" ref={divRef} hidden={!isOpen}>
         <MainTextInput
           disabled={props.disabled}
@@ -260,80 +339,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
             ))}
         </Ul>
       </div>
-      <div className="px-2 flex gap-2 items-center flex-wrap">
-        {highlightedCodeSections.length === 0 && (
-          <HeaderButtonWithText
-            text={
-              props.addingHighlightedCode ? "Adding Context" : "Add Context"
-            }
-            onClick={() => {
-              props.onToggleAddContext();
-            }}
-            inverted={props.addingHighlightedCode}
-          >
-            <Plus size="1.6em" />
-          </HeaderButtonWithText>
-        )}
-        {highlightedCodeSections.length > 0 && (
-          <>
-            <HeaderButtonWithText
-              text="Clear Context"
-              onClick={() => {
-                props.deleteContextItems(
-                  highlightedCodeSections.map((_, idx) => idx)
-                );
-              }}
-            >
-              <Trash size="1.6em" />
-            </HeaderButtonWithText>
-            <HeaderButtonWithText
-              text={pinned ? "Unpin Context" : "Pin Context"}
-              inverted={pinned}
-              onClick={() => {
-                setPinned((prev) => !prev);
-                props.onTogglePin();
-              }}
-            >
-              {pinned ? (
-                <LockClosed size="1.6em"></LockClosed>
-              ) : (
-                <LockOpen size="1.6em"></LockOpen>
-              )}
-            </HeaderButtonWithText>
-          </>
-        )}
-        {highlightedCodeSections.map((section, idx) => (
-          <PillButton
-            title={`${section.filepath} (${section.range.start.line + 1}-${
-              section.range.end.line + 1
-            })`}
-            onDelete={() => {
-              if (props.deleteContextItems) {
-                props.deleteContextItems([idx]);
-              }
-              setHighlightedCodeSections((prev) => {
-                const newSections = [...prev];
-                newSections.splice(idx, 1);
-                return newSections;
-              });
-            }}
-            onHover={(val: boolean) => {
-              if (val) {
-                setHoveringButton(val);
-              } else {
-                setTimeout(() => {
-                  setHoveringButton(val);
-                }, 100);
-              }
-            }}
-          />
-        ))}
-
-        <span className="text-trueGray-400 ml-auto mr-4 text-xs text-right">
-          Highlight code to include as context. Currently open file included by
-          default. {highlightedCodeSections.length === 0 && ""}
-        </span>
-      </div>
+      {/* <span className="text-trueGray-400 ml-auto m-auto text-xs text-right">
+        Highlight code to include as context. Currently open file included by
+        default. {highlightedCodeSections.length === 0 && ""}
+      </span> */}
       <ContextDropdown
         onMouseEnter={() => {
           setHoveringContextDropdown(true);
@@ -345,9 +354,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
       >
         {highlightedCodeSections.map((section, idx) => (
           <>
-            <p>{section.filepath}</p>
+            <p>{section.range.filepath}</p>
             <CodeBlock showCopy={false} key={idx}>
-              {section.contents}
+              {section.range.contents}
             </CodeBlock>
           </>
         ))}
diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx
index 5295799a..462f2b46 100644
--- a/extension/react-app/src/components/ContinueButton.tsx
+++ b/extension/react-app/src/components/ContinueButton.tsx
@@ -18,7 +18,7 @@ let StyledButton = styled(Button)`
 
   &:hover {
     transition-delay: 0.5s;
-    transition-property: background;
+    transition-property: "background";
     background: linear-gradient(
       45deg,
       #be1a55 14.44%,
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index 72a653c5..de8e3c98 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -1,6 +1,7 @@
 import React, { useState } from "react";
-
-import { HeaderButton } from ".";
+import { Tooltip } from "react-tooltip";
+import styled from "styled-components";
+import { HeaderButton, StyledTooltip, defaultBorderRadius } from ".";
 
 interface HeaderButtonWithTextProps {
   text: string;
@@ -13,25 +14,28 @@ interface HeaderButtonWithTextProps {
 
 const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
   const [hover, setHover] = useState(false);
-  const paddingLeft = (props.disabled ? (props.active ?  "3px" : "1px"): (hover ? "4px" : "1px"));
   return (
-    <HeaderButton
-      inverted={props.inverted}
-      disabled={props.disabled}
-      style={{ padding: (props.active ?  "3px" : "1px"), paddingLeft, borderRadius: (props.active ?  "50%" : undefined) }}
-      onMouseEnter={() => {
-        if (!props.disabled) {
-          setHover(true);
-        }
-      }}
-      onMouseLeave={() => {
-        setHover(false);
-      }}
-      onClick={props.onClick}
-    >
-      <span hidden={!hover}>{props.text}</span>
-      {props.children}
-    </HeaderButton>
+    <>
+      <HeaderButton
+        data-tooltip-id={`header_button_${props.text}`}
+        inverted={props.inverted}
+        disabled={props.disabled}
+        onMouseEnter={() => {
+          if (!props.disabled) {
+            setHover(true);
+          }
+        }}
+        onMouseLeave={() => {
+          setHover(false);
+        }}
+        onClick={props.onClick}
+      >
+        {props.children}
+      </HeaderButton>
+      <StyledTooltip id={`header_button_${props.text}`} place="bottom">
+        {props.text}
+      </StyledTooltip>
+    </>
   );
 };
 
diff --git a/extension/react-app/src/components/Loader.tsx b/extension/react-app/src/components/Loader.tsx
new file mode 100644
index 00000000..90eff793
--- /dev/null
+++ b/extension/react-app/src/components/Loader.tsx
@@ -0,0 +1,40 @@
+import { Play } from "@styled-icons/heroicons-outline";
+import { useSelector } from "react-redux";
+import styled from "styled-components";
+import { RootStore } from "../redux/store";
+
+const DEFAULT_SIZE = "28px";
+
+const FlashingDiv = styled.div`
+  margin: auto;
+  width: ${DEFAULT_SIZE};
+  animation: flash 1.2s infinite ease-in-out;
+  @keyframes flash {
+    0% {
+      opacity: 0.4;
+    }
+    50% {
+      opacity: 1;
+    }
+    100% {
+      opacity: 0.4;
+    }
+  }
+`;
+
+function Loader(props: { size?: string }) {
+  const vscMediaUrl = useSelector(
+    (state: RootStore) => state.config.vscMediaUrl
+  );
+  return (
+    <FlashingDiv>
+      {vscMediaUrl ? (
+        <img src={`${vscMediaUrl}/play_button.png`} width="22px" />
+      ) : (
+        <Play width={props.size || DEFAULT_SIZE} />
+      )}
+    </FlashingDiv>
+  );
+}
+
+export default Loader;
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 5a02c6b2..a384832e 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,54 +1,136 @@
-import { useState } from "react";
+import { useContext, useState } from "react";
 import styled from "styled-components";
-import { defaultBorderRadius } from ".";
-import { XMark } from "@styled-icons/heroicons-outline";
+import {
+  StyledTooltip,
+  defaultBorderRadius,
+  lightGray,
+  secondaryDark,
+} from ".";
+import { Trash, PaintBrush, MapPin } from "@styled-icons/heroicons-outline";
+import { GUIClientContext } from "../App";
 
 const Button = styled.button`
   border: none;
   color: white;
-  background-color: transparent;
-  border: 1px solid white;
+  background-color: ${secondaryDark};
   border-radius: ${defaultBorderRadius};
-  padding: 3px 6px;
+  padding: 8px;
+  overflow: hidden;
+
+  cursor: pointer;
+`;
+
+const GridDiv = styled.div`
+  position: absolute;
+  left: 0px;
+  top: 0px;
+  width: 100%;
+  height: 100%;
+  display: grid;
+  grid-gap: 0;
+  grid-template-columns: 1fr 1fr;
+  align-items: center;
+  border-radius: ${defaultBorderRadius};
+  overflow: hidden;
+
+  background-color: ${secondaryDark};
+`;
+
+const ButtonDiv = styled.div<{ backgroundColor: string }>`
+  background-color: ${secondaryDark};
+  padding: 3px;
+  height: 100%;
+  display: flex;
+  align-items: center;
 
   &:hover {
-    background-color: white;
-    color: black;
+    background-color: ${(props) => props.backgroundColor};
   }
-
-  cursor: pointer;
 `;
 
 interface PillButtonProps {
   onHover?: (arg0: boolean) => void;
   onDelete?: () => void;
   title: string;
+  index: number;
+  editing: boolean;
+  pinned: boolean;
 }
 
 const PillButton = (props: PillButtonProps) => {
   const [isHovered, setIsHovered] = useState(false);
+  const client = useContext(GUIClientContext);
+
   return (
-    <Button
-      onMouseEnter={() => {
-        setIsHovered(true);
-        if (props.onHover) {
-          props.onHover(true);
-        }
-      }}
-      onMouseLeave={() => {
-        setIsHovered(false);
-        if (props.onHover) {
-          props.onHover(false);
-        }
-      }}
-      onClick={() => {
-        if (props.onDelete) {
-          props.onDelete();
-        }
-      }}
-    >
-      {props.title}
-    </Button>
+    <>
+      <Button
+        style={{
+          position: "relative",
+          borderColor: props.editing
+            ? "#8800aa"
+            : props.pinned
+            ? "#ffff0099"
+            : "transparent",
+          borderWidth: "1px",
+          borderStyle: "solid",
+        }}
+        onMouseEnter={() => {
+          setIsHovered(true);
+          if (props.onHover) {
+            props.onHover(true);
+          }
+        }}
+        onMouseLeave={() => {
+          setIsHovered(false);
+          if (props.onHover) {
+            props.onHover(false);
+          }
+        }}
+      >
+        {isHovered && (
+          <GridDiv>
+            <ButtonDiv
+              data-tooltip-id={`edit-${props.index}`}
+              backgroundColor={"#8800aa55"}
+              onClick={() => {
+                client?.setEditingAtIndices([props.index]);
+              }}
+            >
+              <PaintBrush style={{ margin: "auto" }} width="1.6em"></PaintBrush>
+            </ButtonDiv>
+
+            {/* <ButtonDiv
+            data-tooltip-id={`pin-${props.index}`}
+            backgroundColor={"#ffff0055"}
+            onClick={() => {
+              client?.setPinnedAtIndices([props.index]);
+            }}
+          >
+            <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>
+          </ButtonDiv> */}
+            <StyledTooltip id={`pin-${props.index}`}>
+              Edit this range
+            </StyledTooltip>
+            <ButtonDiv
+              data-tooltip-id={`delete-${props.index}`}
+              backgroundColor={"#cc000055"}
+              onClick={() => {
+                if (props.onDelete) {
+                  props.onDelete();
+                }
+              }}
+            >
+              <Trash style={{ margin: "auto" }} width="1.6em"></Trash>
+            </ButtonDiv>
+          </GridDiv>
+        )}
+        {props.title}
+      </Button>
+      <StyledTooltip id={`edit-${props.index}`}>
+        {props.editing ? "Editing this range" : "Edit this range"}
+      </StyledTooltip>
+      <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
+    </>
   );
 };
 
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 2aed2e72..91d7b8ef 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -13,7 +13,7 @@ import {
   ArrowPath,
   XMark,
 } from "@styled-icons/heroicons-outline";
-import { Stop } from "@styled-icons/heroicons-solid";
+import { StopCircle } from "@styled-icons/heroicons-solid";
 import { HistoryNode } from "../../../schema/HistoryNode";
 import ReactMarkdown from "react-markdown";
 import HeaderButtonWithText from "./HeaderButtonWithText";
@@ -67,7 +67,6 @@ const HeaderDiv = styled.div<{ error: boolean; loading: boolean }>`
 
 const ContentDiv = styled.div<{ isUserInput: boolean }>`
   padding: 8px;
-  padding-left: 16px;
   background-color: ${(props) =>
     props.isUserInput ? secondaryDark : vscBackground};
   font-size: 13px;
@@ -167,7 +166,7 @@ function StepContainer(props: StepContainerProps) {
               ? "#f00"
               : props.historyNode.active
               ? undefined
-              : "white"
+              : "transparent"
           }
           className="overflow-hidden cursor-pointer"
           onClick={(e) => {
@@ -182,7 +181,7 @@ function StepContainer(props: StepContainerProps) {
             loading={props.historyNode.active as boolean | false}
             error={props.historyNode.observation?.error ? true : false}
           >
-            <h4 className="m-2">
+            <div className="m-2">
               {!isUserInput &&
                 (props.open ? (
                   <ChevronDown size="1.4em" />
@@ -191,7 +190,7 @@ function StepContainer(props: StepContainerProps) {
                 ))}
               {props.historyNode.observation?.title ||
                 (props.historyNode.step.name as any)}
-            </h4>
+            </div>
             {/* <HeaderButton
               onClick={(e) => {
                 e.stopPropagation();
@@ -203,16 +202,14 @@ function StepContainer(props: StepContainerProps) {
 
             <>
               <HeaderButtonWithText
-                disabled={props.historyNode.active as boolean}
                 onClick={(e) => {
                   e.stopPropagation();
                   props.onDelete();
                 }}
                 text={props.historyNode.active ? "Stop" : "Delete"}
-                active={props.historyNode.active}
               >
                 {props.historyNode.active ? (
-                  <Stop size="1.2em" onClick={props.onDelete} />
+                  <StopCircle size="1.6em" onClick={props.onDelete} />
                 ) : (
                   <XMark size="1.6em" onClick={props.onDelete} />
                 )}
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index a564f884..ea5727f0 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -8,6 +8,7 @@ const ScreenCover = styled.div`
   width: 100%;
   height: 100%;
   background-color: rgba(168, 168, 168, 0.5);
+  z-index: 100;
 `;
 
 const DialogContainer = styled.div`
@@ -35,7 +36,6 @@ const TextArea = styled.textarea`
   border-radius: 8px;
   padding: 8px;
   outline: 1px solid black;
-  font-family: Arial, Helvetica, sans-serif;
   resize: none;
 
   &:focus {
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 28437d35..f51f0cb5 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -15,12 +15,10 @@ interface UserInputContainerProps {
 }
 
 const StyledDiv = styled.div`
-  background-color: rgb(45 45 45);
+  background-color: ${secondaryDark};
   padding: 8px;
   padding-left: 16px;
   padding-right: 16px;
-  border-bottom: 1px solid white;
-  border-top: 1px solid white;
   font-size: 13px;
   display: flex;
   align-items: center;
@@ -29,7 +27,7 @@ const StyledDiv = styled.div`
 const UserInputContainer = (props: UserInputContainerProps) => {
   return (
     <StyledDiv>
-      <b>{props.children}</b>
+      {props.children}
       <div style={{ marginLeft: "auto" }}>
         <HeaderButtonWithText
           onClick={(e) => {
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index db1925ed..9ae0f097 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -1,7 +1,9 @@
+import { Tooltip } from "react-tooltip";
 import styled, { keyframes } from "styled-components";
 
 export const defaultBorderRadius = "5px";
-export const secondaryDark = "rgb(42 42 42)";
+export const lightGray = "rgb(100 100 100)";
+export const secondaryDark = "rgb(45 45 45)";
 export const vscBackground = "rgb(30 30 30)";
 export const vscBackgroundTransparent = "#1e1e1ede";
 export const buttonColor = "rgb(113 28 59)";
@@ -26,6 +28,16 @@ export const Button = styled.button`
   }
 `;
 
+export const StyledTooltip = styled(Tooltip)`
+  font-size: 12px;
+  background-color: rgb(60 60 60);
+  border-radius: ${defaultBorderRadius};
+  padding: 6px;
+  padding-left: 12px;
+  padding-right: 12px;
+  z-index: 100;
+`;
+
 export const TextArea = styled.textarea`
   width: 100%;
   border-radius: ${defaultBorderRadius};
@@ -128,19 +140,17 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>`
   background-color: ${({ inverted }) => (inverted ? "white" : "transparent")};
   color: ${({ inverted }) => (inverted ? "black" : "white")};
 
-  border: 1px solid white;
+  border: none;
   border-radius: ${defaultBorderRadius};
   cursor: pointer;
 
   &:hover {
     background-color: ${({ inverted }) =>
-      typeof inverted === "undefined" || inverted ? "white" : "transparent"};
-    color: ${({ inverted }) =>
-      typeof inverted === "undefined" || inverted ? "black" : "white"};
+      typeof inverted === "undefined" || inverted ? lightGray : "transparent"};
   }
   display: flex;
   align-items: center;
   justify-content: center;
   gap: 4px;
-  padding: 1px;
+  padding: 2px;
 `;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index f123bb2b..a179c2bf 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -23,6 +23,10 @@ abstract class AbstractContinueGUIClientProtocol {
 
   abstract deleteContextAtIndices(indices: number[]): void;
 
+  abstract setEditingAtIndices(indices: number[]): void;
+
+  abstract setPinnedAtIndices(indices: number[]): void;
+
   abstract toggleAddingHighlightedCode(): void;
 }
 
diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
index 49f200ae..2060dd7f 100644
--- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts
+++ b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
@@ -75,6 +75,14 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
     this.messenger.send("delete_context_at_indices", { indices });
   }
 
+  setEditingAtIndices(indices: number[]) {
+    this.messenger.send("set_editing_at_indices", { indices });
+  }
+
+  setPinnedAtIndices(indices: number[]) {
+    this.messenger.send("set_pinned_at_indices", { indices });
+  }
+
   toggleAddingHighlightedCode(): void {
     this.messenger.send("toggle_adding_highlighted_code", {});
   }
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index 6dc514ec..682551f8 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -10,19 +10,12 @@
   --def-border-radius: 5px;
 }
 
-@font-face {
-  font-family: "Mona Sans";
-  src: url("assets/Mona-Sans.woff2") format("woff2 supports variations"),
-    url("assets/Mona-Sans.woff2") format("woff2-variations");
-  font-weight: 200 900;
-  font-stretch: 75% 85%;
-}
-
 html,
 body,
 #root {
   height: 100%;
   background-color: var(--vsc-background);
+  font-family: "Lexend", sans-serif;
 }
 
 body {
diff --git a/extension/react-app/src/main.tsx b/extension/react-app/src/main.tsx
index 0b02575c..a76bced6 100644
--- a/extension/react-app/src/main.tsx
+++ b/extension/react-app/src/main.tsx
@@ -1,6 +1,8 @@
 import React from "react";
 import ReactDOM from "react-dom/client";
 import App from "./App";
+import { Provider } from "react-redux";
+import store from "./redux/store";
 import "./index.css";
 
 import posthog from "posthog-js";
@@ -17,7 +19,9 @@ posthog.init("phc_JS6XFROuNbhJtVCEdTSYk6gl5ArRrTNMpCcguAXlSPs", {
 ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
   <React.StrictMode>
     <PostHogProvider client={posthog}>
-      <App />
+      <Provider store={store}>
+        <App />
+      </Provider>
     </PostHogProvider>
   </React.StrictMode>
 );
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index e5320c6a..3cce30de 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -1,11 +1,13 @@
 import styled from "styled-components";
-import { defaultBorderRadius, Loader } from "../components";
+import { defaultBorderRadius } from "../components";
+import Loader from "../components/Loader";
 import ContinueButton from "../components/ContinueButton";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { FullState, HighlightedRangeContext } 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 useContinueGUIProtocol from "../hooks/useWebsocket";
+import { GUIClientContext } from "../App";
 import {
   BookOpen,
   ChatBubbleOvalLeftEllipsis,
@@ -52,6 +54,7 @@ interface GUIProps {
 }
 
 function GUI(props: GUIProps) {
+  const client = useContext(GUIClientContext);
   const posthog = usePostHog();
   const vscMachineId = useSelector(
     (state: RootStore) => state.config.vscMachineId
@@ -70,7 +73,9 @@ function GUI(props: GUIProps) {
   const [usingFastModel, setUsingFastModel] = useState(false);
   const [waitingForSteps, setWaitingForSteps] = useState(false);
   const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
-  const [highlightedRanges, setHighlightedRanges] = useState([]);
+  const [highlightedRanges, setHighlightedRanges] = useState<
+    HighlightedRangeContext[]
+  >([]);
   const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
   const [availableSlashCommands, setAvailableSlashCommands] = useState<
     { name: string; description: string }[]
@@ -112,7 +117,6 @@ function GUI(props: GUIProps) {
   const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
 
   const topGuiDivRef = useRef<HTMLDivElement>(null);
-  const client = useContinueGUIProtocol();
 
   const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
     null
@@ -148,7 +152,7 @@ function GUI(props: GUIProps) {
   }, []);
 
   useEffect(() => {
-    client?.onStateUpdate((state) => {
+    client?.onStateUpdate((state: FullState) => {
       // Scroll only if user is at very bottom of the window.
       setUsingFastModel(state.default_model === "gpt-3.5-turbo");
       const shouldScrollToBottom =
@@ -289,7 +293,7 @@ function GUI(props: GUIProps) {
       >
         {typeof client === "undefined" && (
           <>
-            <Loader></Loader>
+            <Loader />
             <p style={{ textAlign: "center" }}>Loading Continue server...</p>
           </>
         )}
@@ -316,7 +320,8 @@ function GUI(props: GUIProps) {
                 setStepsOpen(nextStepsOpen);
               }}
               onToggleAll={() => {
-                setStepsOpen((prev) => prev.map((_, index) => !prev[index]));
+                const shouldOpen = !stepsOpen[index];
+                setStepsOpen((prev) => prev.map(() => shouldOpen));
               }}
               key={index}
               onUserInput={(input: string) => {
@@ -381,6 +386,7 @@ function GUI(props: GUIProps) {
           borderRadius: defaultBorderRadius,
           padding: "16px",
           margin: "16px",
+          zIndex: 100,
         }}
         hidden={!showDataSharingInfo}
       >
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
new file mode 100644
index 00000000..981e772e
--- /dev/null
+++ b/extension/schema/FullState.d.ts
@@ -0,0 +1,133 @@
+/* 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 FullState = FullState1;
+export type Name = string;
+export type Hide = boolean;
+export type Description = string;
+export type SystemMessage = string;
+export type Role = "assistant" | "user" | "system" | "function";
+export type Content = string;
+export type Name1 = string;
+export type Summary = string;
+export type Name2 = string;
+export type Arguments = string;
+export type ChatContext = ChatMessage[];
+export type ManageOwnChatContext = boolean;
+export type Depth = number;
+export type Deleted = boolean;
+export type Active = boolean;
+export type Timeline = HistoryNode[];
+export type CurrentIndex = number;
+export type Active1 = boolean;
+export type UserInputQueue = string[];
+export type DefaultModel = string;
+export type Filepath = string;
+export type Line = number;
+export type Character = number;
+export type Contents = string;
+export type Editing = boolean;
+export type Pinned = boolean;
+export type HighlightedRanges = HighlightedRangeContext[];
+export type Name3 = string;
+export type Description1 = string;
+export type SlashCommands = SlashCommandDescription[];
+export type AddingHighlightedCode = boolean;
+
+/**
+ * A full state of the program, including the history
+ */
+export interface FullState1 {
+  history: History;
+  active: Active1;
+  user_input_queue: UserInputQueue;
+  default_model: DefaultModel;
+  highlighted_ranges: HighlightedRanges;
+  slash_commands: SlashCommands;
+  adding_highlighted_code: AddingHighlightedCode;
+  [k: string]: unknown;
+}
+/**
+ * A history of steps taken and their results
+ */
+export interface History {
+  timeline: Timeline;
+  current_index: CurrentIndex;
+  [k: string]: unknown;
+}
+/**
+ * A point in history, a list of which make up History
+ */
+export interface HistoryNode {
+  step: Step;
+  observation?: Observation;
+  depth: Depth;
+  deleted?: Deleted;
+  active?: Active;
+  [k: string]: unknown;
+}
+export interface Step {
+  name?: Name;
+  hide?: Hide;
+  description?: Description;
+  system_message?: SystemMessage;
+  chat_context?: ChatContext;
+  manage_own_chat_context?: ManageOwnChatContext;
+  [k: string]: unknown;
+}
+export interface ChatMessage {
+  role: Role;
+  content?: Content;
+  name?: Name1;
+  summary: Summary;
+  function_call?: FunctionCall;
+  [k: string]: unknown;
+}
+export interface FunctionCall {
+  name: Name2;
+  arguments: Arguments;
+  [k: string]: unknown;
+}
+export interface Observation {
+  [k: string]: unknown;
+}
+/**
+ * Context for a highlighted range
+ */
+export interface HighlightedRangeContext {
+  range: RangeInFileWithContents;
+  editing: Editing;
+  pinned: Pinned;
+  [k: string]: unknown;
+}
+/**
+ * A range in a file with the contents of the range.
+ */
+export interface RangeInFileWithContents {
+  filepath: Filepath;
+  range: Range;
+  contents: Contents;
+  [k: string]: unknown;
+}
+/**
+ * A range in a file. 0-indexed.
+ */
+export interface Range {
+  start: Position;
+  end: Position;
+  [k: string]: unknown;
+}
+export interface Position {
+  line: Line;
+  character: Character;
+  [k: string]: unknown;
+}
+export interface SlashCommandDescription {
+  name: Name3;
+  description: Description1;
+  [k: string]: unknown;
+}
diff --git a/extension/src/debugPanel.ts b/extension/src/debugPanel.ts
index b88c86f3..487bbedf 100644
--- a/extension/src/debugPanel.ts
+++ b/extension/src/debugPanel.ts
@@ -341,6 +341,10 @@ export function setupDebugPanel(
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <script>const vscode = acquireVsCodeApi();</script>
         <link href="${styleMainUri}" rel="stylesheet">
+
+        <link rel="preconnect" href="https://fonts.googleapis.com">
+        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+        <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300&display=swap" rel="stylesheet">
         
         <title>Continue</title>
       </head>
diff --git a/schema/json/FullState.json b/schema/json/FullState.json
new file mode 100644
index 00000000..af0f25e1
--- /dev/null
+++ b/schema/json/FullState.json
@@ -0,0 +1,304 @@
+{
+  "title": "FullState",
+  "$ref": "#/definitions/src__continuedev__core__main__FullState",
+  "definitions": {
+    "FunctionCall": {
+      "title": "FunctionCall",
+      "type": "object",
+      "properties": {
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "arguments": {
+          "title": "Arguments",
+          "type": "string"
+        }
+      },
+      "required": [
+        "name",
+        "arguments"
+      ]
+    },
+    "ChatMessage": {
+      "title": "ChatMessage",
+      "type": "object",
+      "properties": {
+        "role": {
+          "title": "Role",
+          "enum": [
+            "assistant",
+            "user",
+            "system",
+            "function"
+          ],
+          "type": "string"
+        },
+        "content": {
+          "title": "Content",
+          "type": "string"
+        },
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "summary": {
+          "title": "Summary",
+          "type": "string"
+        },
+        "function_call": {
+          "$ref": "#/definitions/FunctionCall"
+        }
+      },
+      "required": [
+        "role",
+        "summary"
+      ]
+    },
+    "Step": {
+      "title": "Step",
+      "type": "object",
+      "properties": {
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "hide": {
+          "title": "Hide",
+          "default": false,
+          "type": "boolean"
+        },
+        "description": {
+          "title": "Description",
+          "type": "string"
+        },
+        "system_message": {
+          "title": "System Message",
+          "type": "string"
+        },
+        "chat_context": {
+          "title": "Chat Context",
+          "default": [],
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ChatMessage"
+          }
+        },
+        "manage_own_chat_context": {
+          "title": "Manage Own Chat Context",
+          "default": false,
+          "type": "boolean"
+        }
+      }
+    },
+    "Observation": {
+      "title": "Observation",
+      "type": "object",
+      "properties": {}
+    },
+    "HistoryNode": {
+      "title": "HistoryNode",
+      "description": "A point in history, a list of which make up History",
+      "type": "object",
+      "properties": {
+        "step": {
+          "$ref": "#/definitions/Step"
+        },
+        "observation": {
+          "$ref": "#/definitions/Observation"
+        },
+        "depth": {
+          "title": "Depth",
+          "type": "integer"
+        },
+        "deleted": {
+          "title": "Deleted",
+          "default": false,
+          "type": "boolean"
+        },
+        "active": {
+          "title": "Active",
+          "default": true,
+          "type": "boolean"
+        }
+      },
+      "required": [
+        "step",
+        "depth"
+      ]
+    },
+    "History": {
+      "title": "History",
+      "description": "A history of steps taken and their results",
+      "type": "object",
+      "properties": {
+        "timeline": {
+          "title": "Timeline",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/HistoryNode"
+          }
+        },
+        "current_index": {
+          "title": "Current Index",
+          "type": "integer"
+        }
+      },
+      "required": [
+        "timeline",
+        "current_index"
+      ]
+    },
+    "Position": {
+      "title": "Position",
+      "type": "object",
+      "properties": {
+        "line": {
+          "title": "Line",
+          "type": "integer"
+        },
+        "character": {
+          "title": "Character",
+          "type": "integer"
+        }
+      },
+      "required": [
+        "line",
+        "character"
+      ]
+    },
+    "Range": {
+      "title": "Range",
+      "description": "A range in a file. 0-indexed.",
+      "type": "object",
+      "properties": {
+        "start": {
+          "$ref": "#/definitions/Position"
+        },
+        "end": {
+          "$ref": "#/definitions/Position"
+        }
+      },
+      "required": [
+        "start",
+        "end"
+      ]
+    },
+    "RangeInFileWithContents": {
+      "title": "RangeInFileWithContents",
+      "description": "A range in a file with the contents of the range.",
+      "type": "object",
+      "properties": {
+        "filepath": {
+          "title": "Filepath",
+          "type": "string"
+        },
+        "range": {
+          "$ref": "#/definitions/Range"
+        },
+        "contents": {
+          "title": "Contents",
+          "type": "string"
+        }
+      },
+      "required": [
+        "filepath",
+        "range",
+        "contents"
+      ]
+    },
+    "HighlightedRangeContext": {
+      "title": "HighlightedRangeContext",
+      "description": "Context for a highlighted range",
+      "type": "object",
+      "properties": {
+        "range": {
+          "$ref": "#/definitions/RangeInFileWithContents"
+        },
+        "editing": {
+          "title": "Editing",
+          "type": "boolean"
+        },
+        "pinned": {
+          "title": "Pinned",
+          "type": "boolean"
+        }
+      },
+      "required": [
+        "range",
+        "editing",
+        "pinned"
+      ]
+    },
+    "SlashCommandDescription": {
+      "title": "SlashCommandDescription",
+      "type": "object",
+      "properties": {
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "description": {
+          "title": "Description",
+          "type": "string"
+        }
+      },
+      "required": [
+        "name",
+        "description"
+      ]
+    },
+    "src__continuedev__core__main__FullState": {
+      "title": "FullState",
+      "description": "A full state of the program, including the history",
+      "type": "object",
+      "properties": {
+        "history": {
+          "$ref": "#/definitions/History"
+        },
+        "active": {
+          "title": "Active",
+          "type": "boolean"
+        },
+        "user_input_queue": {
+          "title": "User Input Queue",
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "default_model": {
+          "title": "Default Model",
+          "type": "string"
+        },
+        "highlighted_ranges": {
+          "title": "Highlighted Ranges",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/HighlightedRangeContext"
+          }
+        },
+        "slash_commands": {
+          "title": "Slash Commands",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/SlashCommandDescription"
+          }
+        },
+        "adding_highlighted_code": {
+          "title": "Adding Highlighted Code",
+          "type": "boolean"
+        }
+      },
+      "required": [
+        "history",
+        "active",
+        "user_input_queue",
+        "default_model",
+        "highlighted_ranges",
+        "slash_commands",
+        "adding_highlighted_code"
+      ]
+    }
+  }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 48e5c8001e897eb37493357087410ee8f98217fa Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 15 Jul 2023 14:30:11 -0700
Subject: ctrl shortcuts on windows, load models immediately

---
 continuedev/src/continuedev/core/autopilot.py      | 10 ++--
 continuedev/src/continuedev/core/sdk.py            | 59 +++++++++++++++-------
 .../src/continuedev/libs/llm/hf_inference_api.py   |  6 ++-
 continuedev/src/continuedev/server/ide.py          |  4 +-
 .../src/continuedev/server/session_manager.py      |  6 +--
 .../react-app/src/components/StepContainer.tsx     |  2 +-
 extension/react-app/src/components/TextDialog.tsx  |  6 ++-
 extension/react-app/src/pages/gui.tsx              |  6 +--
 extension/react-app/src/util/index.ts              | 30 +++++++++++
 9 files changed, 98 insertions(+), 31 deletions(-)
 create mode 100644 extension/react-app/src/util/index.ts

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index 82439f49..0696c360 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -50,6 +50,8 @@ class Autopilot(ContinueBaseModel):
     full_state: Union[FullState, None] = None
     _on_update_callbacks: List[Callable[[FullState], None]] = []
 
+    continue_sdk: ContinueSDK = None
+
     _active: bool = False
     _should_halt: bool = False
     _main_user_input_queue: List[str] = []
@@ -57,9 +59,11 @@ class Autopilot(ContinueBaseModel):
     _user_input_queue = AsyncSubscriptionQueue()
     _retry_queue = AsyncSubscriptionQueue()
 
-    @cached_property
-    def continue_sdk(self) -> ContinueSDK:
-        return ContinueSDK(self)
+    @classmethod
+    async def create(cls, policy: Policy, ide: AbstractIdeProtocolServer, full_state: FullState) -> "Autopilot":
+        autopilot = cls(ide=ide, policy=policy)
+        autopilot.continue_sdk = await ContinueSDK.create(autopilot)
+        return autopilot
 
     class Config:
         arbitrary_types_allowed = True
diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py
index aa2d8892..d73561d2 100644
--- a/continuedev/src/continuedev/core/sdk.py
+++ b/continuedev/src/continuedev/core/sdk.py
@@ -1,6 +1,6 @@
 import asyncio
 from functools import cached_property
-from typing import Coroutine, Union
+from typing import Coroutine, Dict, Union
 import os
 
 from ..steps.core.core import DefaultModelEditCodeStep
@@ -13,7 +13,7 @@ from ..libs.llm.hf_inference_api import HuggingFaceInferenceAPI
 from ..libs.llm.openai import OpenAI
 from .observation import Observation
 from ..server.ide_protocol import AbstractIdeProtocolServer
-from .main import Context, ContinueCustomException, HighlightedRangeContext, History, Step, ChatMessage, ChatMessageRole
+from .main import Context, ContinueCustomException, History, Step, ChatMessage
 from ..steps.core.core import *
 from ..libs.llm.proxy_server import ProxyServer
 
@@ -22,26 +22,46 @@ class Autopilot:
     pass
 
 
+ModelProvider = Literal["openai", "hf_inference_api", "ggml", "anthropic"]
+MODEL_PROVIDER_TO_ENV_VAR = {
+    "openai": "OPENAI_API_KEY",
+    "hf_inference_api": "HUGGING_FACE_TOKEN",
+    "anthropic": "ANTHROPIC_API_KEY"
+}
+
+
 class Models:
-    def __init__(self, sdk: "ContinueSDK"):
+    provider_keys: Dict[ModelProvider, str] = {}
+    model_providers: List[ModelProvider]
+
+    def __init__(self, sdk: "ContinueSDK", model_providers: List[ModelProvider]):
         self.sdk = sdk
+        self.model_providers = model_providers
+
+    @classmethod
+    async def create(cls, sdk: "ContinueSDK", with_providers: List[ModelProvider] = ["openai"]) -> "Models":
+        models = Models(sdk, with_providers)
+        for provider in with_providers:
+            if provider in MODEL_PROVIDER_TO_ENV_VAR:
+                env_var = MODEL_PROVIDER_TO_ENV_VAR[provider]
+                models.provider_keys[provider] = await sdk.get_user_secret(
+                    env_var, f'Please add your {env_var} to the .env file')
+
+        return models
 
     def __load_openai_model(self, model: str) -> OpenAI:
-        async def load_openai_model():
-            api_key = await self.sdk.get_user_secret(
-                'OPENAI_API_KEY', 'Enter your OpenAI API key or press enter to try for free')
-            if api_key == "":
-                return ProxyServer(self.sdk.ide.unique_id, model)
-            return OpenAI(api_key=api_key, default_model=model)
-        return asyncio.get_event_loop().run_until_complete(load_openai_model())
+        api_key = self.provider_keys["openai"]
+        if api_key == "":
+            return ProxyServer(self.sdk.ide.unique_id, model)
+        return OpenAI(api_key=api_key, default_model=model)
+
+    def __load_hf_inference_api_model(self, model: str) -> HuggingFaceInferenceAPI:
+        api_key = self.provider_keys["hf_inference_api"]
+        return HuggingFaceInferenceAPI(api_key=api_key, model=model)
 
     @cached_property
     def starcoder(self):
-        async def load_starcoder():
-            api_key = await self.sdk.get_user_secret(
-                'HUGGING_FACE_TOKEN', 'Please add your Hugging Face token to the .env file')
-            return HuggingFaceInferenceAPI(api_key=api_key)
-        return asyncio.get_event_loop().run_until_complete(load_starcoder())
+        return self.__load_hf_inference_api_model("bigcode/starcoder")
 
     @cached_property
     def gpt35(self):
@@ -74,7 +94,7 @@ class Models:
     @property
     def default(self):
         default_model = self.sdk.config.default_model
-        return self.__model_from_name(default_model) if default_model is not None else self.gpt35
+        return self.__model_from_name(default_model) if default_model is not None else self.gpt4
 
 
 class ContinueSDK(AbstractContinueSDK):
@@ -87,10 +107,15 @@ class ContinueSDK(AbstractContinueSDK):
     def __init__(self, autopilot: Autopilot):
         self.ide = autopilot.ide
         self.__autopilot = autopilot
-        self.models = Models(self)
         self.context = autopilot.context
         self.config = self._load_config()
 
+    @classmethod
+    async def create(cls, autopilot: Autopilot) -> "ContinueSDK":
+        sdk = ContinueSDK(autopilot)
+        sdk.models = await Models.create(sdk)
+        return sdk
+
     config: ContinueConfig
 
     def _load_config(self) -> ContinueConfig:
diff --git a/continuedev/src/continuedev/libs/llm/hf_inference_api.py b/continuedev/src/continuedev/libs/llm/hf_inference_api.py
index 1586c620..803ba122 100644
--- a/continuedev/src/continuedev/libs/llm/hf_inference_api.py
+++ b/continuedev/src/continuedev/libs/llm/hf_inference_api.py
@@ -9,7 +9,11 @@ DEFAULT_MAX_TIME = 120.
 
 class HuggingFaceInferenceAPI(LLM):
     api_key: str
-    model: str = "bigcode/starcoder"
+    model: str
+
+    def __init__(self, api_key: str, model: str):
+        self.api_key = api_key
+        self.model = model
 
     def complete(self, prompt: str, with_history: List[ChatMessage] = [], **kwargs):
         """Return the completion of the text with the given temperature."""
diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py
index 7875c94d..77b13483 100644
--- a/continuedev/src/continuedev/server/ide.py
+++ b/continuedev/src/continuedev/server/ide.py
@@ -227,8 +227,8 @@ class IdeProtocolServer(AbstractIdeProtocolServer):
         })
 
     async def getSessionId(self):
-        session_id = self.session_manager.new_session(
-            self, self.session_id).session_id
+        session_id = (await self.session_manager.new_session(
+            self, self.session_id)).session_id
         await self._send_json("getSessionId", {
             "sessionId": session_id
         })
diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py
index fb8ac386..6d109ca6 100644
--- a/continuedev/src/continuedev/server/session_manager.py
+++ b/continuedev/src/continuedev/server/session_manager.py
@@ -53,18 +53,18 @@ class SessionManager:
             session_files = os.listdir(sessions_folder)
             if f"{session_id}.json" in session_files and session_id in self.registered_ides:
                 if self.registered_ides[session_id].session_id is not None:
-                    return self.new_session(self.registered_ides[session_id], session_id=session_id)
+                    return await self.new_session(self.registered_ides[session_id], session_id=session_id)
 
             raise KeyError("Session ID not recognized", session_id)
         return self.sessions[session_id]
 
-    def new_session(self, ide: AbstractIdeProtocolServer, session_id: Union[str, None] = None) -> Session:
+    async def new_session(self, ide: AbstractIdeProtocolServer, session_id: Union[str, None] = None) -> Session:
         full_state = None
         if session_id is not None and os.path.exists(getSessionFilePath(session_id)):
             with open(getSessionFilePath(session_id), "r") as f:
                 full_state = FullState(**json.load(f))
 
-        autopilot = DemoAutopilot(
+        autopilot = await DemoAutopilot.create(
             policy=DemoPolicy(), ide=ide, full_state=full_state)
         session_id = session_id or str(uuid4())
         ide.session_id = session_id
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 14e9b854..7f23e333 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -181,7 +181,7 @@ function StepContainer(props: StepContainerProps) {
           }
           className="overflow-hidden cursor-pointer"
           onClick={(e) => {
-            if (e.metaKey) {
+            if (isMetaEquivalentKeyPressed(e)) {
               props.onToggleAll();
             } else {
               props.onToggle();
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index ea5727f0..c724697d 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -81,7 +81,11 @@ const TextDialog = (props: {
             rows={10}
             ref={textAreaRef}
             onKeyDown={(e) => {
-              if (e.key === "Enter" && e.metaKey && textAreaRef.current) {
+              if (
+                e.key === "Enter" &&
+                isMetaEquivalentKeyPressed(e) &&
+                textAreaRef.current
+              ) {
                 props.onEnter(textAreaRef.current.value);
                 setText("");
               } else if (e.key === "Escape") {
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 57cebac3..cb0404ab 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -137,12 +137,12 @@ function GUI(props: GUIProps) {
   useEffect(() => {
     const listener = (e: any) => {
       // Cmd + i to toggle fast model
-      if (e.key === "i" && e.metaKey && e.shiftKey) {
+      if (e.key === "i" && isMetaEquivalentKeyPressed(e) && e.shiftKey) {
         setUsingFastModel((prev) => !prev);
         // Cmd + backspace to stop currently running step
       } else if (
         e.key === "Backspace" &&
-        e.metaKey &&
+        isMetaEquivalentKeyPressed(e) &&
         typeof history?.current_index !== "undefined" &&
         history.timeline[history.current_index]?.active
       ) {
@@ -220,7 +220,7 @@ function GUI(props: GUIProps) {
     if (mainTextInputRef.current) {
       let input = (mainTextInputRef.current as any).inputValue;
       // cmd+enter to /edit
-      if (event?.metaKey) {
+      if (isMetaEquivalentKeyPressed(event)) {
         input = `/edit ${input}`;
       }
       (mainTextInputRef.current as any).setInputValue("");
diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts
new file mode 100644
index 00000000..ad711321
--- /dev/null
+++ b/extension/react-app/src/util/index.ts
@@ -0,0 +1,30 @@
+type Platform = "mac" | "linux" | "windows" | "unknown";
+
+function getPlatform(): Platform {
+  const platform = window.navigator.platform.toUpperCase();
+  if (platform.indexOf("MAC") >= 0) {
+    return "mac";
+  } else if (platform.indexOf("LINUX") >= 0) {
+    return "linux";
+  } else if (platform.indexOf("WIN") >= 0) {
+    return "windows";
+  } else {
+    return "unknown";
+  }
+}
+
+function isMetaEquivalentKeyPressed(event: {
+  metaKey: boolean;
+  ctrlKey: boolean;
+}): boolean {
+  const platform = getPlatform();
+  switch (platform) {
+    case "mac":
+      return event.metaKey;
+    case "linux":
+    case "windows":
+      return event.ctrlKey;
+    default:
+      return event.metaKey;
+  }
+}
-- 
cgit v1.2.3-70-g09d2


From 3a39b979c55b005d9bb18b88b43ca7293ee5410d Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 15 Jul 2023 16:32:56 -0700
Subject: patch

---
 extension/package-lock.json                       |  4 ++--
 extension/package.json                            |  2 +-
 extension/react-app/src/components/TextDialog.tsx |  1 +
 extension/react-app/src/pages/gui.tsx             |  1 +
 extension/src/activation/environmentSetup.ts      | 19 ++++++++-----------
 5 files changed, 13 insertions(+), 14 deletions(-)

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/extension/package-lock.json b/extension/package-lock.json
index 0edd4885..e77bfac2 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "continue",
-  "version": "0.0.166",
+  "version": "0.0.167",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "continue",
-      "version": "0.0.166",
+      "version": "0.0.167",
       "license": "Apache-2.0",
       "dependencies": {
         "@electron/rebuild": "^3.2.10",
diff --git a/extension/package.json b/extension/package.json
index 7cd7b793..bbd18b12 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.166",
+  "version": "0.0.167",
   "publisher": "Continue",
   "engines": {
     "vscode": "^1.67.0"
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index c724697d..646d6846 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -2,6 +2,7 @@
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import { Button, buttonColor, secondaryDark, vscBackground } from ".";
+import { isMetaEquivalentKeyPressed } from "../util";
 
 const ScreenCover = styled.div`
   position: absolute;
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index cb0404ab..64207487 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -23,6 +23,7 @@ import { RootStore } from "../redux/store";
 import { postVscMessage } from "../vscode";
 import UserInputContainer from "../components/UserInputContainer";
 import Onboarding from "../components/Onboarding";
+import { isMetaEquivalentKeyPressed } from "../util";
 
 const TopGUIDiv = styled.div`
   overflow: hidden;
diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts
index 374c38c0..6a66532e 100644
--- a/extension/src/activation/environmentSetup.ts
+++ b/extension/src/activation/environmentSetup.ts
@@ -11,7 +11,8 @@ import * as os from "os";
 import fkill from "fkill";
 import { sendTelemetryEvent, TelemetryEvent } from "../telemetry";
 
-const WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR = "A Python virtual enviroment cannot be activated because running scripts is disabled for this user. Please enable signed scripts to run with this command in PowerShell: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, reload VS Code, and then try again.";
+const WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR =
+  "A Python virtual enviroment cannot be activated because running scripts is disabled for this user. In order to use Continue, please enable signed scripts to run with this command in PowerShell: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, reload VS Code, and then try again.";
 
 const MAX_RETRIES = 3;
 async function retryThenFail(
@@ -26,7 +27,8 @@ async function retryThenFail(
     }
 
     // Show corresponding error message depending on the platform
-    let msg = "Failed to set up Continue extension. Please email nate@continue.dev and we'll get this fixed ASAP!";
+    let msg =
+      "Failed to set up Continue extension. Please email nate@continue.dev and we'll get this fixed ASAP!";
     try {
       switch (process.platform) {
         case "win32":
@@ -35,14 +37,14 @@ async function retryThenFail(
         case "darwin":
           break;
         case "linux":
-          const [pythonCmd,] = await getPythonPipCommands();
+          const [pythonCmd] = await getPythonPipCommands();
           msg = await getLinuxAptInstallError(pythonCmd);
           break;
       }
     } finally {
       vscode.window.showErrorMessage(msg);
     }
-    
+
     sendTelemetryEvent(TelemetryEvent.ExtensionSetupError, {
       error: e.message,
     });
@@ -216,10 +218,7 @@ async function getLinuxAptInstallError(pythonCmd: string) {
   const version = stdout.split(" ")[1].split(".")[1];
   const installVenvCommand = `apt-get install python3.${version}-venv`;
   await runCommand("apt-get update");
-  // Ask the user to run the command to install python3-venv (requires sudo, so we can't)
-  // First, get the python version
-  const msg = `[Important] Continue needs to create a Python virtual environment, but python3.${version}-venv is not installed. Please run this command in your terminal: \`${installVenvCommand}\`, reload VS Code, and then try again.`;
-  return msg;
+  return `[Important] Continue needs to create a Python virtual environment, but python3.${version}-venv is not installed. Please run this command in your terminal: \`${installVenvCommand}\`, reload VS Code, and then try again.`;
 }
 
 async function setupPythonEnv() {
@@ -246,9 +245,7 @@ async function setupPythonEnv() {
       stderr &&
       stderr.includes("running scripts is disabled on this system")
     ) {
-      await vscode.window.showErrorMessage(
-        WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR
-      );
+      await vscode.window.showErrorMessage(WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR);
       throw new Error(stderr);
     } else if (
       stderr?.includes("On Debian/Ubuntu systems") ||
-- 
cgit v1.2.3-70-g09d2


From 08221a0879b4a163eab6860524f255dbcb4743ae Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Mon, 17 Jul 2023 12:05:03 -0700
Subject: match vscode color theme

---
 continuedev/src/continuedev/libs/util/dedent.py    | 25 -----------
 continuedev/src/continuedev/libs/util/strings.py   | 49 ++++++++++++++++++++++
 continuedev/src/continuedev/steps/chat.py          |  8 ++--
 continuedev/src/continuedev/steps/core/core.py     | 17 ++------
 extension/package-lock.json                        |  4 +-
 extension/package.json                             |  2 +-
 extension/react-app/src/components/ComboBox.tsx    | 10 +++--
 .../react-app/src/components/InputAndButton.tsx    | 10 ++---
 extension/react-app/src/components/PillButton.tsx  |  9 +++-
 .../react-app/src/components/StepContainer.tsx     | 12 ++++--
 extension/react-app/src/components/TextDialog.tsx  | 14 ++++---
 extension/react-app/src/components/index.ts        | 23 ++++++----
 extension/react-app/src/index.css                  |  4 +-
 extension/react-app/src/pages/gui.tsx              | 11 +++--
 14 files changed, 119 insertions(+), 79 deletions(-)
 delete mode 100644 continuedev/src/continuedev/libs/util/dedent.py
 create mode 100644 continuedev/src/continuedev/libs/util/strings.py

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/src/continuedev/libs/util/dedent.py b/continuedev/src/continuedev/libs/util/dedent.py
deleted file mode 100644
index e59c2e97..00000000
--- a/continuedev/src/continuedev/libs/util/dedent.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from typing import Tuple
-
-
-def dedent_and_get_common_whitespace(s: str) -> Tuple[str, str]:
-    lines = s.splitlines()
-    if len(lines) == 0:
-        return "", ""
-
-    # Longest common whitespace prefix
-    lcp = lines[0].split(lines[0].strip())[0]
-    # Iterate through the lines
-    for i in range(1, len(lines)):
-        # Empty lines are wildcards
-        if lines[i].strip() == "":
-            continue
-        # Iterate through the leading whitespace characters of the current line
-        for j in range(0, len(lcp)):
-            # If it doesn't have the same whitespace as lcp, then update lcp
-            if j >= len(lines[i]) or lcp[j] != lines[i][j]:
-                lcp = lcp[:j]
-                if lcp == "":
-                    return s, ""
-                break
-
-    return "\n".join(map(lambda x: x.lstrip(lcp), lines)), lcp
diff --git a/continuedev/src/continuedev/libs/util/strings.py b/continuedev/src/continuedev/libs/util/strings.py
new file mode 100644
index 00000000..f1fb8d0b
--- /dev/null
+++ b/continuedev/src/continuedev/libs/util/strings.py
@@ -0,0 +1,49 @@
+from typing import Tuple
+
+
+def dedent_and_get_common_whitespace(s: str) -> Tuple[str, str]:
+    lines = s.splitlines()
+    if len(lines) == 0:
+        return "", ""
+
+    # Longest common whitespace prefix
+    lcp = lines[0].split(lines[0].strip())[0]
+    # Iterate through the lines
+    for i in range(1, len(lines)):
+        # Empty lines are wildcards
+        if lines[i].strip() == "":
+            continue
+        # Iterate through the leading whitespace characters of the current line
+        for j in range(0, len(lcp)):
+            # If it doesn't have the same whitespace as lcp, then update lcp
+            if j >= len(lines[i]) or lcp[j] != lines[i][j]:
+                lcp = lcp[:j]
+                if lcp == "":
+                    return s, ""
+                break
+
+    return "\n".join(map(lambda x: x.lstrip(lcp), lines)), lcp
+
+
+def remove_quotes_and_escapes(output: str) -> str:
+    """
+    Clean up the output of the completion API, removing unnecessary escapes and quotes
+    """
+    output = output.strip()
+
+    # Replace smart quotes
+    output = output.replace("“", '"')
+    output = output.replace("”", '"')
+    output = output.replace("‘", "'")
+    output = output.replace("’", "'")
+
+    # Remove escapes
+    output = output.replace('\\"', '"')
+    output = output.replace("\\'", "'")
+    output = output.replace("\\n", "\n")
+    output = output.replace("\\t", "\t")
+    output = output.replace("\\\\", "\\")
+    if (output.startswith('"') and output.endswith('"')) or (output.startswith("'") and output.endswith("'")):
+        output = output[1:-1]
+
+    return output
diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py
index 3751dec2..7c6b42db 100644
--- a/continuedev/src/continuedev/steps/chat.py
+++ b/continuedev/src/continuedev/steps/chat.py
@@ -3,6 +3,7 @@ from typing import Any, Coroutine, List
 
 from pydantic import Field
 
+from ..libs.util.strings import remove_quotes_and_escapes
 from .main import EditHighlightedCodeStep
 from .core.core import MessageStep
 from ..core.main import FunctionCall, Models
@@ -43,11 +44,8 @@ class SimpleChatStep(Step):
         finally:
             await generator.aclose()
 
-        self.name = (await sdk.models.gpt35.complete(
-            f"Write a short title for the following chat message: {self.description}")).strip()
-
-        if self.name.startswith('"') and self.name.endswith('"'):
-            self.name = self.name[1:-1]
+        self.name = remove_quotes_and_escapes(await sdk.models.gpt35.complete(
+            f"Write a short title for the following chat message: {self.description}"))
 
         self.chat_context.append(ChatMessage(
             role="assistant",
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index d5a7cd9a..5b9b9fd5 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -12,7 +12,7 @@ from ...models.filesystem import FileSystem, RangeInFile, RangeInFileWithContent
 from ...core.observation import Observation, TextObservation, TracebackObservation, UserInputObservation
 from ...core.main import ChatMessage, ContinueCustomException, Step, SequentialStep
 from ...libs.util.count_tokens import MAX_TOKENS_FOR_MODEL, DEFAULT_MAX_TOKENS
-from ...libs.util.dedent import dedent_and_get_common_whitespace
+from ...libs.util.strings import dedent_and_get_common_whitespace, remove_quotes_and_escapes
 import difflib
 
 
@@ -157,17 +157,6 @@ class DefaultModelEditCodeStep(Step):
     _new_contents: str = ""
     _prompt_and_completion: str = ""
 
-    def _cleanup_output(self, output: str) -> str:
-        output = output.replace('\\"', '"')
-        output = output.replace("\\'", "'")
-        output = output.replace("\\n", "\n")
-        output = output.replace("\\t", "\t")
-        output = output.replace("\\\\", "\\")
-        if output.startswith('"') and output.endswith('"'):
-            output = output[1:-1]
-
-        return output
-
     async def describe(self, models: Models) -> Coroutine[str, None, None]:
         if self._previous_contents.strip() == self._new_contents.strip():
             description = "No edits were made"
@@ -183,9 +172,9 @@ class DefaultModelEditCodeStep(Step):
 
                 Please give brief a description of the changes made above using markdown bullet points. Be concise:"""))
         name = await models.gpt3516k.complete(f"Write a very short title to describe this requested change (no quotes): '{self.user_input}'. This is the title:")
-        self.name = self._cleanup_output(name)
+        self.name = remove_quotes_and_escapes(name)
 
-        return f"{self._cleanup_output(description)}"
+        return f"{remove_quotes_and_escapes(description)}"
 
     async def get_prompt_parts(self, rif: RangeInFileWithContents, sdk: ContinueSDK, full_file_contents: str):
         # We don't know here all of the functions being passed in.
diff --git a/extension/package-lock.json b/extension/package-lock.json
index 33f81dec..0e0125b0 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "continue",
-  "version": "0.0.176",
+  "version": "0.0.177",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "continue",
-      "version": "0.0.176",
+      "version": "0.0.177",
       "license": "Apache-2.0",
       "dependencies": {
         "@electron/rebuild": "^3.2.10",
diff --git a/extension/package.json b/extension/package.json
index e515ed36..8462bf68 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.176",
+  "version": "0.0.177",
   "publisher": "Continue",
   "engines": {
     "vscode": "^1.67.0"
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index dbebd534..0ea8a3e1 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -6,6 +6,7 @@ import {
   lightGray,
   secondaryDark,
   vscBackground,
+  vscForeground,
 } from ".";
 import CodeBlock from "./CodeBlock";
 import PillButton from "./PillButton";
@@ -48,7 +49,7 @@ const MainTextInput = styled.textarea`
   height: auto;
   width: 100%;
   background-color: ${secondaryDark};
-  color: white;
+  color: ${vscForeground};
   z-index: 1;
   border: 1px solid transparent;
 
@@ -71,7 +72,7 @@ const Ul = styled.ul<{
   position: absolute;
   background: ${vscBackground};
   background-color: ${secondaryDark};
-  color: white;
+  color: ${vscForeground};
   max-height: ${UlMaxHeight}px;
   width: calc(100% - 16px);
   overflow-y: scroll;
@@ -95,6 +96,7 @@ const Li = styled.li<{
   selected: boolean;
   isLastItem: boolean;
 }>`
+  background-color: ${secondaryDark};
   ${({ highlighted }) => highlighted && "background: #ff000066;"}
   ${({ selected }) => selected && "font-weight: bold;"}
     padding: 0.5rem 0.75rem;
@@ -218,7 +220,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 ? "Editing such a large range may be slow"
                 : undefined
             }
-            onlyShowDelete={highlightedCodeSections.length <= 1}
+            onlyShowDelete={
+              highlightedCodeSections.length <= 1 || section.editing
+            }
             editing={section.editing}
             pinned={section.pinned}
             index={idx}
diff --git a/extension/react-app/src/components/InputAndButton.tsx b/extension/react-app/src/components/InputAndButton.tsx
index 0a8592f2..8019d014 100644
--- a/extension/react-app/src/components/InputAndButton.tsx
+++ b/extension/react-app/src/components/InputAndButton.tsx
@@ -1,6 +1,6 @@
 import React, { useRef } from "react";
 import styled from "styled-components";
-import { vscBackground } from ".";
+import { vscBackground, vscForeground } from ".";
 
 interface InputAndButtonProps {
   onUserInput: (input: string) => void;
@@ -16,7 +16,7 @@ const Input = styled.input`
   padding: 0.5rem;
   border: 1px solid white;
   background-color: ${vscBackground};
-  color: white;
+  color: ${vscForeground};
   border-radius: 4px;
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
@@ -27,7 +27,7 @@ const Button = styled.button`
   padding: 0.5rem;
   border: 1px solid white;
   background-color: ${vscBackground};
-  color: white;
+  color: ${vscForeground};
   border-radius: 4px;
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
@@ -35,8 +35,8 @@ const Button = styled.button`
   cursor: pointer;
 
   &:hover {
-    background-color: white;
-    color: black;
+    background-color: ${vscForeground};
+    color: ${vscBackground};
   }
 `;
 
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 5a16516e..eba5cf8f 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,6 +1,11 @@
 import { useContext, useState } from "react";
 import styled from "styled-components";
-import { StyledTooltip, defaultBorderRadius, secondaryDark } from ".";
+import {
+  StyledTooltip,
+  defaultBorderRadius,
+  secondaryDark,
+  vscForeground,
+} from ".";
 import {
   Trash,
   PaintBrush,
@@ -10,7 +15,7 @@ import { GUIClientContext } from "../App";
 
 const Button = styled.button`
   border: none;
-  color: white;
+  color: ${vscForeground};
   background-color: ${secondaryDark};
   border-radius: ${defaultBorderRadius};
   padding: 8px;
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 93bdbc89..26bc8e33 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -6,6 +6,7 @@ import {
   secondaryDark,
   vscBackground,
   vscBackgroundTransparent,
+  vscForeground,
 } from ".";
 import {
   ChevronDown,
@@ -120,20 +121,22 @@ const StyledMarkdownPreview = styled(MarkdownPreview)`
   }
 
   code {
-    color: #f69292;
+    color: #f78383;
     word-wrap: break-word;
+    border-radius: ${defaultBorderRadius};
+    background-color: ${secondaryDark};
   }
 
   pre > code {
     background-color: ${secondaryDark};
-    color: white;
+    color: ${vscForeground};
   }
 
   background-color: ${vscBackground};
   font-family: "Lexend", sans-serif;
   font-size: 13px;
   padding: 8px;
-  color: white;
+  color: ${vscForeground};
 `;
 
 // #endregion
@@ -267,6 +270,9 @@ function StepContainer(props: StepContainerProps) {
           ) : (
             <StyledMarkdownPreview
               source={props.historyNode.step.description || ""}
+              wrapperElement={{
+                "data-color-mode": "dark",
+              }}
             />
           )}
         </ContentDiv>
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 646d6846..cba3852d 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -1,7 +1,7 @@
 // Write a component that displays a dialog box with a text field and a button.
 import React, { useEffect, useState } from "react";
 import styled from "styled-components";
-import { Button, buttonColor, secondaryDark, vscBackground } from ".";
+import { Button, secondaryDark, vscBackground, vscForeground } from ".";
 import { isMetaEquivalentKeyPressed } from "../util";
 
 const ScreenCover = styled.div`
@@ -21,13 +21,13 @@ const DialogContainer = styled.div`
 `;
 
 const Dialog = styled.div`
-  background-color: white;
+  color: ${vscForeground};
+  background-color: ${vscBackground};
   border-radius: 8px;
   padding: 8px;
   display: flex;
   flex-direction: column;
-  /* box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.5); */
-  border: 2px solid ${buttonColor};
+  box-shadow: 0 0 10px 0 ${vscForeground};
   width: fit-content;
   margin: auto;
 `;
@@ -38,14 +38,16 @@ const TextArea = styled.textarea`
   padding: 8px;
   outline: 1px solid black;
   resize: none;
+  background-color: ${secondaryDark};
+  color: ${vscForeground};
 
   &:focus {
-    outline: 1px solid ${buttonColor};
+    outline: 1px solid ${vscForeground};
   }
 `;
 
 const P = styled.p`
-  color: black;
+  color: ${vscForeground};
   margin: 8px auto;
 `;
 
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 9ae0f097..cb5e7915 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -3,12 +3,16 @@ import styled, { keyframes } from "styled-components";
 
 export const defaultBorderRadius = "5px";
 export const lightGray = "rgb(100 100 100)";
-export const secondaryDark = "rgb(45 45 45)";
-export const vscBackground = "rgb(30 30 30)";
+// export const secondaryDark = "rgb(45 45 45)";
+// export const vscBackground = "rgb(30 30 30)";
 export const vscBackgroundTransparent = "#1e1e1ede";
 export const buttonColor = "rgb(113 28 59)";
 export const buttonColorHover = "rgb(113 28 59 0.67)";
 
+export const secondaryDark = "var(--vscode-textBlockQuote-background)";
+export const vscBackground = "var(--vscode-editor-background)";
+export const vscForeground = "var(--vscode-editor-foreground)";
+
 export const Button = styled.button`
   padding: 10px 12px;
   margin: 8px 0;
@@ -46,8 +50,8 @@ export const TextArea = styled.textarea`
   resize: vertical;
 
   padding: 4px;
-  caret-color: white;
-  color: white;
+  caret-color: ${vscForeground};
+  color: #{vscForeground};
 
   &:focus {
     outline: 1px solid ${buttonColor};
@@ -120,7 +124,7 @@ export const MainTextInput = styled.textarea`
   border: 1px solid #ccc;
   margin: 8px 8px;
   background-color: ${vscBackground};
-  color: white;
+  color: ${vscForeground};
   outline: 1px solid orange;
   resize: none;
 `;
@@ -137,8 +141,9 @@ export const appear = keyframes`
 `;
 
 export const HeaderButton = styled.button<{ inverted: boolean | undefined }>`
-  background-color: ${({ inverted }) => (inverted ? "white" : "transparent")};
-  color: ${({ inverted }) => (inverted ? "black" : "white")};
+  background-color: ${({ inverted }) =>
+    inverted ? vscForeground : "transparent"};
+  color: ${({ inverted }) => (inverted ? vscBackground : vscForeground)};
 
   border: none;
   border-radius: ${defaultBorderRadius};
@@ -146,7 +151,9 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>`
 
   &:hover {
     background-color: ${({ inverted }) =>
-      typeof inverted === "undefined" || inverted ? lightGray : "transparent"};
+      typeof inverted === "undefined" || inverted
+        ? secondaryDark
+        : "transparent"};
   }
   display: flex;
   align-items: center;
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index 6e33c89c..bac7fe97 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -14,13 +14,13 @@ html,
 body,
 #root {
   height: 100%;
-  background-color: var(--vsc-background);
+  background-color: var(--vscode-editor-background);
   font-family: "Lexend", sans-serif;
 }
 
 body {
   padding: 0;
-  color: white;
+  color: var(--vscode-editor-foreground);
   padding: 0px;
   margin: 0px;
   height: 100%;
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 64207487..c35cf21b 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,5 +1,9 @@
 import styled from "styled-components";
-import { defaultBorderRadius } from "../components";
+import {
+  defaultBorderRadius,
+  vscBackground,
+  vscForeground,
+} from "../components";
 import Loader from "../components/Loader";
 import ContinueButton from "../components/ContinueButton";
 import { FullState, HighlightedRangeContext } from "../../../schema/FullState";
@@ -371,12 +375,13 @@ function GUI(props: GUIProps) {
         style={{
           position: "fixed",
           bottom: "50px",
-          backgroundColor: "white",
-          color: "black",
+          backgroundColor: vscBackground,
+          color: vscForeground,
           borderRadius: defaultBorderRadius,
           padding: "16px",
           margin: "16px",
           zIndex: 100,
+          boxShadow: `0px 0px 10px 0px ${vscForeground}`,
         }}
         hidden={!showDataSharingInfo}
       >
-- 
cgit v1.2.3-70-g09d2


From 0ad32bd6dfaf96af0a2db82fb2b06c200e131e62 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 22 Jul 2023 12:14:07 -0700
Subject: how to use private model docs and button

---
 docs/docs/customization.md                        | 124 ++++++++++++++++++++++
 docs/sidebars.js                                  |  10 +-
 extension/package-lock.json                       |   4 +-
 extension/package.json                            |   2 +-
 extension/react-app/src/components/TextDialog.tsx |  60 ++++++-----
 extension/react-app/src/pages/gui.tsx             |  40 +++----
 6 files changed, 189 insertions(+), 51 deletions(-)
 create mode 100644 docs/docs/customization.md

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/docs/docs/customization.md b/docs/docs/customization.md
new file mode 100644
index 00000000..cd306cfe
--- /dev/null
+++ b/docs/docs/customization.md
@@ -0,0 +1,124 @@
+# Customization
+
+Continue can be deeply customized by editing the `ContinueConfig` object in `~/.continue/config.py` on your machine. This file is created the first time you run Continue.
+
+## Change the default LLM
+
+Change the `default_model` field to any of "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "claude-2", or "ggml".
+
+### claude-2 and gpt-X
+
+If you have access, simply set `default_model` to the model you would like to use, then you will be prompted for a personal API key after reloading VS Code. If using an OpenAI model, you can press enter to try with our API key for free.
+
+### Local models with ggml
+
+See our [5 minute quickstart](https://github.com/continuedev/ggml-server-example) to run any model locally with ggml. While these models don't yet perform as well, they are free, entirely private, and run offline.
+
+### Azure OpenAI Service
+
+If you'd like to use OpenAI models but are concerned about privacy, you can use the Azure OpenAI service, which is GDPR and HIPAA compliant. After applying for access [here](https://azure.microsoft.com/en-us/products/ai-services/openai-service), you will typically hear back within only a few days. Once you have access, set `default_model` to "gpt-4", and then set the `azure_openai_info` property in the `ContinueConfig` like so:
+
+```python
+config = ContinueConfig(
+    ...
+    azure_openai_info=AzureInfo(
+        endpoint="https://my-azure-openai-instance.openai.azure.com/",
+        engine="my-azure-openai-deployment",
+        api_version="2023-03-15-preview"
+    )
+)
+```
+
+The easiest way to find this information is from the chat playground in the Azure OpenAI portal. Under the "Chat Session" section, click "View Code" to see each of these parameters. Finally, find one of your Azure OpenAI keys and enter it in the VS Code settings under `continue.OPENAI_API_KEY`.
+
+## Customize System Message
+
+You can write your own system message, a set of instructions that will always be top-of-mind for the LLM, by setting the `system_message` property to any string. For example, you might request "Please make all responses as concise as possible and never repeat something you have already explained."
+
+System messages can also reference files. For example, if there is a markdown file (e.g. at `/Users/nate/Documents/docs/reference.md`) you'd like the LLM to know about, you can reference it with [Mustache](http://mustache.github.io/mustache.5.html) templating like this: "Please reference this documentation: {{ Users/nate/Documents/docs/reference.md }}". As of now, you must use an absolute path.
+
+## Custom Commands
+
+You can add custom slash commands by adding a `CustomCommand` object to the `custom_commands` property. Each `CustomCommand` has
+
+- `name`: the name of the command, which will be invoked with `/name`
+- `description`: a short description of the command, which will appear in the dropdown
+- `prompt`: a set of instructions to the LLM, which will be shown in the prompt
+
+Custom commands are great when you are frequently reusing a prompt. For example, if you've crafted a great prompt and frequently ask the LLM to check for mistakes in your code, you could add a command like this:
+
+```python
+config = ContinueConfig(
+    ...
+    custom_commands=[
+        CustomCommand(
+            name="check",
+            description="Check for mistakes in my code",
+            prompt=dedent("""\
+            Please read the highlighted code and check for any mistakes. You should look for the following, and be extremely vigilant:
+            - Syntax errors
+            - Logic errors
+            - Security vulnerabilities
+            - Performance issues
+            - Anything else that looks wrong
+
+            Once you find an error, please explain it as clearly as possible, but without using extra words. For example, instead of saying "I think there is a syntax error on line 5", you should say "Syntax error on line 5". Give your answer as one bullet point per mistake found.""")
+        )
+    ]
+)
+```
+
+## Temperature
+
+Set `temperature` to any value between 0 and 1. Higher values will make the LLM more creative, while lower values will make it more predictable. The default is 0.5.
+
+## Custom Context Providers
+
+When you type '@' in the Continue text box, it will display a dropdown of items that can be selected to include in your message as context. For example, you might want to reference a GitHub Issue, file, or Slack thread. All of these options are provided by a `ContextProvider` class, and we make it easy to write your own. As an example, here is the `GitHubIssuesContextProvider`, which lets you search all open GitHub Issues in a repo:
+
+```python
+class GitHubIssuesContextProvider(ContextProvider):
+    """
+    The GitHubIssuesContextProvider is a ContextProvider that allows you to search GitHub Issues in a repo.
+    """
+
+    title = "issues"
+    repo_name: str
+    auth_token: str
+
+    async def provide_context_items(self) -> List[ContextItem]:
+        auth = Auth.Token(self.auth_token)
+        gh = Github(auth=auth)
+
+        repo = gh.get_repo(self.repo_name)
+        issues = repo.get_issues().get_page(0)
+
+        items = [ContextItem(
+            content=issue.body,
+            description=ContextItemDescription(
+                name=f"Issue #{issue.number}",
+                description=issue.title,
+                id=ContextItemId(
+                    provider_title=self.title,
+                    item_id=issue.id
+                )
+            )
+        ) for issue in issues]
+        self.context_items = {
+            item.description.id.to_string(): item for item in items}
+        return items
+```
+
+It can then be set in the `ContinueConfig` like so:
+
+```python
+config = ContinueConfig(
+    ...
+    context_providers=[
+        GitHubIssuesContextProvider(
+            repo_name="my-github-username-or-org/my-github-repo",
+            auth_token="my-github-auth-token"
+        )
+    ]
+)
+```
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 9baf1b94..83b34ee8 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -13,7 +13,15 @@
 
 /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
 const sidebars = {
-  docsSidebar: ["intro", "getting-started", "how-to-use-continue", "how-continue-works", "telemetry", "collecting-data"],
+  docsSidebar: [
+    "intro",
+    "getting-started",
+    "how-to-use-continue",
+    "how-continue-works",
+    "telemetry",
+    "collecting-data",
+    "customization",
+  ],
 };
 
 module.exports = sidebars;
diff --git a/extension/package-lock.json b/extension/package-lock.json
index 5c8e27d0..933da12b 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "continue",
-  "version": "0.0.188",
+  "version": "0.0.189",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "continue",
-      "version": "0.0.188",
+      "version": "0.0.189",
       "license": "Apache-2.0",
       "dependencies": {
         "@electron/rebuild": "^3.2.10",
diff --git a/extension/package.json b/extension/package.json
index 3d44c156..08737ff4 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.188",
+  "version": "0.0.189",
   "publisher": "Continue",
   "engines": {
     "vscode": "^1.67.0"
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index cba3852d..9597b578 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import { Button, secondaryDark, vscBackground, vscForeground } from ".";
 import { isMetaEquivalentKeyPressed } from "../util";
+import { ReactMarkdown } from "react-markdown/lib/react-markdown";
 
 const ScreenCover = styled.div`
   position: absolute;
@@ -56,6 +57,7 @@ const TextDialog = (props: {
   onEnter: (text: string) => void;
   onClose: () => void;
   message?: string;
+  entryOn?: boolean;
 }) => {
   const [text, setText] = useState("");
   const textAreaRef = React.createRef<HTMLTextAreaElement>();
@@ -79,33 +81,37 @@ const TextDialog = (props: {
         }}
       >
         <Dialog>
-          <P>{props.message || ""}</P>
-          <TextArea
-            rows={10}
-            ref={textAreaRef}
-            onKeyDown={(e) => {
-              if (
-                e.key === "Enter" &&
-                isMetaEquivalentKeyPressed(e) &&
-                textAreaRef.current
-              ) {
-                props.onEnter(textAreaRef.current.value);
-                setText("");
-              } else if (e.key === "Escape") {
-                props.onClose();
-              }
-            }}
-          />
-          <Button
-            onClick={() => {
-              if (textAreaRef.current) {
-                props.onEnter(textAreaRef.current.value);
-                setText("");
-              }
-            }}
-          >
-            Enter
-          </Button>
+          <ReactMarkdown>{props.message || ""}</ReactMarkdown>
+          {props.entryOn && (
+            <>
+              <TextArea
+                rows={10}
+                ref={textAreaRef}
+                onKeyDown={(e) => {
+                  if (
+                    e.key === "Enter" &&
+                    isMetaEquivalentKeyPressed(e) &&
+                    textAreaRef.current
+                  ) {
+                    props.onEnter(textAreaRef.current.value);
+                    setText("");
+                  } else if (e.key === "Escape") {
+                    props.onClose();
+                  }
+                }}
+              />
+              <Button
+                onClick={() => {
+                  if (textAreaRef.current) {
+                    props.onEnter(textAreaRef.current.value);
+                    setText("");
+                  }
+                }}
+              >
+                Enter
+              </Button>
+            </>
+          )}
         </Dialog>
       </DialogContainer>
     </ScreenCover>
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index fccc9b4b..49f41dcf 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -75,7 +75,6 @@ function GUI(props: GUIProps) {
     }
   }, [dataSwitchOn]);
 
-  const [usingFastModel, setUsingFastModel] = useState(false);
   const [waitingForSteps, setWaitingForSteps] = useState(false);
   const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
   const [highlightedRanges, setHighlightedRanges] = useState<
@@ -117,6 +116,7 @@ function GUI(props: GUIProps) {
 
   const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
   const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
+  const [feedbackEntryOn, setFeedbackEntryOn] = useState(true);
 
   const topGuiDivRef = useRef<HTMLDivElement>(null);
 
@@ -140,12 +140,9 @@ function GUI(props: GUIProps) {
   }, [topGuiDivRef.current, scrollTimeout]);
 
   useEffect(() => {
+    // Cmd + Backspace to delete current step
     const listener = (e: any) => {
-      // Cmd + i to toggle fast model
-      if (e.key === "i" && isMetaEquivalentKeyPressed(e) && e.shiftKey) {
-        setUsingFastModel((prev) => !prev);
-        // Cmd + backspace to stop currently running step
-      } else if (
+      if (
         e.key === "Backspace" &&
         isMetaEquivalentKeyPressed(e) &&
         typeof history?.current_index !== "undefined" &&
@@ -164,7 +161,6 @@ function GUI(props: GUIProps) {
   useEffect(() => {
     client?.onStateUpdate((state: FullState) => {
       // Scroll only if user is at very bottom of the window.
-      setUsingFastModel(state.default_model === "gpt-3.5-turbo");
       const shouldScrollToBottom =
         topGuiDivRef.current &&
         topGuiDivRef.current?.offsetHeight - window.scrollY < 100;
@@ -281,6 +277,7 @@ function GUI(props: GUIProps) {
           setShowFeedbackDialog(false);
         }}
         message={feedbackDialogMessage}
+        entryOn={feedbackEntryOn}
       />
 
       <TopGUIDiv
@@ -431,24 +428,26 @@ function GUI(props: GUIProps) {
         </div>
         <HeaderButtonWithText
           onClick={() => {
-            // client?.changeDefaultModel(
-            //   usingFastModel ? "gpt-4" : "gpt-3.5-turbo"
-            // );
-            if (!usingFastModel) {
-              // Show the dialog
-              setFeedbackDialogMessage(
-                "We don't yet support local models, but we're working on it! If privacy is a concern of yours, please write a short note to let us know."
-              );
-              setShowFeedbackDialog(true);
-            }
-            setUsingFastModel((prev) => !prev);
+            // 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={usingFastModel ? "local" : "gpt-4"}
+          text={"Use Private Model"}
         >
           <div
             style={{ fontSize: "18px", marginLeft: "2px", marginRight: "2px" }}
           >
-            {usingFastModel ? "🔒" : "🧠"}
+            🔒
           </div>
         </HeaderButtonWithText>
         <HeaderButtonWithText
@@ -473,6 +472,7 @@ function GUI(props: GUIProps) {
             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"
-- 
cgit v1.2.3-70-g09d2


From 4d7e72970f770eb49627589fb142c93dfb6fd73b Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 22 Jul 2023 22:37:13 -0700
Subject: @ feature (very large commit)

---
 continuedev/poetry.lock                            | 340 ++++++++++++++++++++-
 continuedev/pyproject.toml                         |   2 +
 continuedev/src/continuedev/core/autopilot.py      | 138 ++-------
 continuedev/src/continuedev/core/config.py         |   3 +
 continuedev/src/continuedev/core/context.py        | 205 +++++++++++++
 .../src/continuedev/core/context_manager.py        | 119 --------
 continuedev/src/continuedev/core/main.py           |  59 +++-
 continuedev/src/continuedev/core/sdk.py            |  31 +-
 .../highlighted_code_context_provider.py           | 191 ++++++++++++
 continuedev/src/continuedev/libs/util/paths.py     |  20 +-
 .../src/continuedev/models/generate_json_schema.py |   3 +
 continuedev/src/continuedev/server/gui.py          |  37 +--
 continuedev/src/continuedev/server/gui_protocol.py |  10 +-
 continuedev/src/continuedev/server/main.py         |  28 +-
 .../src/continuedev/server/meilisearch_server.py   |  56 ++++
 .../src/continuedev/server/session_manager.py      |   6 +-
 extension/react-app/package-lock.json              |  98 ++++++
 extension/react-app/package.json                   |   1 +
 extension/react-app/src/components/ComboBox.tsx    | 174 ++++++-----
 extension/react-app/src/components/PillButton.tsx  |  87 ++++--
 .../react-app/src/components/StepContainer.tsx     |  29 +-
 .../src/components/StyledMarkdownPreview.tsx       |  37 +++
 extension/react-app/src/components/TextDialog.tsx  |   2 +-
 .../src/hooks/AbstractContinueGUIClientProtocol.ts |   8 +-
 .../src/hooks/ContinueGUIClientProtocol.ts         |  15 +-
 extension/react-app/src/pages/gui.tsx              |  81 ++---
 .../src/redux/selectors/uiStateSelectors.ts        |   5 +
 .../react-app/src/redux/slices/uiStateSlice.ts     |  24 ++
 extension/react-app/src/redux/store.ts             |   6 +
 extension/schema/ContextItem.d.ts                  |  45 +++
 extension/schema/FullState.d.ts                    |  65 ++--
 schema/json/ContextItem.json                       |  76 +++++
 schema/json/FullState.json                         | 132 ++++----
 33 files changed, 1554 insertions(+), 579 deletions(-)
 create mode 100644 continuedev/src/continuedev/core/context.py
 delete mode 100644 continuedev/src/continuedev/core/context_manager.py
 create mode 100644 continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py
 create mode 100644 continuedev/src/continuedev/server/meilisearch_server.py
 create mode 100644 extension/react-app/src/components/StyledMarkdownPreview.tsx
 create mode 100644 extension/react-app/src/redux/selectors/uiStateSelectors.ts
 create mode 100644 extension/react-app/src/redux/slices/uiStateSlice.ts
 create mode 100644 extension/schema/ContextItem.d.ts
 create mode 100644 schema/json/ContextItem.json

(limited to 'extension/react-app/src/components/TextDialog.tsx')

diff --git a/continuedev/poetry.lock b/continuedev/poetry.lock
index 1cd4a591..b67128fd 100644
--- a/continuedev/poetry.lock
+++ b/continuedev/poetry.lock
@@ -220,6 +220,24 @@ files = [
     {file = "boltons-23.0.0.tar.gz", hash = "sha256:8c50a71829525835ca3c849c7ed2511610c972b4dddfcd41a4a5447222beb4b0"},
 ]
 
+[[package]]
+name = "camel-converter"
+version = "3.0.2"
+description = "Converts a string from snake case to camel case or camel case to snake case"
+category = "main"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+    {file = "camel_converter-3.0.2-py3-none-any.whl", hash = "sha256:88e5d91be5b2dff9c0748ba515774c3421088922d9e77c39f8742eb41cb7db88"},
+    {file = "camel_converter-3.0.2.tar.gz", hash = "sha256:3b3d076e824ae979b271b4d497c90514c2b218811f76b0c368fb69da2556fe07"},
+]
+
+[package.dependencies]
+pydantic = {version = ">=1.8.2", optional = true, markers = "extra == \"pydantic\""}
+
+[package.extras]
+pydantic = ["pydantic (>=1.8.2)"]
+
 [[package]]
 name = "certifi"
 version = "2022.12.7"
@@ -232,6 +250,83 @@ files = [
     {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
 ]
 
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+    {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+    {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+    {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+    {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+    {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+    {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+    {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+    {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+    {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+    {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+    {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+    {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+    {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+    {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+    {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+    {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+    {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+    {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+    {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
 [[package]]
 name = "charset-normalizer"
 version = "3.1.0"
@@ -356,6 +451,52 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
+[[package]]
+name = "cryptography"
+version = "41.0.2"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"},
+    {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"},
+    {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"},
+    {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"},
+    {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"},
+    {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"},
+    {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"},
+    {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"},
+    {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"},
+    {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"},
+    {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"},
+    {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"},
+    {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"},
+    {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"},
+    {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"},
+    {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"},
+    {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"},
+    {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"},
+    {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"},
+    {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"},
+    {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"},
+    {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"},
+    {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"},
+]
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
+nox = ["nox"]
+pep8test = ["black", "check-sdist", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
+
 [[package]]
 name = "dataclasses-json"
 version = "0.5.7"
@@ -376,6 +517,24 @@ typing-inspect = ">=0.4.0"
 [package.extras]
 dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest (>=6.2.3)", "simplejson", "types-dataclasses"]
 
+[[package]]
+name = "deprecated"
+version = "1.2.14"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
+    {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
+]
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
+
 [[package]]
 name = "diff-match-patch"
 version = "20230430"
@@ -816,6 +975,22 @@ files = [
 [package.dependencies]
 marshmallow = ">=2.0.0"
 
+[[package]]
+name = "meilisearch"
+version = "0.28.1"
+description = "The python client for Meilisearch API."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "meilisearch-0.28.1-py3-none-any.whl", hash = "sha256:b4cfaf140fc323c429a4e174b801bcc6dddef88bf901560ebf2fc5c0014099b5"},
+    {file = "meilisearch-0.28.1.tar.gz", hash = "sha256:4e8627abe1394f04125c99e60f9f4356ba200effe5735ce9b8a44b09e3c6403c"},
+]
+
+[package.dependencies]
+camel-converter = {version = "*", extras = ["pydantic"]}
+requests = "*"
+
 [[package]]
 name = "monotonic"
 version = "1.6"
@@ -1198,6 +1373,18 @@ files = [
 [package.extras]
 test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+
 [[package]]
 name = "pydantic"
 version = "1.10.7"
@@ -1251,6 +1438,72 @@ typing-extensions = ">=4.2.0"
 dotenv = ["python-dotenv (>=0.10.4)"]
 email = ["email-validator (>=1.0.3)"]
 
+[[package]]
+name = "pygithub"
+version = "1.59.0"
+description = "Use the full Github API v3"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "PyGithub-1.59.0-py3-none-any.whl", hash = "sha256:126bdbae72087d8d038b113aab6b059b4553cb59348e3024bb1a1cae406ace9e"},
+    {file = "PyGithub-1.59.0.tar.gz", hash = "sha256:6e05ff49bac3caa7d1d6177a10c6e55a3e20c85b92424cc198571fd0cf786690"},
+]
+
+[package.dependencies]
+deprecated = "*"
+pyjwt = {version = ">=2.4.0", extras = ["crypto"]}
+pynacl = ">=1.4.0"
+requests = ">=2.14.0"
+
+[[package]]
+name = "pyjwt"
+version = "2.8.0"
+description = "JSON Web Token implementation in Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
+    {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
+[[package]]
+name = "pynacl"
+version = "1.5.0"
+description = "Python binding to the Networking and Cryptography (NaCl) library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
+    {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
+    {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
+]
+
+[package.dependencies]
+cffi = ">=1.4.1"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
+tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
+
 [[package]]
 name = "pyrsistent"
 version = "0.19.3"
@@ -1935,6 +2188,91 @@ files = [
     {file = "websockets-11.0.2.tar.gz", hash = "sha256:b1a69701eb98ed83dd099de4a686dc892c413d974fa31602bc00aca7cb988ac9"},
 ]
 
+[[package]]
+name = "wrapt"
+version = "1.15.0"
+description = "Module for decorators, wrappers and monkey patching."
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+    {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
+    {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
+    {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
+    {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
+    {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
+    {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
+    {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
+    {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
+    {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
+    {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
+    {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
+    {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
+    {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
+    {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
+    {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
+    {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
+    {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
+    {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
+    {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
+    {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
+    {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
+    {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
+    {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
+    {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
+    {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
+    {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
+    {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
+    {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
+    {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
+    {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
+    {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
+    {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
+    {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
+    {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
+    {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
+    {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
+    {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
+    {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
+    {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
+    {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
+    {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
+    {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+]
+
 [[package]]
 name = "yarl"
 version = "1.9.2"
@@ -2042,4 +2380,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8.1"
-content-hash = "3fcd19c11b9c338a181e591b56e21d59c7834abff431fb9f40cc1ea874b64557"
+content-hash = "83880c8de27a4094fa8e695bd8db5d4053fc2d61ffc028f09cfa097f315c35f2"
diff --git a/continuedev/pyproject.toml b/continuedev/pyproject.toml
index 0abc9504..64d5e90c 100644
--- a/continuedev/pyproject.toml
+++ b/continuedev/pyproject.toml
@@ -27,6 +27,8 @@ directory-tree = "^0.0.3.1"
 anthropic = "^0.3.4"
 chevron = "^0.14.0"
 psutil = "^5.9.5"
+meilisearch = "^0.28.1"
+pygithub = "^1.59.0"
 
 [tool.poetry.scripts]
 typegen = "src.continuedev.models.generate_json_schema:main" 
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index abda50b0..c0f95414 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -9,10 +9,12 @@ from pydantic import root_validator
 from ..models.filesystem import RangeInFileWithContents
 from ..models.filesystem_edit import FileEditWithFullContents
 from .observation import Observation, InternalErrorObservation
+from .context import ContextItem, ContextItemDescription, ContextItemId, ContextManager
+from ..libs.context_providers.highlighted_code_context_provider import HighlightedCodeContextProvider
 from ..server.ide_protocol import AbstractIdeProtocolServer
 from ..libs.util.queue import AsyncSubscriptionQueue
 from ..models.main import ContinueBaseModel
-from .main import Context, ContinueCustomException, HighlightedRangeContext, Policy, History, FullState, Step, HistoryNode
+from .main import Context, ContinueCustomException, Policy, History, FullState, Step, HistoryNode
 from ..steps.core.core import ReversibleStep, ManualEditStep, UserInputStep
 from ..libs.util.telemetry import capture_event
 from .sdk import ContinueSDK
@@ -47,10 +49,11 @@ class Autopilot(ContinueBaseModel):
     history: History = History.from_empty()
     context: Context = Context()
     full_state: Union[FullState, None] = None
-    _on_update_callbacks: List[Callable[[FullState], None]] = []
-
+    context_manager: Union[ContextManager, None] = None
     continue_sdk: ContinueSDK = None
 
+    _on_update_callbacks: List[Callable[[FullState], None]] = []
+
     _active: bool = False
     _should_halt: bool = False
     _main_user_input_queue: List[str] = []
@@ -62,6 +65,14 @@ class Autopilot(ContinueBaseModel):
     async def create(cls, policy: Policy, ide: AbstractIdeProtocolServer, full_state: FullState) -> "Autopilot":
         autopilot = cls(ide=ide, policy=policy)
         autopilot.continue_sdk = await ContinueSDK.create(autopilot)
+
+        # Load documents into the search index
+        autopilot.context_manager = ContextManager(
+            autopilot.continue_sdk.config.context_providers + [
+                HighlightedCodeContextProvider(ide=ide)
+            ])
+        await autopilot.context_manager.load_index()
+
         return autopilot
 
     class Config:
@@ -75,15 +86,16 @@ class Autopilot(ContinueBaseModel):
             values['history'] = full_state.history
         return values
 
-    def get_full_state(self) -> FullState:
+    async def get_full_state(self) -> FullState:
         full_state = FullState(
             history=self.history,
             active=self._active,
             user_input_queue=self._main_user_input_queue,
             default_model=self.continue_sdk.config.default_model,
-            highlighted_ranges=self._highlighted_ranges,
             slash_commands=self.get_available_slash_commands(),
-            adding_highlighted_code=self._adding_highlighted_code,
+            adding_highlighted_code=self.context_manager.context_providers[
+                "code"].adding_highlighted_code,
+            selected_context_items=await self.context_manager.get_selected_items()
         )
         self.full_state = full_state
         return full_state
@@ -104,8 +116,8 @@ class Autopilot(ContinueBaseModel):
         self._main_user_input_queue = []
         self._active = False
 
-        # Also remove all context
-        self._highlighted_ranges = []
+        # Clear context
+        await self.context_manager.clear_context()
 
         await self.update_subscribers()
 
@@ -114,7 +126,7 @@ class Autopilot(ContinueBaseModel):
         self._on_update_callbacks.append(callback)
 
     async def update_subscribers(self):
-        full_state = self.get_full_state()
+        full_state = await self.get_full_state()
         for callback in self._on_update_callbacks:
             await callback(full_state)
 
@@ -159,81 +171,10 @@ class Autopilot(ContinueBaseModel):
                     step = tb_step.step({"output": output, **tb_step.params})
                     await self._run_singular_step(step)
 
-    _highlighted_ranges: List[HighlightedRangeContext] = []
-    _adding_highlighted_code: bool = False
-
-    def _make_sure_is_editing_range(self):
-        """If none of the highlighted ranges are currently being edited, the first should be selected"""
-        if len(self._highlighted_ranges) == 0:
-            return
-        if not any(map(lambda x: x.editing, self._highlighted_ranges)):
-            self._highlighted_ranges[0].editing = True
-
-    def _disambiguate_highlighted_ranges(self):
-        """If any files have the same name, also display their folder name"""
-        name_status: Dict[str, set] = {
-        }  # basename -> set of full paths with that basename
-        for rif in self._highlighted_ranges:
-            basename = os.path.basename(rif.range.filepath)
-            if basename in name_status:
-                name_status[basename].add(rif.range.filepath)
-            else:
-                name_status[basename] = {rif.range.filepath}
-
-        for rif in self._highlighted_ranges:
-            basename = os.path.basename(rif.range.filepath)
-            if len(name_status[basename]) > 1:
-                rif.display_name = os.path.join(
-                    os.path.basename(os.path.dirname(rif.range.filepath)), basename)
-            else:
-                rif.display_name = basename
-
     async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]):
-        # Filter out rifs from ~/.continue/diffs folder
-        range_in_files = [
-            rif for rif in range_in_files if not os.path.dirname(rif.filepath) == os.path.expanduser("~/.continue/diffs")]
-
-        # Make sure all filepaths are relative to workspace
-        workspace_path = self.continue_sdk.ide.workspace_directory
-
-        # If not adding highlighted code
-        if not self._adding_highlighted_code:
-            if len(self._highlighted_ranges) == 1 and len(range_in_files) <= 1 and (len(range_in_files) == 0 or range_in_files[0].range.start == range_in_files[0].range.end):
-                # If un-highlighting the range to edit, then remove the range
-                self._highlighted_ranges = []
-                await self.update_subscribers()
-            elif len(range_in_files) > 0:
-                # Otherwise, replace the current range with the new one
-                # This is the first range to be highlighted
-                self._highlighted_ranges = [HighlightedRangeContext(
-                    range=range_in_files[0], editing=True, pinned=False, display_name=os.path.basename(range_in_files[0].filepath))]
-                await self.update_subscribers()
-            return
-
-        # If current range overlaps with any others, delete them and only keep the new range
-        new_ranges = []
-        for i, rif in enumerate(self._highlighted_ranges):
-            found_overlap = False
-            for new_rif in range_in_files:
-                if rif.range.filepath == new_rif.filepath and rif.range.range.overlaps_with(new_rif.range):
-                    found_overlap = True
-                    break
-
-                # 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 rif.range.filepath == new_rif.filepath and rif.range.contents == new_rif.contents:
-                    found_overlap = True
-                    break
-
-            if not found_overlap:
-                new_ranges.append(rif)
-
-        self._highlighted_ranges = new_ranges + [HighlightedRangeContext(
-            range=rif, editing=False, pinned=False, display_name=os.path.basename(rif.filepath)
-        ) for rif in range_in_files]
-
-        self._make_sure_is_editing_range()
-        self._disambiguate_highlighted_ranges()
+        # Add to context manager
+        await self.context_manager.context_providers["code"].handle_highlighted_code(
+            range_in_files)
 
         await self.update_subscribers()
 
@@ -250,29 +191,16 @@ class Autopilot(ContinueBaseModel):
 
         await self.update_subscribers()
 
-    async def delete_context_at_indices(self, indices: List[int]):
-        kept_ranges = []
-        for i, rif in enumerate(self._highlighted_ranges):
-            if i not in indices:
-                kept_ranges.append(rif)
-        self._highlighted_ranges = kept_ranges
-
-        self._make_sure_is_editing_range()
-
+    async def delete_context_with_ids(self, ids: List[str]):
+        await self.context_manager.delete_context_with_ids(ids)
         await self.update_subscribers()
 
     async def toggle_adding_highlighted_code(self):
-        self._adding_highlighted_code = not self._adding_highlighted_code
-        await self.update_subscribers()
-
-    async def set_editing_at_indices(self, indices: List[int]):
-        for i in range(len(self._highlighted_ranges)):
-            self._highlighted_ranges[i].editing = i in indices
+        self.context_manager.context_providers["code"].adding_highlighted_code = not self.context_manager.context_providers["code"].adding_highlighted_code
         await self.update_subscribers()
 
-    async def set_pinned_at_indices(self, indices: List[int]):
-        for i in range(len(self._highlighted_ranges)):
-            self._highlighted_ranges[i].pinned = i in indices
+    async def set_editing_at_ids(self, ids: List[str]):
+        self.context_manager.context_providers["code"].set_editing_at_ids(ids)
         await self.update_subscribers()
 
     async def _run_singular_step(self, step: "Step", is_future_step: bool = False) -> Coroutine[Observation, None, None]:
@@ -437,10 +365,6 @@ class Autopilot(ContinueBaseModel):
         if len(self._main_user_input_queue) > 1:
             return
 
-        # Remove context unless pinned
-        # self._highlighted_ranges = [
-        #     hr for hr in self._highlighted_ranges if hr.pinned]
-
         # await self._request_halt()
         # Just run the step that takes user input, and
         # then up to the policy to decide how to deal with it.
@@ -456,3 +380,7 @@ class Autopilot(ContinueBaseModel):
         await self._request_halt()
         await self.reverse_to_index(index)
         await self.run_from_step(UserInputStep(user_input=user_input))
+
+    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 54f15143..bb9ca323 100644
--- a/continuedev/src/continuedev/core/config.py
+++ b/continuedev/src/continuedev/core/config.py
@@ -1,6 +1,7 @@
 import json
 import os
 from .main import Step
+from .context import ContextProvider
 from pydantic import BaseModel, validator
 from typing import List, Literal, Optional, Dict, Type, Union
 import yaml
@@ -50,6 +51,8 @@ class ContinueConfig(BaseModel):
     system_message: Optional[str] = None
     azure_openai_info: Optional[AzureInfo] = None
 
+    context_providers: List[ContextProvider] = []
+
     # Want to force these to be the slash commands for now
     @validator('slash_commands', pre=True)
     def default_slash_commands_validator(cls, v):
diff --git a/continuedev/src/continuedev/core/context.py b/continuedev/src/continuedev/core/context.py
new file mode 100644
index 00000000..67bba651
--- /dev/null
+++ b/continuedev/src/continuedev/core/context.py
@@ -0,0 +1,205 @@
+
+from abc import abstractmethod
+from typing import Dict, List
+import meilisearch
+from pydantic import BaseModel
+
+
+from .main import ChatMessage, ContextItem, ContextItemDescription, ContextItemId
+from ..server.meilisearch_server import check_meilisearch_running
+
+
+SEARCH_INDEX_NAME = "continue_context_items"
+
+
+class ContextProvider(BaseModel):
+    """
+    The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'.
+    When you type '@', the context provider will be asked to populate a list of options.
+    These options will be updated on each keystroke.
+    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).
+    """
+
+    title: str
+
+    selected_items: List[ContextItem] = []
+
+    async def get_selected_items(self) -> List[ContextItem]:
+        """
+        Returns all of the selected ContextItems.
+
+        Default implementation simply returns self.selected_items.
+
+        Other implementations may add an async processing step.
+        """
+        return self.selected_items
+
+    @abstractmethod
+    async def provide_context_items(self) -> List[ContextItem]:
+        """
+        Provide documents for search index. This is run on startup.
+
+        This is the only method that must be implemented.
+        """
+
+    async def get_chat_messages(self) -> List[ChatMessage]:
+        """
+        Returns all of the chat messages for the context provider.
+
+        Default implementation has a string template.
+        """
+        return [ChatMessage(role="user", content=f"{item.description.name}: {item.description.description}\n\n{item.content}", summary=item.description.description) for item in await self.get_selected_items()]
+
+    async def get_item(self, id: ContextItemId, query: str, search_client: meilisearch.Client) -> ContextItem:
+        """
+        Returns the ContextItem with the given id.
+
+        Default implementation uses the search index to get the item.
+        """
+        result = search_client.index(
+            SEARCH_INDEX_NAME).get_document(id.to_string())
+        return ContextItem(
+            description=ContextItemDescription(
+                name=result.name,
+                description=result.description,
+                id=id
+            ),
+            content=result.content
+        )
+
+    async def delete_context_with_ids(self, ids: List[ContextItemId]):
+        """
+        Deletes the ContextItems with the given IDs, lets ContextProviders recalculate.
+
+        Default implementation simply deletes those with the given ids.
+        """
+        id_strings = {id.to_string() for id in ids}
+        self.selected_items = list(
+            filter(lambda item: item.description.id.to_string() not in id_strings, self.selected_items))
+
+    async def clear_context(self):
+        """
+        Clears all context.
+
+        Default implementation simply clears the selected items.
+        """
+        self.selected_items = []
+
+    async def add_context_item(self, id: ContextItemId, query: str, search_client: meilisearch.Client):
+        """
+        Adds the given ContextItem to the list of ContextItems.
+
+        Default implementation simply appends the item, not allowing duplicates.
+
+        This method also allows you not to have to load all of the information until an item is selected.
+        """
+
+        # Don't add duplicate context
+        for item in self.selected_items:
+            if item.description.id.item_id == id.item_id:
+                return
+
+        new_item = await self.get_item(id, query, search_client)
+        self.selected_items.append(new_item)
+
+
+class ContextManager:
+    """
+    The context manager is responsible for storing the context to be passed to the LLM, including
+    - ContextItems (highlighted code, GitHub Issues, etc.)
+    - ChatMessages in the history
+    - System Message
+    - Functions
+
+    It is responsible for compiling all of this information into a single prompt without exceeding the token limit.
+    """
+
+    async def get_selected_items(self) -> List[ContextItem]:
+        """
+        Returns all of the selected ContextItems.
+        """
+        return sum([await provider.get_selected_items() for provider in self.context_providers.values()], [])
+
+    async def get_chat_messages(self) -> List[ChatMessage]:
+        """
+        Returns chat messages from each provider.
+        """
+        return sum([await provider.get_chat_messages() for provider in self.context_providers.values()], [])
+
+    search_client: meilisearch.Client
+
+    def __init__(self, context_providers: List[ContextProvider]):
+        self.search_client = meilisearch.Client('http://localhost:7700')
+
+        # If meilisearch isn't running, don't use any ContextProviders that might depend on it
+        if not check_meilisearch_running():
+            context_providers = list(
+                filter(lambda cp: cp.title == "code", context_providers))
+
+        self.context_providers = {
+            prov.title: prov for prov in context_providers}
+        self.provider_titles = {
+            provider.title for provider in context_providers}
+
+    async def load_index(self):
+        for _, provider in self.context_providers.items():
+            context_items = await provider.provide_context_items()
+            documents = [
+                {
+                    "id": item.description.id.to_string(),
+                    "name": item.description.name,
+                    "description": item.description.description,
+                    "content": item.content
+                }
+                for item in context_items
+            ]
+            if len(documents) > 0:
+                self.search_client.index(
+                    SEARCH_INDEX_NAME).add_documents(documents)
+
+    # def compile_chat_messages(self, max_tokens: int) -> List[Dict]:
+    #     """
+    #     Compiles the chat prompt into a single string.
+    #     """
+    #     return compile_chat_messages(self.model, self.chat_history, max_tokens, self.prompt, self.functions, self.system_message)
+
+    async def select_context_item(self, id: str, query: str):
+        """
+        Selects the ContextItem with the given id.
+        """
+        id: ContextItemId = ContextItemId.from_string(id)
+        if id.provider_title not in self.provider_titles:
+            raise ValueError(
+                f"Context provider with title {id.provider_title} not found")
+
+        await self.context_providers[id.provider_title].add_context_item(id, query, self.search_client)
+
+    async def delete_context_with_ids(self, ids: List[str]):
+        """
+        Deletes the ContextItems with the given IDs, lets ContextProviders recalculate.
+        """
+
+        # Group by provider title
+        provider_title_to_ids: Dict[str, List[ContextItemId]] = {}
+        for id in ids:
+            id: ContextItemId = ContextItemId.from_string(id)
+            if id.provider_title not in provider_title_to_ids:
+                provider_title_to_ids[id.provider_title] = []
+            provider_title_to_ids[id.provider_title].append(id)
+
+        # Recalculate context for each updated provider
+        for provider_title, ids in provider_title_to_ids.items():
+            await self.context_providers[provider_title].delete_context_with_ids(ids)
+
+    async def clear_context(self):
+        """
+        Clears all context.
+        """
+        for provider in self.context_providers.values():
+            await self.context_providers[provider.title].clear_context()
+
+
+"""
+Should define "ArgsTransformer" and "PromptTransformer" classes for the different LLMs. A standard way for them to ingest the
+same format of prompts so you don't have to redo all of this logic.
+"""
diff --git a/continuedev/src/continuedev/core/context_manager.py b/continuedev/src/continuedev/core/context_manager.py
deleted file mode 100644
index 37905535..00000000
--- a/continuedev/src/continuedev/core/context_manager.py
+++ /dev/null
@@ -1,119 +0,0 @@
-
-from abc import ABC, abstractmethod, abstractproperty
-from ast import List
-from pydantic import BaseModel
-
-from ..libs.util.count_tokens import compile_chat_messages
-
-
-class ContextItemDescription(BaseModel):
-    """
-    A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
-
-    The id can be used to retrieve the ContextItem from the ContextManager.
-    """
-    name: str
-    description: str
-    id:  str
-
-
-class ContextItem(BaseModel):
-    """
-    A ContextItem is a single item that is stored in the ContextManager.
-    """
-    description: ContextItemDescription
-    content: str
-
-
-class ContextManager(ABC):
-    """
-    The context manager is responsible for storing the context to be passed to the LLM, including
-    - ContextItems (highlighted code, GitHub Issues, etc.)
-    - ChatMessages in the history
-    - System Message
-    - Functions
-
-    It is responsible for compiling all of this information into a single prompt without exceeding the token limit.
-    """
-
-    def compile_chat_messages(self, max_tokens: int) -> List[Dict]:
-        """
-        Compiles the chat prompt into a single string.
-        """
-        return compile_chat_messages(self.model, self.chat_history, max_tokens, self.prompt, self.functions, self.system_message)
-
-
-"""
-Should define "ArgsTransformer" and "PromptTransformer" classes for the different LLMs. A standard way for them to ingest the
-same format of prompts so you don't have to redo all of this logic.
-"""
-
-
-class ContextProvider(ABC):
-    """
-    The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'.
-    When you type '@', the context provider will be asked to populate a list of options.
-    These options will be updated on each keystroke.
-    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).
-    """
-
-    title: str
-
-    @abstractmethod
-    async def load(self):
-        """
-        Loads the ContextProvider, possibly reading persisted data from disk. This will be called on startup.
-        """
-
-    @abstractmethod
-    async def save(self):
-        """
-        Saves the ContextProvider, possibly writing persisted data to disk. This will be called upon cache refresh.
-        """
-
-    @abstractmethod
-    async def refresh_cache(self):
-        """
-        Refreshes the cache of items. This will be called on startup and periodically.
-        """
-
-    @abstractmethod
-    async def get_item_descriptions(self, query: str) -> List[ContextItemDescription]:
-        """
-        Returns a list of options that should be displayed to the user.
-        """
-
-    @abstractmethod
-    async def get_item(self, id: str) -> ContextItem:
-        """
-        Returns the ContextItem with the given id. This allows you not to have to load all of the information until an item is selected.
-        """
-
-    @abstractmethod
-    async def should_refresh(self) -> bool:
-        """
-        Returns whether the ContextProvider should be refreshed.
-
-        For example, embeddings might need to be recalculated after commits,
-        or GitHub issues might need to be refreshed after a new issue is created.
-
-        This method will be called every startup? Every once in a while? Every hour?
-        User defined? Maybe just have a schedule instead of this method.
-        """
-
-
-class GitHubIssuesContextProvider(ContextProvider):
-    """
-    The GitHubIssuesContextProvider is a ContextProvider that allows you to search GitHub issues in a repo.
-    """
-
-    title = "issues"
-
-    def __init__(self, repo: str):
-        self.repo = repo
-
-    async def get_item_descriptions(self, query: str) -> List[ContextItemDescription]:
-        pass
-
-    async def get_item(self, id: str) -> ContextItem:
-        pass
diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py
index 50d01f8d..6c6adccc 100644
--- a/continuedev/src/continuedev/core/main.py
+++ b/continuedev/src/continuedev/core/main.py
@@ -1,12 +1,11 @@
 import json
-from textwrap import dedent
-from typing import Callable, Coroutine, Dict, Generator, List, Literal, Tuple, Union
+from typing import Coroutine, Dict, List, Literal, Union
+from pydantic.schema import schema
+
 
-from ..models.filesystem import RangeInFileWithContents
 from ..models.main import ContinueBaseModel
-from pydantic import validator
+from pydantic import BaseModel, validator
 from .observation import Observation
-from pydantic.schema import schema
 
 ChatMessageRole = Literal["assistant", "user", "system", "function"]
 
@@ -201,12 +200,48 @@ class SlashCommandDescription(ContinueBaseModel):
     description: str
 
 
-class HighlightedRangeContext(ContinueBaseModel):
-    """Context for a highlighted range"""
-    range: RangeInFileWithContents
-    editing: bool
-    pinned: bool
-    display_name: str
+class ContextItemId(BaseModel):
+    """
+    A ContextItemId is a unique identifier for a ContextItem.
+    """
+    provider_title: str
+    item_id: str
+
+    def to_string(self) -> str:
+        return f"{self.provider_title}-{self.item_id}"
+
+    @staticmethod
+    def from_string(string: str) -> 'ContextItemId':
+        provider_title, item_id = string.split('-')
+        return ContextItemId(provider_title=provider_title, item_id=item_id)
+
+
+class ContextItemDescription(BaseModel):
+    """
+    A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
+
+    The id can be used to retrieve the ContextItem from the ContextManager.
+    """
+    name: str
+    description: str
+    id:  ContextItemId
+
+
+class ContextItem(BaseModel):
+    """
+    A ContextItem is a single item that is stored in the ContextManager.
+    """
+    description: ContextItemDescription
+    content: str
+
+    @validator('content', pre=True)
+    def content_must_be_string(cls, v):
+        if v is None:
+            return ''
+        return v
+
+    editing: bool = False
+    editable: bool = False
 
 
 class FullState(ContinueBaseModel):
@@ -215,9 +250,9 @@ class FullState(ContinueBaseModel):
     active: bool
     user_input_queue: List[str]
     default_model: str
-    highlighted_ranges: List[HighlightedRangeContext]
     slash_commands: List[SlashCommandDescription]
     adding_highlighted_code: bool
+    selected_context_items: List[ContextItem]
 
 
 class ContinueSDK:
diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py
index 4100efa6..59f33707 100644
--- a/continuedev/src/continuedev/core/sdk.py
+++ b/continuedev/src/continuedev/core/sdk.py
@@ -3,8 +3,10 @@ from functools import cached_property
 from typing import Coroutine, Dict, Union
 import os
 
+
 from ..steps.core.core import DefaultModelEditCodeStep
 from ..models.main import Range
+from .context import ContextItem
 from .abstract_sdk import AbstractContinueSDK
 from .config import ContinueConfig, load_config, load_global_config, update_global_config
 from ..models.filesystem_edit import FileEdit, FileSystemEdit, AddFile, DeleteFile, AddDirectory, DeleteDirectory
@@ -289,28 +291,13 @@ class ContinueSDK(AbstractContinueSDK):
 
     async def get_chat_context(self) -> List[ChatMessage]:
         history_context = self.history.to_chat_history()
-        highlighted_code = [
-            hr.range for hr in self.__autopilot._highlighted_ranges]
-
-        preface = "The following code is highlighted"
-
-        # If no higlighted ranges, use first file as context
-        if len(highlighted_code) == 0:
-            preface = "The following file is open"
-            visible_files = await self.ide.getVisibleFiles()
-            if len(visible_files) > 0:
-                content = await self.ide.readFile(visible_files[0])
-                highlighted_code = [
-                    RangeInFileWithContents.from_entire_file(visible_files[0], content)]
-
-        for rif in highlighted_code:
-            msg = ChatMessage(content=f"{preface} ({rif.filepath}):\n```\n{rif.contents}\n```",
-                              role="user", summary=f"{preface}: {rif.filepath}")
-
-            # Don't insert after latest user message or function call
-            i = -1
-            if len(history_context) > 0 and (history_context[i].role == "user" or history_context[i].role == "function"):
-                i -= 1
+
+        context_messages: List[ChatMessage] = await self.__autopilot.context_manager.get_chat_messages()
+
+        # Insert at the end, but don't insert after latest user message or function call
+        i = -2 if (len(history_context) > 0 and (
+            history_context[-1].role == "user" or history_context[-1].role == "function")) else -1
+        for msg in context_messages:
             history_context.insert(i, msg)
 
         return history_context
diff --git a/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py b/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py
new file mode 100644
index 00000000..23d4fc86
--- /dev/null
+++ b/continuedev/src/continuedev/libs/context_providers/highlighted_code_context_provider.py
@@ -0,0 +1,191 @@
+import os
+from typing import Any, Dict, List
+
+import meilisearch
+from ...core.main import ChatMessage
+from ...models.filesystem import RangeInFile, RangeInFileWithContents
+from ...core.context import ContextItem, ContextItemDescription, ContextItemId
+from pydantic import BaseModel
+
+
+class HighlightedRangeContextItem(BaseModel):
+    rif: RangeInFileWithContents
+    item: ContextItem
+
+
+class HighlightedCodeContextProvider(BaseModel):
+    """
+    The ContextProvider class is a plugin that lets you provide new information to the LLM by typing '@'.
+    When you type '@', the context provider will be asked to populate a list of options.
+    These options will be updated on each keystroke.
+    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).
+    """
+
+    title = "code"
+
+    ide: Any  # IdeProtocolServer
+
+    highlighted_ranges: List[HighlightedRangeContextItem] = []
+    adding_highlighted_code: bool = False
+
+    should_get_fallback_context_item: bool = True
+    last_added_fallback: bool = False
+
+    async def _get_fallback_context_item(self) -> HighlightedRangeContextItem:
+        if not self.should_get_fallback_context_item:
+            return None
+
+        visible_files = await self.ide.getVisibleFiles()
+        if len(visible_files) > 0:
+            content = await self.ide.readFile(visible_files[0])
+            rif = RangeInFileWithContents.from_entire_file(
+                visible_files[0], content)
+
+            item = self._rif_to_context_item(rif, 0, True)
+            item.description.name = self._rif_to_name(
+                rif, show_line_nums=False)
+
+            self.last_added_fallback = True
+            return HighlightedRangeContextItem(rif=rif, item=item)
+
+        return None
+
+    async def get_selected_items(self) -> List[ContextItem]:
+        items = [hr.item for hr in self.highlighted_ranges]
+
+        if len(items) == 0 and (fallback_item := await self._get_fallback_context_item()):
+            items = [fallback_item.item]
+
+        return items
+
+    async def get_chat_messages(self) -> List[ContextItem]:
+        ranges = self.highlighted_ranges
+        if len(ranges) == 0 and (fallback_item := await self._get_fallback_context_item()):
+            ranges = [fallback_item]
+
+        return [ChatMessage(
+            role="user",
+            content=f"Code in this file is highlighted ({r.rif.filepath}):\n```\n{r.rif.contents}\n```",
+            summary=f"Code in this file is highlighted: {r.rif.filepath}"
+        ) for r in ranges]
+
+    def _make_sure_is_editing_range(self):
+        """If none of the highlighted ranges are currently being edited, the first should be selected"""
+        if len(self.highlighted_ranges) == 0:
+            return
+        if not any(map(lambda x: x.item.editing, self.highlighted_ranges)):
+            self.highlighted_ranges[0].item.editing = True
+
+    def _disambiguate_highlighted_ranges(self):
+        """If any files have the same name, also display their folder name"""
+        name_status: Dict[str, set] = {
+        }  # basename -> set of full paths with that basename
+        for hr in self.highlighted_ranges:
+            basename = os.path.basename(hr.rif.filepath)
+            if basename in name_status:
+                name_status[basename].add(hr.rif.filepath)
+            else:
+                name_status[basename] = {hr.rif.filepath}
+
+        for hr in self.highlighted_ranges:
+            if len(name_status[basename]) > 1:
+                hr.item.description.name = self._rif_to_name(hr.rif, display_filename=os.path.join(
+                    os.path.basename(os.path.dirname(hr.rif.filepath)), basename))
+            else:
+                hr.item.description.name = self._rif_to_name(
+                    hr.rif, display_filename=basename)
+
+    async def provide_context_items(self) -> List[ContextItem]:
+        return []
+
+    async def delete_context_with_ids(self, ids: List[ContextItemId]) -> List[ContextItem]:
+        indices_to_delete = [
+            int(id.item_id) for id in ids
+        ]
+
+        kept_ranges = []
+        for i, hr in enumerate(self.highlighted_ranges):
+            if i not in indices_to_delete:
+                kept_ranges.append(hr)
+        self.highlighted_ranges = kept_ranges
+
+        self._make_sure_is_editing_range()
+
+        if len(self.highlighted_ranges) == 0 and self.last_added_fallback:
+            self.should_get_fallback_context_item = False
+
+        return [hr.item for hr in self.highlighted_ranges]
+
+    def _rif_to_name(self, rif: RangeInFileWithContents, display_filename: str = None, show_line_nums: bool = True) -> str:
+        line_nums = f" ({rif.range.start.line + 1}-{rif.range.end.line + 1})" if show_line_nums else ""
+        return f"{display_filename or os.path.basename(rif.filepath)}{line_nums}"
+
+    def _rif_to_context_item(self, rif: RangeInFileWithContents, idx: int, editing: bool) -> ContextItem:
+        return ContextItem(
+            description=ContextItemDescription(
+                name=self._rif_to_name(rif),
+                description=rif.filepath,
+                id=ContextItemId(
+                    provider_title=self.title,
+                    item_id=str(idx)
+                )
+            ),
+            content=rif.contents,
+            editing=editing,
+            editable=True
+        )
+
+    async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]):
+        self.should_get_fallback_context_item = True
+        self.last_added_fallback = False
+
+        # Filter out rifs from ~/.continue/diffs folder
+        range_in_files = [
+            rif for rif in range_in_files if not os.path.dirname(rif.filepath) == os.path.expanduser("~/.continue/diffs")]
+
+        # If not adding highlighted code
+        if not self.adding_highlighted_code:
+            if len(self.highlighted_ranges) == 1 and len(range_in_files) <= 1 and (len(range_in_files) == 0 or range_in_files[0].range.start == range_in_files[0].range.end):
+                # If un-highlighting the range to edit, then remove the range
+                self.highlighted_ranges = []
+            elif len(range_in_files) > 0:
+                # Otherwise, replace the current range with the new one
+                # This is the first range to be highlighted
+                self.highlighted_ranges = [
+                    HighlightedRangeContextItem(
+                        rif=range_in_files[0],
+                        item=self._rif_to_context_item(range_in_files[0], 0, True))]
+
+            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
+
+                # 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 hr.rif.filepath == new_rif.filepath and hr.rif.contents == new_rif.contents:
+                    found_overlap = True
+                    break
+
+            if not found_overlap:
+                new_ranges.append(HighlightedRangeContextItem(rif=hr.rif, item=self._rif_to_context_item(
+                    hr.rif, len(new_ranges), False)))
+
+        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._make_sure_is_editing_range()
+        self._disambiguate_highlighted_ranges()
+
+    async def set_editing_at_ids(self, ids: List[str]):
+        for hr in self.highlighted_ranges:
+            hr.item.editing = hr.item.description.id.to_string() in ids
+
+    async def add_context_item(self, id: ContextItemId, query: str, search_client: meilisearch.Client, prev: List[ContextItem] = None) -> List[ContextItem]:
+        raise NotImplementedError()
diff --git a/continuedev/src/continuedev/libs/util/paths.py b/continuedev/src/continuedev/libs/util/paths.py
index fddef887..d6ce13b3 100644
--- a/continuedev/src/continuedev/libs/util/paths.py
+++ b/continuedev/src/continuedev/libs/util/paths.py
@@ -2,16 +2,26 @@ import os
 
 from ..constants.main import CONTINUE_SESSIONS_FOLDER, CONTINUE_GLOBAL_FOLDER, CONTINUE_SERVER_FOLDER
 
-def getGlobalFolderPath():
-    return os.path.join(os.path.expanduser("~"), CONTINUE_GLOBAL_FOLDER)
 
+def getGlobalFolderPath():
+    path = os.path.join(os.path.expanduser("~"), CONTINUE_GLOBAL_FOLDER)
+    os.makedirs(path, exist_ok=True)
+    return path
 
 
 def getSessionsFolderPath():
-    return os.path.join(getGlobalFolderPath(), CONTINUE_SESSIONS_FOLDER)
+    path = os.path.join(getGlobalFolderPath(), CONTINUE_SESSIONS_FOLDER)
+    os.makedirs(path, exist_ok=True)
+    return path
+
 
 def getServerFolderPath():
-    return os.path.join(getGlobalFolderPath(), CONTINUE_SERVER_FOLDER)
+    path = os.path.join(getGlobalFolderPath(), CONTINUE_SERVER_FOLDER)
+    os.makedirs(path, exist_ok=True)
+    return path
+
 
 def getSessionFilePath(session_id: str):
-    return os.path.join(getSessionsFolderPath(), f"{session_id}.json")
\ No newline at end of file
+    path = os.path.join(getSessionsFolderPath(), f"{session_id}.json")
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    return path
diff --git a/continuedev/src/continuedev/models/generate_json_schema.py b/continuedev/src/continuedev/models/generate_json_schema.py
index 6cebf429..06614984 100644
--- a/continuedev/src/continuedev/models/generate_json_schema.py
+++ b/continuedev/src/continuedev/models/generate_json_schema.py
@@ -2,6 +2,7 @@ from .main import *
 from .filesystem import RangeInFile, FileEdit
 from .filesystem_edit import FileEditWithFullContents
 from ..core.main import History, HistoryNode, FullState
+from ..core.context import ContextItem
 from pydantic import schema_json_of
 import os
 
@@ -13,6 +14,8 @@ MODELS_TO_GENERATE = [
     FileEditWithFullContents
 ] + [
     History, HistoryNode, FullState
+] + [
+    ContextItem
 ]
 
 RENAMES = {
diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py
index ae57c0b6..36b2f3fa 100644
--- a/continuedev/src/continuedev/server/gui.py
+++ b/continuedev/src/continuedev/server/gui.py
@@ -91,25 +91,19 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
                 self.on_clear_history()
             elif message_type == "delete_at_index":
                 self.on_delete_at_index(data["index"])
-            elif message_type == "delete_context_at_indices":
-                self.on_delete_context_at_indices(data["indices"])
+            elif message_type == "delete_context_with_ids":
+                self.on_delete_context_with_ids(data["ids"])
             elif message_type == "toggle_adding_highlighted_code":
                 self.on_toggle_adding_highlighted_code()
             elif message_type == "set_editing_at_indices":
                 self.on_set_editing_at_indices(data["indices"])
-            elif message_type == "set_pinned_at_indices":
-                self.on_set_pinned_at_indices(data["indices"])
             elif message_type == "show_logs_at_index":
                 self.on_show_logs_at_index(data["index"])
+            elif message_type == "select_context_item":
+                self.select_context_item(data["id"], data["query"])
         except Exception as e:
             print(e)
 
-    async def send_state_update(self):
-        state = self.session.autopilot.get_full_state().dict()
-        await self._send_json("state_update", {
-            "state": state
-        })
-
     def on_main_input(self, input: str):
         # Do something with user input
         create_async_task(self.session.autopilot.accept_user_input(
@@ -144,10 +138,10 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
         create_async_task(self.session.autopilot.delete_at_index(
             index), self.session.autopilot.continue_sdk.ide.unique_id)
 
-    def on_delete_context_at_indices(self, indices: List[int]):
+    def on_delete_context_with_ids(self, ids: List[str]):
         create_async_task(
-            self.session.autopilot.delete_context_at_indices(
-                indices), self.session.autopilot.continue_sdk.ide.unique_id
+            self.session.autopilot.delete_context_with_ids(
+                ids), self.session.autopilot.continue_sdk.ide.unique_id
         )
 
     def on_toggle_adding_highlighted_code(self):
@@ -162,18 +156,17 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
                 indices), self.session.autopilot.continue_sdk.ide.unique_id
         )
 
-    def on_set_pinned_at_indices(self, indices: List[int]):
-        create_async_task(
-            self.session.autopilot.set_pinned_at_indices(
-                indices), self.session.autopilot.continue_sdk.ide.unique_id
-        )
-
     def on_show_logs_at_index(self, index: int):
         name = f"continue_logs.txt"
         logs = "\n\n############################################\n\n".join(
             ["This is a log of the exact prompt/completion pairs sent/received from the LLM during this step"] + self.session.autopilot.continue_sdk.history.timeline[index].logs)
         create_async_task(
-            self.session.autopilot.ide.showVirtualFile(name, logs))
+            self.session.autopilot.ide.showVirtualFile(name, logs), self.session.autopilot.continue_sdk.ide.unique_id)
+
+    def select_context_item(self, id: str, query: str):
+        """Called when user selects an item from the dropdown"""
+        create_async_task(
+            self.session.autopilot.select_context_item(id, query), self.session.autopilot.continue_sdk.ide.unique_id)
 
 
 @router.websocket("/ws")
@@ -188,7 +181,7 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we
         protocol.websocket = websocket
 
         # Update any history that may have happened before connection
-        await protocol.send_state_update()
+        await protocol.session.autopilot.update_subscribers()
 
         while AppStatus.should_exit is False:
             message = await websocket.receive_text()
@@ -214,5 +207,5 @@ async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(we
         if websocket.client_state != WebSocketState.DISCONNECTED:
             await websocket.close()
 
-        session_manager.persist_session(session.session_id)
+        await session_manager.persist_session(session.session_id)
         session_manager.remove_session(session.session_id)
diff --git a/continuedev/src/continuedev/server/gui_protocol.py b/continuedev/src/continuedev/server/gui_protocol.py
index 9766fcd0..fb230216 100644
--- a/continuedev/src/continuedev/server/gui_protocol.py
+++ b/continuedev/src/continuedev/server/gui_protocol.py
@@ -1,6 +1,8 @@
 from typing import Any, Dict, List
 from abc import ABC, abstractmethod
 
+from ..core.context import ContextItem
+
 
 class AbstractGUIProtocolServer(ABC):
     @abstractmethod
@@ -23,10 +25,6 @@ class AbstractGUIProtocolServer(ABC):
     def on_step_user_input(self, input: str, index: int):
         """Called when the user inputs a step"""
 
-    @abstractmethod
-    async def send_state_update(self, state: dict):
-        """Send a state update to the client"""
-
     @abstractmethod
     def on_retry_at_index(self, index: int):
         """Called when the user requests a retry at a previous index"""
@@ -42,3 +40,7 @@ class AbstractGUIProtocolServer(ABC):
     @abstractmethod
     def on_delete_at_index(self, index: int):
         """Called when the user requests to delete a step at a given index"""
+
+    @abstractmethod
+    def select_context_item(self, id: str, query: str):
+        """Called when user selects an item from the dropdown"""
diff --git a/continuedev/src/continuedev/server/main.py b/continuedev/src/continuedev/server/main.py
index 42dc0cc1..7ee64041 100644
--- a/continuedev/src/continuedev/server/main.py
+++ b/continuedev/src/continuedev/server/main.py
@@ -1,15 +1,20 @@
+import asyncio
+import subprocess
 import time
+import meilisearch
 import psutil
 import os
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
-from .ide import router as ide_router
-from .gui import router as gui_router
-from .session_manager import session_manager
 import atexit
 import uvicorn
 import argparse
 
+from .ide import router as ide_router
+from .gui import router as gui_router
+from .session_manager import session_manager
+from .meilisearch_server import start_meilisearch
+
 app = FastAPI()
 
 app.include_router(ide_router)
@@ -41,15 +46,20 @@ args = parser.parse_args()
 # log_file = open('output.log', 'a')
 # sys.stdout = log_file
 
-
 def run_server():
     uvicorn.run(app, host="0.0.0.0", port=args.port)
 
 
-def cleanup():
+async def cleanup_coroutine():
     print("Cleaning up sessions")
     for session_id in session_manager.sessions:
-        session_manager.persist_session(session_id)
+        await session_manager.persist_session(session_id)
+
+
+def cleanup():
+    loop = asyncio.new_event_loop()
+    loop.run_until_complete(cleanup_coroutine())
+    loop.close()
 
 
 def cpu_usage_report():
@@ -77,6 +87,12 @@ if __name__ == "__main__":
         # cpu_thread = threading.Thread(target=cpu_usage_loop)
         # cpu_thread.start()
 
+        try:
+            start_meilisearch()
+        except Exception as e:
+            print("Failed to start MeiliSearch")
+            print(e)
+
         run_server()
     except Exception as e:
         cleanup()
diff --git a/continuedev/src/continuedev/server/meilisearch_server.py b/continuedev/src/continuedev/server/meilisearch_server.py
new file mode 100644
index 00000000..419f081f
--- /dev/null
+++ b/continuedev/src/continuedev/server/meilisearch_server.py
@@ -0,0 +1,56 @@
+import os
+import subprocess
+
+import meilisearch
+from ..libs.util.paths import getServerFolderPath
+
+
+def check_meilisearch_installed() -> bool:
+    """
+    Checks if MeiliSearch is installed.
+    """
+
+    serverPath = getServerFolderPath()
+    meilisearchPath = os.path.join(serverPath, "meilisearch")
+
+    return os.path.exists(meilisearchPath)
+
+
+def check_meilisearch_running() -> bool:
+    """
+    Checks if MeiliSearch is running.
+    """
+
+    try:
+        client = meilisearch.Client('http://localhost:7700')
+        resp = client.health()
+        if resp["status"] != "available":
+            return False
+        return True
+    except Exception:
+        return False
+
+
+def start_meilisearch():
+    """
+    Starts the MeiliSearch server, wait for it.
+    """
+
+    # Doesn't work on windows for now
+    if not os.name == "posix":
+        return
+
+    serverPath = getServerFolderPath()
+
+    # Check if MeiliSearch is installed
+    if not check_meilisearch_installed():
+        # Download MeiliSearch
+        print("Downloading MeiliSearch...")
+        subprocess.run(
+            f"curl -L https://install.meilisearch.com | sh", shell=True, check=True, cwd=serverPath)
+
+    # Check if MeiliSearch is running
+    if not check_meilisearch_running():
+        print("Starting MeiliSearch...")
+        subprocess.Popen(["./meilisearch"], cwd=serverPath, stdout=subprocess.DEVNULL,
+                         stderr=subprocess.STDOUT, close_fds=True, start_new_session=True)
diff --git a/continuedev/src/continuedev/server/session_manager.py b/continuedev/src/continuedev/server/session_manager.py
index 90172a4e..96daf92c 100644
--- a/continuedev/src/continuedev/server/session_manager.py
+++ b/continuedev/src/continuedev/server/session_manager.py
@@ -74,7 +74,7 @@ class SessionManager:
 
         async def on_update(state: FullState):
             await session_manager.send_ws_data(session_id, "state_update", {
-                "state": autopilot.get_full_state().dict()
+                "state": state.dict()
             })
 
         autopilot.on_update(on_update)
@@ -84,9 +84,9 @@ class SessionManager:
     def remove_session(self, session_id: str):
         del self.sessions[session_id]
 
-    def persist_session(self, session_id: str):
+    async def persist_session(self, session_id: str):
         """Save the session's FullState as a json file"""
-        full_state = self.sessions[session_id].autopilot.get_full_state()
+        full_state = await self.sessions[session_id].autopilot.get_full_state()
         if not os.path.exists(getSessionsFolderPath()):
             os.mkdir(getSessionsFolderPath())
         with open(getSessionFilePath(session_id), "w") as f:
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index 13e02e86..fa7834f1 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -13,6 +13,7 @@
         "@types/vscode-webview": "^1.57.1",
         "@uiw/react-markdown-preview": "^4.1.13",
         "downshift": "^7.6.0",
+        "meilisearch": "^0.33.0",
         "posthog-js": "^1.58.0",
         "prismjs": "^1.29.0",
         "react": "^18.2.0",
@@ -1423,6 +1424,14 @@
       "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
       "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
     },
+    "node_modules/cross-fetch": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+      "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+      "dependencies": {
+        "node-fetch": "^2.6.12"
+      }
+    },
     "node_modules/css-color-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -2500,6 +2509,14 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/meilisearch": {
+      "version": "0.33.0",
+      "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz",
+      "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==",
+      "dependencies": {
+        "cross-fetch": "^3.1.6"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3092,6 +3109,25 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/node-fetch": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+      "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/node-releases": {
       "version": "2.0.10",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
@@ -4115,6 +4151,11 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
     "node_modules/trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -4423,6 +4464,20 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "node_modules/xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -5325,6 +5380,14 @@
       "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
       "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
     },
+    "cross-fetch": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+      "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+      "requires": {
+        "node-fetch": "^2.6.12"
+      }
+    },
     "css-color-keywords": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -6098,6 +6161,14 @@
         "@types/mdast": "^3.0.0"
       }
     },
+    "meilisearch": {
+      "version": "0.33.0",
+      "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz",
+      "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==",
+      "requires": {
+        "cross-fetch": "^3.1.6"
+      }
+    },
     "merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -6434,6 +6505,14 @@
       "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
       "dev": true
     },
+    "node-fetch": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+      "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+      "requires": {
+        "whatwg-url": "^5.0.0"
+      }
+    },
     "node-releases": {
       "version": "2.0.10",
       "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
@@ -7129,6 +7208,11 @@
         "is-number": "^7.0.0"
       }
     },
+    "tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
     "trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -7315,6 +7399,20 @@
       "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
       "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="
     },
+    "webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
+    "whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "requires": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index 704f520a..e5f5a329 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -14,6 +14,7 @@
     "@types/vscode-webview": "^1.57.1",
     "@uiw/react-markdown-preview": "^4.1.13",
     "downshift": "^7.6.0",
+    "meilisearch": "^0.33.0",
     "posthog-js": "^1.58.0",
     "prismjs": "^1.29.0",
     "react": "^18.2.0",
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index f327e3a3..22a6d000 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,9 @@
-import React, { useEffect, useImperativeHandle, useState } from "react";
+import React, {
+  useContext,
+  useEffect,
+  useImperativeHandle,
+  useState,
+} from "react";
 import { useCombobox } from "downshift";
 import styled from "styled-components";
 import {
@@ -8,13 +13,15 @@ import {
   vscBackground,
   vscForeground,
 } from ".";
-import CodeBlock from "./CodeBlock";
 import PillButton from "./PillButton";
 import HeaderButtonWithText from "./HeaderButtonWithText";
 import { DocumentPlus } from "@styled-icons/heroicons-outline";
-import { HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem } from "../../../schema/FullState";
 import { postVscMessage } from "../vscode";
-import { getMetaKeyLabel } from "../util";
+import { GUIClientContext } from "../App";
+import { MeiliSearch } from "meilisearch";
+
+const SEARCH_INDEX_NAME = "continue_context_items";
 
 // #region styled components
 const mainInputFontSize = 13;
@@ -64,14 +71,14 @@ const Ul = styled.ul<{
   hidden: boolean;
   showAbove: boolean;
   ulHeightPixels: number;
+  inputBoxHeight?: string;
 }>`
   ${(props) =>
     props.showAbove
       ? `transform: translateY(-${props.ulHeightPixels + 8}px);`
-      : `transform: translateY(${2 * mainInputFontSize}px);`}
+      : `transform: translateY(${5 * mainInputFontSize}px);`}
   position: absolute;
   background: ${vscBackground};
-  background-color: ${secondaryDark};
   color: ${vscForeground};
   max-height: ${UlMaxHeight}px;
   width: calc(100% - 16px);
@@ -80,15 +87,9 @@ const Ul = styled.ul<{
   padding: 0;
   ${({ hidden }) => hidden && "display: none;"}
   border-radius: ${defaultBorderRadius};
-  outline: 0.5px solid gray;
+  outline: 1px solid ${lightGray};
   z-index: 2;
-  // Get rid of scrollbar and its padding
-  scrollbar-width: none;
   -ms-overflow-style: none;
-  &::-webkit-scrollbar {
-    width: 0px;
-    background: transparent; /* make scrollbar transparent */
-  }
 `;
 
 const Li = styled.li<{
@@ -96,49 +97,90 @@ const Li = styled.li<{
   selected: boolean;
   isLastItem: boolean;
 }>`
-  background-color: ${secondaryDark};
-  ${({ highlighted }) => highlighted && "background: #ff000066;"}
+  background-color: ${({ highlighted }) =>
+    highlighted ? lightGray : secondaryDark};
+  ${({ highlighted }) => highlighted && `background: ${vscBackground};`}
   ${({ selected }) => selected && "font-weight: bold;"}
     padding: 0.5rem 0.75rem;
   display: flex;
   flex-direction: column;
   ${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
-  border-top: 1px solid gray;
+  /* border-top: 1px solid gray; */
   cursor: pointer;
 `;
 
 // #endregion
 
 interface ComboBoxProps {
-  items: { name: string; description: string }[];
+  items: { name: string; description: string; id?: string }[];
   onInputValueChange: (inputValue: string) => void;
   disabled?: boolean;
   onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
-  highlightedCodeSections: HighlightedRangeContext[];
-  deleteContextItems: (indices: number[]) => void;
-  onTogglePin: () => void;
+  selectedContextItems: ContextItem[];
   onToggleAddContext: () => void;
   addingHighlightedCode: boolean;
 }
 
 const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+  const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+  const client = useContext(GUIClientContext);
+
   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 [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
-    props.highlightedCodeSections || []
-  );
+
   const inputRef = React.useRef<HTMLInputElement>(null);
+  const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
+    undefined
+  );
 
-  useEffect(() => {
-    setHighlightedCodeSections(props.highlightedCodeSections || []);
-  }, [props.highlightedCodeSections]);
+  // Whether the current input follows an '@' and should be treated as context query
+  const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
 
   const { getInputProps, ...downshiftProps } = useCombobox({
-    onInputValueChange({ inputValue }) {
+    onSelectedItemChange: ({ selectedItem }) => {
+      if (selectedItem?.id) {
+        // Get the query from the input value
+        const segs = downshiftProps.inputValue.split("@");
+        const query = segs[segs.length - 1];
+        const restOfInput = segs.splice(0, segs.length - 1).join("@");
+
+        // Tell server the context item was selected
+        client?.selectContextItem(selectedItem.id, query);
+
+        // Remove the '@' and the context query from the input
+        if (downshiftProps.inputValue.includes("@")) {
+          downshiftProps.setInputValue(restOfInput);
+        }
+      }
+    },
+    onInputValueChange({ inputValue, highlightedIndex }) {
       if (!inputValue) return;
       props.onInputValueChange(inputValue);
+
+      if (inputValue.endsWith("@") || currentlyInContextQuery) {
+        setCurrentlyInContextQuery(true);
+
+        const segs = inputValue.split("@");
+        const providerAndQuery = segs[segs.length - 1];
+        const [provider, query] = providerAndQuery.split(" ");
+        searchClient
+          .index(SEARCH_INDEX_NAME)
+          .search(providerAndQuery)
+          .then((res) => {
+            setItems(
+              res.hits.map((hit) => {
+                return {
+                  name: hit.name,
+                  description: hit.description,
+                  id: hit.id,
+                };
+              })
+            );
+          });
+        return;
+      }
       setItems(
         props.items.filter((item) =>
           item.name.toLowerCase().startsWith(inputValue.toLowerCase())
@@ -151,6 +193,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
     },
   });
 
+  useEffect(() => {
+    console.log(
+      "downshiftProps.highlightedIndex",
+      downshiftProps.highlightedIndex
+    );
+    if (downshiftProps.highlightedIndex < 0) {
+      downshiftProps.setHighlightedIndex(0);
+    }
+  }, [downshiftProps.inputValue]);
+
   useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
 
   const [metaKeyPressed, setMetaKeyPressed] = useState(false);
@@ -199,50 +251,22 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
   return (
     <>
       <div className="px-2 flex gap-2 items-center flex-wrap mt-2">
-        {/* {highlightedCodeSections.length > 1 && (
-          <>
-            <HeaderButtonWithText
-              text="Clear Context"
-              onClick={() => {
-                props.deleteContextItems(
-                  highlightedCodeSections.map((_, idx) => idx)
-                );
-              }}
-            >
-              <Trash size="1.6em" />
-            </HeaderButtonWithText>
-          </>
-        )} */}
-        {highlightedCodeSections.map((section, idx) => (
-          <PillButton
-            warning={
-              section.range.contents.length > 4000 && section.editing
-                ? "Editing such a large range may be slow"
-                : undefined
-            }
-            onlyShowDelete={
-              highlightedCodeSections.length <= 1 || section.editing
-            }
-            editing={section.editing}
-            pinned={section.pinned}
-            index={idx}
-            key={`${section.display_name}${idx}`}
-            title={`${section.display_name} (${
-              section.range.range.start.line + 1
-            }-${section.range.range.end.line + 1})`}
-            onDelete={() => {
-              if (props.deleteContextItems) {
-                props.deleteContextItems([idx]);
+        {props.selectedContextItems.map((item, idx) => {
+          return (
+            <PillButton
+              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
               }
-              setHighlightedCodeSections((prev) => {
-                const newSections = [...prev];
-                newSections.splice(idx, 1);
-                return newSections;
-              });
-            }}
-          />
-        ))}
-        {props.highlightedCodeSections.length > 0 &&
+              addingHighlightedCode={props.addingHighlightedCode}
+              index={idx}
+            />
+          );
+        })}
+        {props.selectedContextItems.length > 0 &&
           (props.addingHighlightedCode ? (
             <EmptyPillDiv
               onClick={() => {
@@ -275,6 +299,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 target.scrollHeight,
                 300
               ).toString()}px`;
+              setInputBoxHeight(target.style.height);
 
               // setShowContextDropdown(target.value.endsWith("@"));
             },
@@ -289,6 +314,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
               if (event.key === "Enter" && event.shiftKey) {
                 // Prevent Downshift's default 'Enter' behavior.
                 (event.nativeEvent as any).preventDownshiftDefault = true;
+                setCurrentlyInContextQuery(false);
               } else if (
                 event.key === "Enter" &&
                 (!downshiftProps.isOpen || items.length === 0)
@@ -302,6 +328,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 (event.nativeEvent as any).preventDownshiftDefault = true;
 
                 if (props.onEnter) props.onEnter(event);
+                setCurrentlyInContextQuery(false);
               } else if (event.key === "Tab" && items.length > 0) {
                 downshiftProps.setInputValue(items[0].name);
                 event.preventDefault();
@@ -321,6 +348,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 }
                 downshiftProps.setInputValue(history[positionInHistory - 1]);
                 setPositionInHistory((prev) => prev - 1);
+                setCurrentlyInContextQuery(false);
               } else if (event.key === "ArrowDown") {
                 if (positionInHistory < history.length) {
                   downshiftProps.setInputValue(history[positionInHistory + 1]);
@@ -328,6 +356,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 setPositionInHistory((prev) =>
                   Math.min(prev + 1, history.length)
                 );
+                setCurrentlyInContextQuery(false);
               }
             },
             ref: inputRef,
@@ -351,13 +380,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 selected={downshiftProps.selectedItem === item}
               >
                 <span>
-                  {item.name}: {item.description}
+                  {item.name}:{"  "}
+                  <span style={{ color: lightGray }}>{item.description}</span>
                 </span>
               </Li>
             ))}
         </Ul>
       </div>
-      {highlightedCodeSections.length === 0 &&
+      {props.selectedContextItems.length === 0 &&
         (downshiftProps.inputValue?.startsWith("/edit") ||
           (focused &&
             metaKeyPressed &&
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index c24dba83..af4263af 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,9 +1,10 @@
-import { useContext, useState } from "react";
+import { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import {
   StyledTooltip,
   defaultBorderRadius,
   secondaryDark,
+  vscBackground,
   vscForeground,
 } from ".";
 import {
@@ -12,6 +13,14 @@ import {
   ExclamationTriangle,
 } from "@styled-icons/heroicons-outline";
 import { GUIClientContext } from "../App";
+import { useDispatch } from "react-redux";
+import {
+  setBottomMessage,
+  setBottomMessageCloseTimeout,
+} 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;
@@ -67,19 +76,48 @@ const CircleDiv = styled.div`
 
 interface PillButtonProps {
   onHover?: (arg0: boolean) => void;
-  onDelete?: () => void;
-  title: string;
-  index: number;
-  editing: boolean;
-  pinned: boolean;
+  item: ContextItem;
   warning?: string;
-  onlyShowDelete?: boolean;
+  index: number;
+  addingHighlightedCode?: boolean;
 }
 
 const PillButton = (props: PillButtonProps) => {
   const [isHovered, setIsHovered] = useState(false);
   const client = useContext(GUIClientContext);
 
+  const dispatch = useDispatch();
+
+  useEffect(() => {
+    if (isHovered) {
+      dispatch(setBottomMessageCloseTimeout(undefined));
+      dispatch(
+        setBottomMessage(
+          <>
+            <b>{props.item.description.name}</b>:{" "}
+            {props.item.description.description}
+            <StyledMarkdownPreview
+              source={`\`\`\`\n${props.item.content}\n\`\`\``}
+              wrapperElement={{
+                "data-color-mode": "dark",
+              }}
+            />
+          </>
+        )
+      );
+    } else {
+      dispatch(
+        setBottomMessageCloseTimeout(
+          setTimeout(() => {
+            if (!isHovered) {
+              dispatch(setBottomMessage(undefined));
+            }
+          }, 2000)
+        )
+      );
+    }
+  }, [isHovered]);
+
   return (
     <>
       <div style={{ position: "relative" }}>
@@ -88,10 +126,8 @@ const PillButton = (props: PillButtonProps) => {
             position: "relative",
             borderColor: props.warning
               ? "red"
-              : props.editing
+              : props.item.editing
               ? "#8800aa"
-              : props.pinned
-              ? "#ffff0099"
               : "transparent",
             borderWidth: "1px",
             borderStyle: "solid",
@@ -112,10 +148,14 @@ const PillButton = (props: PillButtonProps) => {
           {isHovered && (
             <GridDiv
               style={{
-                gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr",
+                gridTemplateColumns:
+                  props.item.editable && props.addingHighlightedCode
+                    ? "1fr 1fr"
+                    : "1fr",
+                backgroundColor: vscBackground,
               }}
             >
-              {props.onlyShowDelete || (
+              {props.item.editable && props.addingHighlightedCode && (
                 <ButtonDiv
                   data-tooltip-id={`edit-${props.index}`}
                   backgroundColor={"#8800aa55"}
@@ -130,15 +170,6 @@ const PillButton = (props: PillButtonProps) => {
                 </ButtonDiv>
               )}
 
-              {/* <ButtonDiv
-            data-tooltip-id={`pin-${props.index}`}
-            backgroundColor={"#ffff0055"}
-            onClick={() => {
-              client?.setPinnedAtIndices([props.index]);
-            }}
-            >
-            <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>
-          </ButtonDiv> */}
               <StyledTooltip id={`pin-${props.index}`}>
                 Edit this range
               </StyledTooltip>
@@ -146,33 +177,33 @@ const PillButton = (props: PillButtonProps) => {
                 data-tooltip-id={`delete-${props.index}`}
                 backgroundColor={"#cc000055"}
                 onClick={() => {
-                  if (props.onDelete) {
-                    props.onDelete();
-                  }
+                  client?.deleteContextWithIds([props.item.description.id]);
                 }}
               >
                 <Trash style={{ margin: "auto" }} width="1.6em"></Trash>
               </ButtonDiv>
             </GridDiv>
           )}
-          {props.title}
+          {props.item.description.name}
         </Button>
         <StyledTooltip id={`edit-${props.index}`}>
-          {props.editing
+          {props.item.editing
             ? "Editing this section (with entire file as context)"
             : "Edit this section"}
         </StyledTooltip>
         <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
         {props.warning && (
           <>
-            <CircleDiv data-tooltip-id={`circle-div-${props.title}`}>
+            <CircleDiv
+              data-tooltip-id={`circle-div-${props.item.description.name}`}
+            >
               <ExclamationTriangle
                 style={{ margin: "auto" }}
                 width="1.0em"
                 strokeWidth={2}
               />
             </CircleDiv>
-            <StyledTooltip id={`circle-div-${props.title}`}>
+            <StyledTooltip id={`circle-div-${props.item.description.name}`}>
               {props.warning}
             </StyledTooltip>
           </>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index bc8665fd..2cfe7ecd 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -18,9 +18,9 @@ import {
 import { StopCircle } from "@styled-icons/heroicons-solid";
 import { HistoryNode } from "../../../schema/HistoryNode";
 import HeaderButtonWithText from "./HeaderButtonWithText";
-import MarkdownPreview from "@uiw/react-markdown-preview";
 import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
 import { GUIClientContext } from "../App";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
 
 interface StepContainerProps {
   historyNode: HistoryNode;
@@ -109,33 +109,6 @@ const GradientBorder = styled.div<{
   background-size: 200% 200%;
 `;
 
-const StyledMarkdownPreview = styled(MarkdownPreview)`
-  pre {
-    background-color: ${secondaryDark};
-    padding: 1px;
-    border-radius: ${defaultBorderRadius};
-    border: 0.5px solid white;
-  }
-
-  code {
-    color: #f78383;
-    word-wrap: break-word;
-    border-radius: ${defaultBorderRadius};
-    background-color: ${secondaryDark};
-  }
-
-  pre > code {
-    background-color: ${secondaryDark};
-    color: ${vscForeground};
-  }
-
-  background-color: ${vscBackground};
-  font-family: "Lexend", sans-serif;
-  font-size: 13px;
-  padding: 8px;
-  color: ${vscForeground};
-`;
-
 // #endregion
 
 function StepContainer(props: StepContainerProps) {
diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx
new file mode 100644
index 00000000..9c2ecb62
--- /dev/null
+++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx
@@ -0,0 +1,37 @@
+import styled from "styled-components";
+import {
+  defaultBorderRadius,
+  secondaryDark,
+  vscBackground,
+  vscForeground,
+} from ".";
+import MarkdownPreview from "@uiw/react-markdown-preview";
+
+const StyledMarkdownPreview = styled(MarkdownPreview)`
+  pre {
+    background-color: ${secondaryDark};
+    padding: 1px;
+    border-radius: ${defaultBorderRadius};
+    border: 0.5px solid white;
+  }
+
+  code {
+    color: #f78383;
+    word-wrap: break-word;
+    border-radius: ${defaultBorderRadius};
+    background-color: ${secondaryDark};
+  }
+
+  pre > code {
+    background-color: ${secondaryDark};
+    color: ${vscForeground};
+  }
+
+  background-color: ${vscBackground};
+  font-family: "Lexend", sans-serif;
+  font-size: 13px;
+  padding: 8px;
+  color: ${vscForeground};
+`;
+
+export default StyledMarkdownPreview;
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index cba3852d..7c6ba052 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -5,7 +5,7 @@ import { Button, secondaryDark, vscBackground, vscForeground } from ".";
 import { isMetaEquivalentKeyPressed } from "../util";
 
 const ScreenCover = styled.div`
-  position: absolute;
+  position: fixed;
   width: 100%;
   height: 100%;
   background-color: rgba(168, 168, 168, 0.5);
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 6c0df8fc..ddf65272 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -1,3 +1,5 @@
+import { ContextItemId } from "../../../schema/FullState";
+
 abstract class AbstractContinueGUIClientProtocol {
   abstract sendMainInput(input: string): void;
 
@@ -21,15 +23,15 @@ abstract class AbstractContinueGUIClientProtocol {
 
   abstract deleteAtIndex(index: number): void;
 
-  abstract deleteContextAtIndices(indices: number[]): void;
+  abstract deleteContextWithIds(ids: ContextItemId[]): void;
 
   abstract setEditingAtIndices(indices: number[]): void;
 
-  abstract setPinnedAtIndices(indices: number[]): void;
-
   abstract toggleAddingHighlightedCode(): void;
 
   abstract showLogsAtIndex(index: number): void;
+
+  abstract selectContextItem(id: string, query: string): void;
 }
 
 export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 7d6c2a71..1048e956 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -1,3 +1,4 @@
+import { ContextItemId } from "../../../schema/FullState";
 import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol";
 import { Messenger, WebsocketMessenger } from "./messenger";
 import { VscodeMessenger } from "./vscodeMessenger";
@@ -68,18 +69,16 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
     this.messenger.send("delete_at_index", { index });
   }
 
-  deleteContextAtIndices(indices: number[]) {
-    this.messenger.send("delete_context_at_indices", { indices });
+  deleteContextWithIds(ids: ContextItemId[]) {
+    this.messenger.send("delete_context_with_ids", {
+      ids: ids.map((id) => `${id.provider_title}-${id.item_id}`),
+    });
   }
 
   setEditingAtIndices(indices: number[]) {
     this.messenger.send("set_editing_at_indices", { indices });
   }
 
-  setPinnedAtIndices(indices: number[]) {
-    this.messenger.send("set_pinned_at_indices", { indices });
-  }
-
   toggleAddingHighlightedCode(): void {
     this.messenger.send("toggle_adding_highlighted_code", {});
   }
@@ -87,6 +86,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
   showLogsAtIndex(index: number): void {
     this.messenger.send("show_logs_at_index", { index });
   }
+
+  selectContextItem(id: string, query: string): void {
+    this.messenger.send("select_context_item", { id, query });
+  }
 }
 
 export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index fccc9b4b..a1ba1c33 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -6,7 +6,7 @@ import {
 } from "../components";
 import Loader from "../components/Loader";
 import ContinueButton from "../components/ContinueButton";
-import { FullState, HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem, FullState } from "../../../schema/FullState";
 import { useCallback, useEffect, useRef, useState, useContext } from "react";
 import { History } from "../../../schema/History";
 import { HistoryNode } from "../../../schema/HistoryNode";
@@ -22,12 +22,16 @@ import TextDialog from "../components/TextDialog";
 import HeaderButtonWithText from "../components/HeaderButtonWithText";
 import ReactSwitch from "react-switch";
 import { usePostHog } from "posthog-js/react";
-import { useSelector } from "react-redux";
+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,
+} from "../redux/slices/uiStateSlice";
 
 const TopGUIDiv = styled.div`
   overflow: hidden;
@@ -78,15 +82,13 @@ function GUI(props: GUIProps) {
   const [usingFastModel, setUsingFastModel] = useState(false);
   const [waitingForSteps, setWaitingForSteps] = useState(false);
   const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
-  const [highlightedRanges, setHighlightedRanges] = useState<
-    HighlightedRangeContext[]
-  >([]);
   const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
+  const [selectedContextItems, setSelectedContextItems] = useState<
+    ContextItem[]
+  >([]);
   const [availableSlashCommands, setAvailableSlashCommands] = useState<
     { name: string; description: string }[]
   >([]);
-  const [pinned, setPinned] = useState(false);
-  const [showDataSharingInfo, setShowDataSharingInfo] = useState(false);
   const [stepsOpen, setStepsOpen] = useState<boolean[]>([
     true,
     true,
@@ -118,6 +120,11 @@ function GUI(props: GUIProps) {
   const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
   const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
 
+  const dispatch = useDispatch();
+  const bottomMessage = useSelector(
+    (state: RootStore) => state.uiState.bottomMessage
+  );
+
   const topGuiDivRef = useRef<HTMLDivElement>(null);
 
   const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
@@ -179,7 +186,8 @@ function GUI(props: GUIProps) {
 
       setWaitingForSteps(waitingForSteps);
       setHistory(state.history);
-      setHighlightedRanges(state.highlighted_ranges);
+      console.log((state as any).selected_context_items);
+      setSelectedContextItems(state.selected_context_items);
       setUserInputQueue(state.user_input_queue);
       setAddingHighlightedCode(state.adding_highlighted_code);
       setAvailableSlashCommands(
@@ -214,13 +222,6 @@ function GUI(props: GUIProps) {
 
   const mainTextInputRef = useRef<HTMLInputElement>(null);
 
-  const deleteContextItems = useCallback(
-    (indices: number[]) => {
-      client?.deleteContextAtIndices(indices);
-    },
-    [client]
-  );
-
   const onMainTextInput = (event?: any) => {
     if (mainTextInputRef.current) {
       let input = (mainTextInputRef.current as any).inputValue;
@@ -360,11 +361,7 @@ function GUI(props: GUIProps) {
           }}
           onInputValueChange={() => {}}
           items={availableSlashCommands}
-          highlightedCodeSections={highlightedRanges}
-          deleteContextItems={deleteContextItems}
-          onTogglePin={() => {
-            setPinned((prev: boolean) => !prev);
-          }}
+          selectedContextItems={selectedContextItems}
           onToggleAddContext={() => {
             client?.toggleAddingHighlightedCode();
           }}
@@ -373,29 +370,30 @@ function GUI(props: GUIProps) {
         <ContinueButton onClick={onMainTextInput} />
       </TopGUIDiv>
       <div
+        onMouseEnter={() => {
+          dispatch(setBottomMessageCloseTimeout(undefined));
+        }}
+        onMouseLeave={() => {
+          dispatch(setBottomMessage(undefined));
+        }}
         style={{
           position: "fixed",
           bottom: "50px",
+          left: "0",
+          right: "0",
+          margin: "16px",
           backgroundColor: vscBackground,
           color: vscForeground,
           borderRadius: defaultBorderRadius,
           padding: "16px",
-          margin: "16px",
           zIndex: 100,
           boxShadow: `0px 0px 10px 0px ${vscForeground}`,
+          maxHeight: "50vh",
+          overflow: "scroll",
         }}
-        hidden={!showDataSharingInfo}
+        hidden={!bottomMessage}
       >
-        By turning on this switch, you will begin collecting accepted and
-        rejected suggestions in .continue/suggestions.json. This data is stored
-        locally on your machine and not sent anywhere.
-        <br />
-        <br />
-        <b>
-          {dataSwitchChecked
-            ? "👍 Data is being collected"
-            : "👎 No data is being collected"}
-        </b>
+        {bottomMessage}
       </div>
       <Footer dataSwitchChecked={dataSwitchChecked}>
         <div
@@ -406,10 +404,25 @@ function GUI(props: GUIProps) {
             alignItems: "center",
           }}
           onMouseEnter={() => {
-            setShowDataSharingInfo(true);
+            dispatch(
+              setBottomMessage(
+                <>
+                  By turning on this switch, you will begin collecting accepted
+                  and rejected suggestions in .continue/suggestions.json. This
+                  data is stored locally on your machine and not sent anywhere.
+                  <br />
+                  <br />
+                  <b>
+                    {dataSwitchChecked
+                      ? "👍 Data is being collected"
+                      : "👎 No data is being collected"}
+                  </b>
+                </>
+              )
+            );
           }}
           onMouseLeave={() => {
-            setShowDataSharingInfo(false);
+            dispatch(setBottomMessage(undefined));
           }}
         >
           <ReactSwitch
diff --git a/extension/react-app/src/redux/selectors/uiStateSelectors.ts b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
new file mode 100644
index 00000000..7ebc9338
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
@@ -0,0 +1,5 @@
+import { RootStore } from "../store";
+
+const selectBottomMessage = (state: RootStore) => state.uiState.bottomMessage;
+
+export { selectBottomMessage };
diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts
new file mode 100644
index 00000000..837d19e9
--- /dev/null
+++ b/extension/react-app/src/redux/slices/uiStateSlice.ts
@@ -0,0 +1,24 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+export const uiStateSlice = createSlice({
+  name: "uiState",
+  initialState: {
+    bottomMessage: undefined,
+    bottomMessageCloseTimeout: undefined,
+  },
+  reducers: {
+    setBottomMessage: (state, action) => {
+      state.bottomMessage = action.payload;
+    },
+    setBottomMessageCloseTimeout: (state, action) => {
+      if (state.bottomMessageCloseTimeout) {
+        clearTimeout(state.bottomMessageCloseTimeout);
+      }
+      state.bottomMessageCloseTimeout = action.payload;
+    },
+  },
+});
+
+export const { setBottomMessage, setBottomMessageCloseTimeout } =
+  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 b6eb55b3..d49513e5 100644
--- a/extension/react-app/src/redux/store.ts
+++ b/extension/react-app/src/redux/store.ts
@@ -3,6 +3,7 @@ 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";
 
 export interface ChatMessage {
@@ -31,6 +32,10 @@ export interface RootStore {
   misc: {
     highlightedCode: RangeInFile | undefined;
   };
+  uiState: {
+    bottomMessage: JSX.Element | undefined;
+    bottomMessageCloseTimeout: NodeJS.Timeout | undefined;
+  };
 }
 
 const store = configureStore({
@@ -39,6 +44,7 @@ const store = configureStore({
     chat: chatReducer,
     config: configReducer,
     misc: miscReducer,
+    uiState: uiStateReducer,
   },
 });
 
diff --git a/extension/schema/ContextItem.d.ts b/extension/schema/ContextItem.d.ts
new file mode 100644
index 00000000..62d2c32f
--- /dev/null
+++ b/extension/schema/ContextItem.d.ts
@@ -0,0 +1,45 @@
+/* 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 ContextItem = ContextItem1;
+export type Name = string;
+export type Description = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content = string;
+export type Editing = boolean;
+export type Editable = boolean;
+
+/**
+ * A ContextItem is a single item that is stored in the ContextManager.
+ */
+export interface ContextItem1 {
+  description: ContextItemDescription;
+  content: Content;
+  editing?: Editing;
+  editable?: Editable;
+  [k: string]: unknown;
+}
+/**
+ * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
+ *
+ * The id can be used to retrieve the ContextItem from the ContextManager.
+ */
+export interface ContextItemDescription {
+  name: Name;
+  description: Description;
+  id: ContextItemId;
+  [k: string]: unknown;
+}
+/**
+ * A ContextItemId is a unique identifier for a ContextItem.
+ */
+export interface ContextItemId {
+  provider_title: ProviderTitle;
+  item_id: ItemId;
+  [k: string]: unknown;
+}
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
index 1b7b1f3b..0095f41b 100644
--- a/extension/schema/FullState.d.ts
+++ b/extension/schema/FullState.d.ts
@@ -27,18 +27,18 @@ export type CurrentIndex = number;
 export type Active1 = boolean;
 export type UserInputQueue = string[];
 export type DefaultModel = string;
-export type Filepath = string;
-export type Line = number;
-export type Character = number;
-export type Contents = string;
-export type Editing = boolean;
-export type Pinned = boolean;
-export type DisplayName = string;
-export type HighlightedRanges = HighlightedRangeContext[];
 export type Name3 = string;
 export type Description1 = string;
 export type SlashCommands = SlashCommandDescription[];
 export type AddingHighlightedCode = boolean;
+export type Name4 = string;
+export type Description2 = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content1 = string;
+export type Editing = boolean;
+export type Editable = boolean;
+export type SelectedContextItems = ContextItem[];
 
 /**
  * A full state of the program, including the history
@@ -48,9 +48,9 @@ export interface FullState1 {
   active: Active1;
   user_input_queue: UserInputQueue;
   default_model: DefaultModel;
-  highlighted_ranges: HighlightedRanges;
   slash_commands: SlashCommands;
   adding_highlighted_code: AddingHighlightedCode;
+  selected_context_items: SelectedContextItems;
   [k: string]: unknown;
 }
 /**
@@ -98,40 +98,37 @@ export interface FunctionCall {
 export interface Observation {
   [k: string]: unknown;
 }
-/**
- * Context for a highlighted range
- */
-export interface HighlightedRangeContext {
-  range: RangeInFileWithContents;
-  editing: Editing;
-  pinned: Pinned;
-  display_name: DisplayName;
+export interface SlashCommandDescription {
+  name: Name3;
+  description: Description1;
   [k: string]: unknown;
 }
 /**
- * A range in a file with the contents of the range.
+ * A ContextItem is a single item that is stored in the ContextManager.
  */
-export interface RangeInFileWithContents {
-  filepath: Filepath;
-  range: Range;
-  contents: Contents;
+export interface ContextItem {
+  description: ContextItemDescription;
+  content: Content1;
+  editing?: Editing;
+  editable?: Editable;
   [k: string]: unknown;
 }
 /**
- * A range in a file. 0-indexed.
+ * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
+ *
+ * The id can be used to retrieve the ContextItem from the ContextManager.
  */
-export interface Range {
-  start: Position;
-  end: Position;
+export interface ContextItemDescription {
+  name: Name4;
+  description: Description2;
+  id: ContextItemId;
   [k: string]: unknown;
 }
-export interface Position {
-  line: Line;
-  character: Character;
-  [k: string]: unknown;
-}
-export interface SlashCommandDescription {
-  name: Name3;
-  description: Description1;
+/**
+ * A ContextItemId is a unique identifier for a ContextItem.
+ */
+export interface ContextItemId {
+  provider_title: ProviderTitle;
+  item_id: ItemId;
   [k: string]: unknown;
 }
diff --git a/schema/json/ContextItem.json b/schema/json/ContextItem.json
new file mode 100644
index 00000000..35766f11
--- /dev/null
+++ b/schema/json/ContextItem.json
@@ -0,0 +1,76 @@
+{
+  "title": "ContextItem",
+  "$ref": "#/definitions/src__continuedev__core__context__ContextItem",
+  "definitions": {
+    "ContextItemId": {
+      "title": "ContextItemId",
+      "description": "A ContextItemId is a unique identifier for a ContextItem.",
+      "type": "object",
+      "properties": {
+        "provider_title": {
+          "title": "Provider Title",
+          "type": "string"
+        },
+        "item_id": {
+          "title": "Item Id",
+          "type": "string"
+        }
+      },
+      "required": [
+        "provider_title",
+        "item_id"
+      ]
+    },
+    "ContextItemDescription": {
+      "title": "ContextItemDescription",
+      "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "description": {
+          "title": "Description",
+          "type": "string"
+        },
+        "id": {
+          "$ref": "#/definitions/ContextItemId"
+        }
+      },
+      "required": [
+        "name",
+        "description",
+        "id"
+      ]
+    },
+    "src__continuedev__core__context__ContextItem": {
+      "title": "ContextItem",
+      "description": "A ContextItem is a single item that is stored in the ContextManager.",
+      "type": "object",
+      "properties": {
+        "description": {
+          "$ref": "#/definitions/ContextItemDescription"
+        },
+        "content": {
+          "title": "Content",
+          "type": "string"
+        },
+        "editing": {
+          "title": "Editing",
+          "default": false,
+          "type": "boolean"
+        },
+        "editable": {
+          "title": "Editable",
+          "default": false,
+          "type": "boolean"
+        }
+      },
+      "required": [
+        "description",
+        "content"
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/schema/json/FullState.json b/schema/json/FullState.json
index 62ed337b..7b6dfd6b 100644
--- a/schema/json/FullState.json
+++ b/schema/json/FullState.json
@@ -157,108 +157,92 @@
         "current_index"
       ]
     },
-    "Position": {
-      "title": "Position",
+    "SlashCommandDescription": {
+      "title": "SlashCommandDescription",
       "type": "object",
       "properties": {
-        "line": {
-          "title": "Line",
-          "type": "integer"
+        "name": {
+          "title": "Name",
+          "type": "string"
         },
-        "character": {
-          "title": "Character",
-          "type": "integer"
+        "description": {
+          "title": "Description",
+          "type": "string"
         }
       },
       "required": [
-        "line",
-        "character"
+        "name",
+        "description"
       ]
     },
-    "Range": {
-      "title": "Range",
-      "description": "A range in a file. 0-indexed.",
+    "ContextItemId": {
+      "title": "ContextItemId",
+      "description": "A ContextItemId is a unique identifier for a ContextItem.",
       "type": "object",
       "properties": {
-        "start": {
-          "$ref": "#/definitions/Position"
+        "provider_title": {
+          "title": "Provider Title",
+          "type": "string"
         },
-        "end": {
-          "$ref": "#/definitions/Position"
+        "item_id": {
+          "title": "Item Id",
+          "type": "string"
         }
       },
       "required": [
-        "start",
-        "end"
+        "provider_title",
+        "item_id"
       ]
     },
-    "RangeInFileWithContents": {
-      "title": "RangeInFileWithContents",
-      "description": "A range in a file with the contents of the range.",
+    "ContextItemDescription": {
+      "title": "ContextItemDescription",
+      "description": "A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.\n\nThe id can be used to retrieve the ContextItem from the ContextManager.",
       "type": "object",
       "properties": {
-        "filepath": {
-          "title": "Filepath",
+        "name": {
+          "title": "Name",
           "type": "string"
         },
-        "range": {
-          "$ref": "#/definitions/Range"
-        },
-        "contents": {
-          "title": "Contents",
+        "description": {
+          "title": "Description",
           "type": "string"
+        },
+        "id": {
+          "$ref": "#/definitions/ContextItemId"
         }
       },
       "required": [
-        "filepath",
-        "range",
-        "contents"
+        "name",
+        "description",
+        "id"
       ]
     },
-    "HighlightedRangeContext": {
-      "title": "HighlightedRangeContext",
-      "description": "Context for a highlighted range",
+    "ContextItem": {
+      "title": "ContextItem",
+      "description": "A ContextItem is a single item that is stored in the ContextManager.",
       "type": "object",
       "properties": {
-        "range": {
-          "$ref": "#/definitions/RangeInFileWithContents"
+        "description": {
+          "$ref": "#/definitions/ContextItemDescription"
+        },
+        "content": {
+          "title": "Content",
+          "type": "string"
         },
         "editing": {
           "title": "Editing",
+          "default": false,
           "type": "boolean"
         },
-        "pinned": {
-          "title": "Pinned",
+        "editable": {
+          "title": "Editable",
+          "default": false,
           "type": "boolean"
-        },
-        "display_name": {
-          "title": "Display Name",
-          "type": "string"
-        }
-      },
-      "required": [
-        "range",
-        "editing",
-        "pinned",
-        "display_name"
-      ]
-    },
-    "SlashCommandDescription": {
-      "title": "SlashCommandDescription",
-      "type": "object",
-      "properties": {
-        "name": {
-          "title": "Name",
-          "type": "string"
-        },
-        "description": {
-          "title": "Description",
-          "type": "string"
         }
       },
       "required": [
-        "name",
-        "description"
+        "description",
+        "content"
       ]
     },
     "src__continuedev__core__main__FullState": {
@@ -284,13 +268,6 @@
           "title": "Default Model",
           "type": "string"
         },
-        "highlighted_ranges": {
-          "title": "Highlighted Ranges",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/HighlightedRangeContext"
-          }
-        },
         "slash_commands": {
           "title": "Slash Commands",
           "type": "array",
@@ -301,6 +278,13 @@
         "adding_highlighted_code": {
           "title": "Adding Highlighted Code",
           "type": "boolean"
+        },
+        "selected_context_items": {
+          "title": "Selected Context Items",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/ContextItem"
+          }
         }
       },
       "required": [
@@ -308,9 +292,9 @@
         "active",
         "user_input_queue",
         "default_model",
-        "highlighted_ranges",
         "slash_commands",
-        "adding_highlighted_code"
+        "adding_highlighted_code",
+        "selected_context_items"
       ]
     }
   }
-- 
cgit v1.2.3-70-g09d2

