summaryrefslogtreecommitdiff
path: root/extension
diff options
context:
space:
mode:
Diffstat (limited to 'extension')
-rw-r--r--extension/react-app/public/logos/anthropic.pngbin0 -> 13331 bytes
-rw-r--r--extension/react-app/public/logos/hf.pngbin0 -> 185450 bytes
-rw-r--r--extension/react-app/public/logos/llamacpp.pngbin0 -> 34267 bytes
-rw-r--r--extension/react-app/public/logos/lmstudio.pngbin0 -> 108401 bytes
-rw-r--r--extension/react-app/public/logos/meta.svg2
-rw-r--r--extension/react-app/public/logos/ollama.pngbin0 -> 10839 bytes
-rw-r--r--extension/react-app/public/logos/openai.svg2
-rw-r--r--extension/react-app/public/logos/replicate.pngbin0 -> 6143 bytes
-rw-r--r--extension/react-app/public/logos/together.pngbin0 -> 49102 bytes
-rw-r--r--extension/react-app/src/App.tsx15
-rw-r--r--extension/react-app/src/components/CheckDiv.tsx3
-rw-r--r--extension/react-app/src/components/ComboBox.tsx416
-rw-r--r--extension/react-app/src/components/ContinueButton.tsx33
-rw-r--r--extension/react-app/src/components/ErrorStepContainer.tsx52
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx14
-rw-r--r--extension/react-app/src/components/Layout.tsx61
-rw-r--r--extension/react-app/src/components/ModelCard.tsx122
-rw-r--r--extension/react-app/src/components/ModelSelect.tsx43
-rw-r--r--extension/react-app/src/components/ModelSettings.tsx2
-rw-r--r--extension/react-app/src/components/Onboarding.tsx130
-rw-r--r--extension/react-app/src/components/PillButton.tsx216
-rw-r--r--extension/react-app/src/components/ProgressBar.tsx9
-rw-r--r--extension/react-app/src/components/StepContainer.tsx241
-rw-r--r--extension/react-app/src/components/Suggestions.tsx228
-rw-r--r--extension/react-app/src/components/TimelineItem.tsx59
-rw-r--r--extension/react-app/src/components/UserInputContainer.tsx376
-rw-r--r--extension/react-app/src/components/dialogs/FTCDialog.tsx72
-rw-r--r--extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx129
-rw-r--r--extension/react-app/src/components/index.ts7
-rw-r--r--extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts4
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts17
-rw-r--r--extension/react-app/src/index.css6
-rw-r--r--extension/react-app/src/pages/gui.tsx438
-rw-r--r--extension/react-app/src/pages/help.tsx98
-rw-r--r--extension/react-app/src/pages/history.tsx172
-rw-r--r--extension/react-app/src/pages/models.tsx167
-rw-r--r--extension/react-app/src/pages/settings.tsx45
-rw-r--r--extension/react-app/src/redux/slices/serverStateReducer.ts84
-rw-r--r--extension/schema/ContinueConfig.d.ts161
-rw-r--r--extension/schema/FullState.d.ts17
-rw-r--r--extension/schema/History.d.ts2
-rw-r--r--extension/schema/HistoryNode.d.ts2
-rw-r--r--extension/schema/LLM.d.ts61
-rw-r--r--extension/schema/Models.d.ts63
-rw-r--r--extension/src/activation/activate.ts2
-rw-r--r--extension/src/commands.ts37
-rw-r--r--extension/src/continueIdeClient.ts78
-rw-r--r--extension/src/diffs.ts21
-rw-r--r--extension/src/util/util.ts2
49 files changed, 2565 insertions, 1144 deletions
diff --git a/extension/react-app/public/logos/anthropic.png b/extension/react-app/public/logos/anthropic.png
new file mode 100644
index 00000000..9adf1b71
--- /dev/null
+++ b/extension/react-app/public/logos/anthropic.png
Binary files differ
diff --git a/extension/react-app/public/logos/hf.png b/extension/react-app/public/logos/hf.png
new file mode 100644
index 00000000..49e2841d
--- /dev/null
+++ b/extension/react-app/public/logos/hf.png
Binary files differ
diff --git a/extension/react-app/public/logos/llamacpp.png b/extension/react-app/public/logos/llamacpp.png
new file mode 100644
index 00000000..119087b0
--- /dev/null
+++ b/extension/react-app/public/logos/llamacpp.png
Binary files differ
diff --git a/extension/react-app/public/logos/lmstudio.png b/extension/react-app/public/logos/lmstudio.png
new file mode 100644
index 00000000..b2b73591
--- /dev/null
+++ b/extension/react-app/public/logos/lmstudio.png
Binary files differ
diff --git a/extension/react-app/public/logos/meta.svg b/extension/react-app/public/logos/meta.svg
new file mode 100644
index 00000000..ba1d38d2
--- /dev/null
+++ b/extension/react-app/public/logos/meta.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#fff" width="800px" height="800px" viewBox="0 0 32 32" id="Camada_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M5,19.5c0-4.6,2.3-9.4,5-9.4c1.5,0,2.7,0.9,4.6,3.6c-1.8,2.8-2.9,4.5-2.9,4.5c-2.4,3.8-3.2,4.6-4.5,4.6 C5.9,22.9,5,21.7,5,19.5 M20.7,17.8L19,15c-0.4-0.7-0.9-1.4-1.3-2c1.5-2.3,2.7-3.5,4.2-3.5c3,0,5.4,4.5,5.4,10.1 c0,2.1-0.7,3.3-2.1,3.3S23.3,22,20.7,17.8 M16.4,11c-2.2-2.9-4.1-4-6.3-4C5.5,7,2,13.1,2,19.5c0,4,1.9,6.5,5.1,6.5 c2.3,0,3.9-1.1,6.9-6.3c0,0,1.2-2.2,2.1-3.7c0.3,0.5,0.6,1,0.9,1.6l1.4,2.4c2.7,4.6,4.2,6.1,6.9,6.1c3.1,0,4.8-2.6,4.8-6.7 C30,12.6,26.4,7,22.1,7C19.8,7,18,8.8,16.4,11"/></svg> \ No newline at end of file
diff --git a/extension/react-app/public/logos/ollama.png b/extension/react-app/public/logos/ollama.png
new file mode 100644
index 00000000..56ef23f4
--- /dev/null
+++ b/extension/react-app/public/logos/ollama.png
Binary files differ
diff --git a/extension/react-app/public/logos/openai.svg b/extension/react-app/public/logos/openai.svg
new file mode 100644
index 00000000..9aacd2a1
--- /dev/null
+++ b/extension/react-app/public/logos/openai.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
+<svg fill="#fff" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg> \ No newline at end of file
diff --git a/extension/react-app/public/logos/replicate.png b/extension/react-app/public/logos/replicate.png
new file mode 100644
index 00000000..f71504f5
--- /dev/null
+++ b/extension/react-app/public/logos/replicate.png
Binary files differ
diff --git a/extension/react-app/public/logos/together.png b/extension/react-app/public/logos/together.png
new file mode 100644
index 00000000..21295358
--- /dev/null
+++ b/extension/react-app/public/logos/together.png
Binary files differ
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx
index edcac4a0..bbb1a952 100644
--- a/extension/react-app/src/App.tsx
+++ b/extension/react-app/src/App.tsx
@@ -1,5 +1,6 @@
import GUI from "./pages/gui";
import History from "./pages/history";
+import Help from "./pages/help";
import Layout from "./components/Layout";
import { createContext, useEffect } from "react";
import useContinueGUIProtocol from "./hooks/useWebsocket";
@@ -18,6 +19,8 @@ import { postVscMessage } from "./vscode";
import { RouterProvider, createMemoryRouter } from "react-router-dom";
import ErrorPage from "./pages/error";
import SettingsPage from "./pages/settings";
+import Models from "./pages/models";
+import HelpPage from "./pages/help";
const router = createMemoryRouter([
{
@@ -38,9 +41,21 @@ const router = createMemoryRouter([
element: <History />,
},
{
+ path: "/help",
+ element: <Help />,
+ },
+ {
path: "/settings",
element: <SettingsPage />,
},
+ {
+ path: "/models",
+ element: <Models />,
+ },
+ {
+ path: "/help",
+ element: <HelpPage />,
+ },
],
},
]);
diff --git a/extension/react-app/src/components/CheckDiv.tsx b/extension/react-app/src/components/CheckDiv.tsx
index e595d70b..eaea0dc1 100644
--- a/extension/react-app/src/components/CheckDiv.tsx
+++ b/extension/react-app/src/components/CheckDiv.tsx
@@ -30,6 +30,9 @@ const StyledDiv = styled.div<{ checked: boolean }>`
margin: 0.5rem;
height: 1.4em;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
function CheckDiv(props: CheckDivProps) {
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 48df368b..e63499bc 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -3,12 +3,12 @@ import React, {
useContext,
useEffect,
useImperativeHandle,
+ useLayoutEffect,
useState,
} from "react";
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
- StyledTooltip,
buttonColor,
defaultBorderRadius,
lightGray,
@@ -19,53 +19,51 @@ import {
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
import {
- BookmarkIcon,
- DocumentPlusIcon,
- FolderArrowDownIcon,
ArrowLeftIcon,
- PlusIcon,
ArrowRightIcon,
+ MagnifyingGlassIcon,
+ TrashIcon,
} from "@heroicons/react/24/outline";
-import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
import { GUIClientContext } from "../App";
import { MeiliSearch } from "meilisearch";
-import {
- setBottomMessage,
- setDialogMessage,
- setShowDialog,
-} from "../redux/slices/uiStateSlice";
+import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
-import SelectContextGroupDialog from "./dialogs/SelectContextGroupDialog";
-import AddContextGroupDialog from "./dialogs/AddContextGroupDialog";
+import ContinueButton from "./ContinueButton";
const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
-const mainInputFontSize = 13;
-const EmptyPillDiv = styled.div`
- padding: 4px;
- padding-left: 8px;
- padding-right: 8px;
- border-radius: ${defaultBorderRadius};
- border: 1px dashed ${lightGray};
- color: ${lightGray};
- background-color: ${vscBackground};
- overflow: hidden;
+const HiddenHeaderButtonWithText = styled.button`
+ opacity: 0;
+ background-color: transparent;
+ border: none;
+ outline: none;
+ color: ${vscForeground};
+ cursor: pointer;
display: flex;
align-items: center;
- text-align: center;
- cursor: pointer;
- font-size: 13px;
+ justify-content: center;
+ height: 0;
+ aspect-ratio: 1;
+ padding: 0;
+ margin-left: -8px;
+
+ border-radius: ${defaultBorderRadius};
- &:hover {
- background-color: ${lightGray};
- color: ${vscBackground};
+ &:focus {
+ margin-left: 1px;
+ height: fit-content;
+ padding: 3px;
+ opacity: 1;
+ outline: 1px solid ${lightGray};
}
`;
+const mainInputFontSize = 13;
+
const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
resize: none;
@@ -79,20 +77,20 @@ const MainTextInput = styled.textarea<{ inQueryForDynamicProvider: boolean }>`
background-color: ${secondaryDark};
color: ${vscForeground};
z-index: 1;
- border: 1px solid
+ border: 0.5px solid
${(props) =>
props.inQueryForDynamicProvider ? buttonColor : "transparent"};
&:focus {
- outline: 1px solid
+ outline: 0.5px solid
${(props) => (props.inQueryForDynamicProvider ? buttonColor : lightGray)};
- border: 1px solid transparent;
+ border: 0.5px solid transparent;
background-color: ${(props) =>
props.inQueryForDynamicProvider ? `${buttonColor}22` : secondaryDark};
}
&::placeholder {
- color: ${lightGray}80;
+ color: ${lightGray}cc;
}
`;
@@ -110,23 +108,6 @@ const DynamicQueryTitleDiv = styled.div`
background-color: ${buttonColor};
`;
-const StyledPlusIcon = styled(PlusIcon)`
- position: absolute;
- right: 0px;
- top: 0px;
- height: fit-content;
- padding: 0;
- cursor: pointer;
- border-radius: ${defaultBorderRadius};
- z-index: 2;
-
- background-color: ${vscBackground};
-
- &:hover {
- background-color: ${secondaryDark};
- }
-`;
-
const UlMaxHeight = 300;
const Ul = styled.ul<{
hidden: boolean;
@@ -137,7 +118,7 @@ const Ul = styled.ul<{
${(props) =>
props.showAbove
? `transform: translateY(-${props.ulHeightPixels + 8}px);`
- : `transform: translateY(${5 * mainInputFontSize}px);`}
+ : `transform: translateY(${5 * mainInputFontSize - 2}px);`}
position: absolute;
background: ${vscBackground};
color: ${vscForeground};
@@ -148,7 +129,7 @@ const Ul = styled.ul<{
padding: 0;
${({ hidden }) => hidden && "display: none;"}
border-radius: ${defaultBorderRadius};
- outline: 1px solid ${lightGray};
+ outline: 0.5px solid ${lightGray};
z-index: 2;
-ms-overflow-style: none;
@@ -180,14 +161,17 @@ const Li = styled.li<{
// #endregion
+interface ComboBoxItem {
+ name: string;
+ description: string;
+ id?: string;
+ content?: string;
+}
interface ComboBoxProps {
- items: { name: string; description: string; id?: string; content?: string }[];
onInputValueChange: (inputValue: string) => void;
disabled?: boolean;
- onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
- selectedContextItems: ContextItem[];
+ onEnter: (e?: React.KeyboardEvent<HTMLInputElement>) => void;
onToggleAddContext: () => void;
- addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
@@ -197,14 +181,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const workspacePaths = useSelector(
(state: RootStore) => state.config.workspacePaths
);
- const savedContextGroups = useSelector(
- (state: RootStore) => state.serverState.saved_context_groups
- );
const [history, setHistory] = React.useState<string[]>([]);
// The position of the current command you are typing now, so the one that will be appended to history once you press enter
const [positionInHistory, setPositionInHistory] = React.useState<number>(0);
- const [items, setItems] = React.useState(props.items);
+ const [items, setItems] = React.useState<ComboBoxItem[]>([]);
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -217,6 +198,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
any | undefined
>(undefined);
+ const sessionId = useSelector(
+ (state: RootStore) => state.serverState.session_info?.session_id
+ );
+ const availableSlashCommands = useSelector(
+ (state: RootStore) => state.serverState.slash_commands
+ ).map((cmd) => {
+ return {
+ name: `/${cmd.name}`,
+ description: cmd.description,
+ };
+ });
+ const selectedContextItems = useSelector(
+ (state: RootStore) => state.serverState.selected_context_items
+ );
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [sessionId, inputRef.current]);
+
useEffect(() => {
if (!currentlyInContextQuery) {
setNestedContextProvider(undefined);
@@ -237,7 +239,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
useEffect(() => {
if (!nestedContextProvider) {
- console.log("setting items", nestedContextProvider);
setItems(
contextProviders?.map((provider) => ({
name: provider.display_title,
@@ -248,6 +249,8 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}, [nestedContextProvider]);
+ const [prevInputValue, setPrevInputValue] = useState("");
+
const onInputValueChangeCallback = useCallback(
({ inputValue, highlightedIndex }: any) => {
// Clear the input
@@ -257,6 +260,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setCurrentlyInContextQuery(false);
return;
}
+
+ // Hacky way of stopping bug where first context provider title is injected into input
+ if (
+ prevInputValue === "" &&
+ contextProviders.some((p) => p.display_title === inputValue)
+ ) {
+ downshiftProps.setInputValue("");
+ setPrevInputValue("");
+ return;
+ }
+ setPrevInputValue(inputValue);
+
if (
inQueryForContextProvider &&
!inputValue.startsWith(`@${inQueryForContextProvider.title}`)
@@ -277,9 +292,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (nestedContextProvider && !inputValue.endsWith("@")) {
// Search only within this specific context provider
+ const spaceSegs = providerAndQuery.split(" ");
getFilteredContextItemsForProvider(
nestedContextProvider.title,
- providerAndQuery
+ spaceSegs.length > 1 ? spaceSegs[1] : ""
).then((res) => {
setItems(res);
});
@@ -316,48 +332,19 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
// Handle slash commands
setItems(
- props.items?.filter((item) =>
- item.name.toLowerCase().startsWith(inputValue.toLowerCase())
+ availableSlashCommands?.filter((slashCommand) =>
+ slashCommand.name.toLowerCase().startsWith(inputValue.toLowerCase())
) || []
);
},
[
- props.items,
+ availableSlashCommands,
currentlyInContextQuery,
nestedContextProvider,
inQueryForContextProvider,
]
);
- const onSelectedItemChangeCallback = useCallback(
- ({ selectedItem }: any) => {
- if (!selectedItem) return;
- if (selectedItem.id) {
- // Get the query from the input value
- const segs = downshiftProps.inputValue.split("@");
- const query = segs[segs.length - 1];
-
- // Tell server the context item was selected
- client?.selectContextItem(selectedItem.id, query);
- if (downshiftProps.inputValue.includes("@")) {
- const selectedNestedContextProvider = contextProviders.find(
- (provider) => provider.title === selectedItem.id
- );
- if (
- !nestedContextProvider &&
- !selectedNestedContextProvider?.dynamic
- ) {
- downshiftProps.setInputValue(`@${selectedItem.id} `);
- setNestedContextProvider(selectedNestedContextProvider);
- } else {
- downshiftProps.setInputValue("");
- }
- }
- }
- },
- [nestedContextProvider, contextProviders, client]
- );
-
const getFilteredContextItemsForProvider = async (
provider: string,
query: string
@@ -390,7 +377,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
const { getInputProps, ...downshiftProps } = useCombobox({
- onSelectedItemChange: onSelectedItemChangeCallback,
onInputValueChange: onInputValueChangeCallback,
items,
itemToString(item) {
@@ -427,7 +413,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const focusedItemIndex = focusableItemsArray.findIndex(
(item) => item === document.activeElement
);
- console.log(focusedItemIndex, focusableItems);
if (focusedItemIndex === focusableItemsArray.length - 1) {
inputRef.current?.focus();
} else if (focusedItemIndex !== -1) {
@@ -457,6 +442,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}, []);
+ useLayoutEffect(() => {
+ if (!ulRef.current) {
+ return;
+ }
+ downshiftProps.setHighlightedIndex(0);
+ }, [items, downshiftProps.setHighlightedIndex, ulRef.current]);
+
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
const [focused, setFocused] = useState(false);
useEffect(() => {
@@ -476,7 +468,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
- });
+ }, []);
useEffect(() => {
if (!inputRef.current) {
@@ -489,7 +481,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
} else if (event.data.type === "focusContinueInputWithEdit") {
inputRef.current!.focus();
- downshiftProps.setInputValue("/edit ");
+ if (!inputRef.current?.value.startsWith("/edit")) {
+ downshiftProps.setInputValue("/edit ");
+ }
}
};
window.addEventListener("message", handler);
@@ -500,21 +494,69 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const selectContextItemFromDropdown = useCallback(
(event: any) => {
- const newProviderName = items[downshiftProps.highlightedIndex].name;
+ const newItem = items[downshiftProps.highlightedIndex];
+ const newProviderName = newItem?.name;
const newProvider = contextProviders.find(
(provider) => provider.display_title === newProviderName
);
if (!newProvider) {
+ if (nestedContextProvider && newItem.id) {
+ // Tell server the context item was selected
+ client?.selectContextItem(newItem.id, "");
+
+ // Clear the input
+ downshiftProps.setInputValue("");
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
+ return;
+ }
+ // This is a slash command
(event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
return;
} else if (newProvider.dynamic && newProvider.requires_query) {
+ // This is a dynamic context provider that requires a query, like URL / Search
setInQueryForContextProvider(newProvider);
downshiftProps.setInputValue(`@${newProvider.title} `);
(event.nativeEvent as any).preventDownshiftDefault = true;
event.preventDefault();
return;
} else if (newProvider.dynamic) {
+ // This is a normal dynamic context provider like Diff or Terminal
+ if (!newItem.id) return;
+
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+
+ // Tell server the context item was selected
+ client?.selectContextItem(newItem.id, query);
+ if (downshiftProps.inputValue.includes("@")) {
+ const selectedNestedContextProvider = contextProviders.find(
+ (provider) => provider.title === newItem.id
+ );
+ if (
+ !nestedContextProvider &&
+ !selectedNestedContextProvider?.dynamic
+ ) {
+ downshiftProps.setInputValue(`@${newItem.id} `);
+ setNestedContextProvider(selectedNestedContextProvider);
+ } else {
+ downshiftProps.setInputValue("");
+ }
+ }
+
+ // Clear the input
+ downshiftProps.setInputValue("");
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ setInQueryForContextProvider(undefined);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
return;
}
@@ -531,25 +573,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
downshiftProps.highlightedIndex,
contextProviders,
nestedContextProvider,
+ downshiftProps.inputValue,
]
);
- const showSelectContextGroupDialog = () => {
- dispatch(setDialogMessage(<SelectContextGroupDialog />));
- dispatch(setShowDialog(true));
- };
-
- const showDialogToSaveContextGroup = () => {
- dispatch(
- setDialogMessage(
- <AddContextGroupDialog
- selectedContextItems={props.selectedContextItems}
- />
- )
- );
- dispatch(setShowDialog(true));
- };
-
const [isComposing, setIsComposing] = useState(false);
return (
@@ -558,18 +585,36 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
className="px-2 flex gap-2 items-center flex-wrap mt-2"
ref={contextItemsDivRef}
>
- {props.selectedContextItems.map((item, idx) => {
+ <HiddenHeaderButtonWithText
+ className={selectedContextItems.length > 0 ? "pill-button" : ""}
+ onClick={() => {
+ client?.deleteContextWithIds(
+ selectedContextItems.map((item) => item.description.id)
+ );
+ inputRef.current?.focus();
+ }}
+ onKeyDown={(e: any) => {
+ if (e.key === "Backspace") {
+ client?.deleteContextWithIds(
+ selectedContextItems.map((item) => item.description.id)
+ );
+ inputRef.current?.focus();
+ }
+ }}
+ >
+ <TrashIcon width="1.4em" height="1.4em" />
+ </HiddenHeaderButtonWithText>
+ {selectedContextItems.map((item, idx) => {
return (
<PillButton
- areMultipleItems={props.selectedContextItems.length > 1}
+ areMultipleItems={selectedContextItems.length > 1}
key={`${item.description.id.item_id}${idx}`}
item={item}
- warning={
- item.content.length > 4000 && item.editing
- ? "Editing such a large range may be slow"
- : undefined
+ editing={
+ item.editing &&
+ (inputRef.current as any)?.value?.startsWith("/edit")
}
- addingHighlightedCode={props.addingHighlightedCode}
+ editingAny={(inputRef.current as any)?.value?.startsWith("/edit")}
index={idx}
onDelete={() => {
client?.deleteContextWithIds([item.description.id]);
@@ -578,64 +623,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
/>
);
})}
- <HeaderButtonWithText
- text="Load bookmarked context"
- onClick={() => {
- showSelectContextGroupDialog();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- showSelectContextGroupDialog();
- }
- }}
- >
- <FolderArrowDownIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- {props.selectedContextItems.length > 0 && (
- <>
- {props.addingHighlightedCode ? (
- <EmptyPillDiv
- onClick={() => {
- props.onToggleAddContext();
- }}
- >
- Highlight code section
- </EmptyPillDiv>
- ) : (
- <HeaderButtonWithText
- text="Add more code to context"
- onClick={() => {
- props.onToggleAddContext();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- props.onToggleAddContext();
- }
- }}
- >
- <DocumentPlusIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- )}
- <HeaderButtonWithText
- text="Bookmark context"
- onClick={() => {
- showDialogToSaveContextGroup();
- }}
- className="pill-button focus:outline-none focus:border-red-600 focus:border focus:border-solid"
- onKeyDown={(e: KeyboardEvent) => {
- e.preventDefault();
- if (e.key === "Enter") {
- showDialogToSaveContextGroup();
- }
- }}
- >
- <BookmarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </>
+
+ {selectedContextItems.length > 0 && (
+ <HeaderButtonWithText
+ onClick={() => {
+ client?.showContextVirtualFile();
+ }}
+ text="View Current Context"
+ >
+ <MagnifyingGlassIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
)}
</div>
<div
@@ -648,7 +645,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
typeof inQueryForContextProvider !== "undefined"
}
disabled={props.disabled}
- placeholder={`Ask a question, type '/' for slash commands, or '@' to add context`}
+ placeholder={`Ask a question, '/' for slash commands, '@' to add context`}
{...getInputProps({
onCompositionStart: () => setIsComposing(true),
onCompositionEnd: () => setIsComposing(false),
@@ -701,13 +698,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
}
setCurrentlyInContextQuery(false);
+ } else if (event.key === "Enter" && currentlyInContextQuery) {
+ // Handle "Enter" for Context Providers
+ selectContextItemFromDropdown(event);
} else if (
- event.key === "Enter" &&
- currentlyInContextQuery &&
- nestedContextProvider === undefined
+ event.key === "Tab" &&
+ downshiftProps.isOpen &&
+ items.length > 0 &&
+ items[downshiftProps.highlightedIndex]?.name.startsWith("/")
) {
- selectContextItemFromDropdown(event);
- } else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
} else if (event.key === "Tab") {
@@ -789,25 +788,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ref: inputRef,
})}
/>
- {inQueryForContextProvider ? (
+ {inQueryForContextProvider && (
<DynamicQueryTitleDiv>
Enter {inQueryForContextProvider.display_title} Query
</DynamicQueryTitleDiv>
- ) : (
- <>
- <StyledPlusIcon
- width="1.4em"
- height="1.4em"
- data-tooltip-id="add-context-button"
- onClick={() => {
- downshiftProps.setInputValue("@");
- inputRef.current?.focus();
- }}
- />
- <StyledTooltip id="add-context-button" place="bottom">
- Add Context to Prompt
- </StyledTooltip>
- </>
)}
<Ul
@@ -816,13 +800,17 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
})}
showAbove={showAbove()}
ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
- hidden={!downshiftProps.isOpen || items.length === 0}
+ hidden={
+ !downshiftProps.isOpen ||
+ items.length === 0 ||
+ inputRef.current?.value === ""
+ }
>
{nestedContextProvider && (
<div
style={{
backgroundColor: secondaryDark,
- borderBottom: `1px solid ${lightGray}`,
+ borderBottom: `0.5px solid ${lightGray}`,
display: "flex",
gap: "4px",
position: "sticky",
@@ -846,27 +834,27 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
items.map((item, index) => (
<Li
style={{
- borderTop: index === 0 ? "none" : undefined,
+ borderTop: index === 0 ? "none" : `0.5px solid ${lightGray}`,
}}
key={`${item.name}${index}`}
{...downshiftProps.getItemProps({ item, index })}
highlighted={downshiftProps.highlightedIndex === index}
selected={downshiftProps.selectedItem === item}
onClick={(e) => {
- // e.stopPropagation();
- // e.preventDefault();
- // (e.nativeEvent as any).preventDownshiftDefault = true;
- // downshiftProps.selectItem(item);
selectContextItemFromDropdown(e);
- onSelectedItemChangeCallback({ selectedItem: item });
+ e.stopPropagation();
+ e.preventDefault();
+ inputRef.current?.focus();
}}
>
- <span>
+ <span className="flex justify-between w-full">
{item.name}
{" "}
<span
style={{
color: lightGray,
+ float: "right",
+ textAlign: "right",
}}
>
{item.description}
@@ -888,7 +876,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
))}
</Ul>
</div>
- {props.selectedContextItems.length === 0 &&
+ {selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
(focused &&
metaKeyPressed &&
@@ -897,6 +885,10 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
Inserting at cursor
</div>
)}
+ <ContinueButton
+ disabled={!(inputRef.current as any)?.value}
+ onClick={() => props.onEnter(undefined)}
+ />
</>
);
});
diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx
index 10ecd94a..95dde177 100644
--- a/extension/react-app/src/components/ContinueButton.tsx
+++ b/extension/react-app/src/components/ContinueButton.tsx
@@ -1,26 +1,42 @@
-import styled, { keyframes } from "styled-components";
+import styled from "styled-components";
import { Button } from ".";
import { PlayIcon } from "@heroicons/react/24/outline";
import { useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useEffect, useState } from "react";
-let StyledButton = styled(Button)<{ color?: string | null }>`
+const StyledButton = styled(Button)<{
+ color?: string | null;
+ isDisabled: boolean;
+}>`
margin: auto;
margin-top: 8px;
margin-bottom: 16px;
display: grid;
grid-template-columns: 22px 1fr;
align-items: center;
- background: ${(props) => props.color || "#be1b55"};
+ background-color: ${(props) => props.color || "#be1b55"};
- &:hover {
- transition-property: "background";
- opacity: 0.7;
+ opacity: ${(props) => (props.isDisabled ? 0.5 : 1.0)};
+
+ cursor: ${(props) => (props.isDisabled ? "default" : "pointer")};
+
+ &:hover:enabled {
+ background-color: ${(props) => props.color || "#be1b55"};
+ ${(props) =>
+ props.isDisabled
+ ? "cursor: default;"
+ : `
+ opacity: 0.7;
+ `}
}
`;
-function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) {
+function ContinueButton(props: {
+ onClick?: () => void;
+ hidden?: boolean;
+ disabled: boolean;
+}) {
const vscMediaUrl = useSelector(
(state: RootStore) => state.config.vscMediaUrl
);
@@ -49,7 +65,8 @@ function ContinueButton(props: { onClick?: () => void; hidden?: boolean }) {
hidden={props.hidden}
style={{ fontSize: "10px" }}
className="m-auto press-start-2p"
- onClick={props.onClick}
+ onClick={props.disabled ? undefined : props.onClick}
+ isDisabled={props.disabled}
>
{vscMediaUrl ? (
<img src={`${vscMediaUrl}/play_button.png`} width="16px" />
diff --git a/extension/react-app/src/components/ErrorStepContainer.tsx b/extension/react-app/src/components/ErrorStepContainer.tsx
new file mode 100644
index 00000000..e8ab7950
--- /dev/null
+++ b/extension/react-app/src/components/ErrorStepContainer.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import styled from "styled-components";
+import { HistoryNode } from "../../../schema/HistoryNode";
+import { defaultBorderRadius, vscBackground } from ".";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+import {
+ MinusCircleIcon,
+ MinusIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+
+const Div = styled.div`
+ padding: 8px;
+ background-color: #ff000011;
+ border-radius: ${defaultBorderRadius};
+ border: 1px solid #cc0000;
+`;
+
+interface ErrorStepContainerProps {
+ historyNode: HistoryNode;
+ onClose: () => void;
+ onDelete: () => void;
+}
+
+function ErrorStepContainer(props: ErrorStepContainerProps) {
+ return (
+ <div style={{ backgroundColor: vscBackground, position: "relative" }}>
+ <div
+ style={{
+ position: "absolute",
+ right: "4px",
+ top: "4px",
+ display: "flex",
+ }}
+ >
+ <HeaderButtonWithText text="Collapse" onClick={() => props.onClose()}>
+ <MinusCircleIcon width="1.3em" height="1.3em" />
+ </HeaderButtonWithText>
+ <HeaderButtonWithText text="Collapse" onClick={() => props.onDelete()}>
+ <XMarkIcon width="1.3em" height="1.3em" />
+ </HeaderButtonWithText>
+ </div>
+ <Div>
+ <pre className="overflow-x-scroll">
+ {props.historyNode.observation?.error as string}
+ </pre>
+ </Div>
+ </div>
+ );
+}
+
+export default ErrorStepContainer;
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index 3122c287..ca359250 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { HeaderButton, StyledTooltip } from ".";
+import ReactDOM from "react-dom";
interface HeaderButtonWithTextProps {
text: string;
@@ -14,6 +15,9 @@ interface HeaderButtonWithTextProps {
const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
const [hover, setHover] = useState(false);
+
+ const tooltipPortalDiv = document.getElementById("tooltip-portal-div");
+
return (
<>
<HeaderButton
@@ -34,9 +38,13 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
>
{props.children}
</HeaderButton>
- <StyledTooltip id={`header_button_${props.text}`} place="bottom">
- {props.text}
- </StyledTooltip>
+ {tooltipPortalDiv &&
+ ReactDOM.createPortal(
+ <StyledTooltip id={`header_button_${props.text}`} place="bottom">
+ {props.text}
+ </StyledTooltip>,
+ tooltipPortalDiv
+ )}
</>
);
};
diff --git a/extension/react-app/src/components/Layout.tsx b/extension/react-app/src/components/Layout.tsx
index 6410db8a..9ec2e671 100644
--- a/extension/react-app/src/components/Layout.tsx
+++ b/extension/react-app/src/components/Layout.tsx
@@ -1,7 +1,6 @@
import styled from "styled-components";
import { defaultBorderRadius, secondaryDark, vscForeground } from ".";
import { Outlet } from "react-router-dom";
-import Onboarding from "./Onboarding";
import TextDialog from "./TextDialog";
import { useContext, useEffect, useState } from "react";
import { GUIClientContext } from "../App";
@@ -15,10 +14,9 @@ import {
import {
PlusIcon,
FolderIcon,
- BookOpenIcon,
- ChatBubbleOvalLeftEllipsisIcon,
SparklesIcon,
Cog6ToothIcon,
+ QuestionMarkCircleIcon,
} from "@heroicons/react/24/outline";
import HeaderButtonWithText from "./HeaderButtonWithText";
import { useNavigate, useLocation } from "react-router-dom";
@@ -62,6 +60,8 @@ const Footer = styled.footer`
align-items: center;
width: calc(100% - 16px);
height: ${FOOTER_HEIGHT};
+
+ overflow: hidden;
`;
const GridDiv = styled.div`
@@ -98,11 +98,20 @@ const Layout = () => {
(state: RootStore) => state.uiState.displayBottomMessageOnBottom
);
+ const timeline = useSelector(
+ (state: RootStore) => state.serverState.history.timeline
+ );
+
// #endregion
useEffect(() => {
const handleKeyDown = (event: any) => {
- if (event.metaKey && event.altKey && event.code === "KeyN") {
+ if (
+ event.metaKey &&
+ event.altKey &&
+ event.code === "KeyN" &&
+ timeline.filter((n) => !n.step.hide).length > 0
+ ) {
client?.loadSession(undefined);
}
if ((event.metaKey || event.ctrlKey) && event.code === "KeyC") {
@@ -121,7 +130,7 @@ const Layout = () => {
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
- }, [client]);
+ }, [client, timeline]);
return (
<LayoutTopDiv>
@@ -133,7 +142,6 @@ const Layout = () => {
gridTemplateRows: "1fr auto",
}}
>
- <Onboarding />
<TextDialog
showDialog={showDialog}
onEnter={() => {
@@ -176,54 +184,26 @@ const Layout = () => {
color="yellow"
/>
)}
-
<ModelSelect />
- {defaultModel === "MaybeProxyOpenAI" &&
+ {defaultModel === "OpenAIFreeTrial" &&
(location.pathname === "/settings" ||
- parseInt(localStorage.getItem("freeTrialCounter") || "0") >=
- 125) && (
+ parseInt(localStorage.getItem("ftc") || "0") >= 125) && (
<ProgressBar
- completed={parseInt(
- localStorage.getItem("freeTrialCounter") || "0"
- )}
+ completed={parseInt(localStorage.getItem("ftc") || "0")}
total={250}
/>
)}
</div>
<HeaderButtonWithText
+ text="Help"
onClick={() => {
- client?.loadSession(undefined);
+ navigate("/help");
}}
- text="New Session (⌥⌘N)"
>
- <PlusIcon width="1.4em" height="1.4em" />
+ <QuestionMarkCircleIcon width="1.4em" height="1.4em" />
</HeaderButtonWithText>
<HeaderButtonWithText
onClick={() => {
- navigate("/history");
- }}
- text="History"
- >
- <FolderIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- <a
- href="https://continue.dev/docs/how-to-use-continue"
- className="no-underline"
- >
- <HeaderButtonWithText text="Docs">
- <BookOpenIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </a>
- <a
- href="https://github.com/continuedev/continue/issues/new/choose"
- className="no-underline"
- >
- <HeaderButtonWithText text="Feedback">
- <ChatBubbleOvalLeftEllipsisIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- </a>
- <HeaderButtonWithText
- onClick={() => {
navigate("/settings");
}}
text="Settings"
@@ -248,6 +228,7 @@ const Layout = () => {
{bottomMessage}
</BottomMessageDiv>
</div>
+ <div id="tooltip-portal-div" />
</LayoutTopDiv>
);
};
diff --git a/extension/react-app/src/components/ModelCard.tsx b/extension/react-app/src/components/ModelCard.tsx
new file mode 100644
index 00000000..a537c5f4
--- /dev/null
+++ b/extension/react-app/src/components/ModelCard.tsx
@@ -0,0 +1,122 @@
+import React, { useContext } from "react";
+import styled from "styled-components";
+import { buttonColor, defaultBorderRadius, lightGray, vscForeground } from ".";
+import { setShowDialog } from "../redux/slices/uiStateSlice";
+import { GUIClientContext } from "../App";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { RootStore } from "../redux/store";
+import { BookOpenIcon } from "@heroicons/react/24/outline";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+import ReactDOM from "react-dom";
+
+export enum ModelTag {
+ "Requires API Key" = "Requires API Key",
+ "Local" = "Local",
+ "Free" = "Free",
+ "Open-Source" = "Open-Source",
+}
+
+const MODEL_TAG_COLORS: any = {};
+MODEL_TAG_COLORS[ModelTag["Requires API Key"]] = "#FF0000";
+MODEL_TAG_COLORS[ModelTag["Local"]] = "#00bb00";
+MODEL_TAG_COLORS[ModelTag["Open-Source"]] = "#0033FF";
+MODEL_TAG_COLORS[ModelTag["Free"]] = "#ffff00";
+
+export interface ModelInfo {
+ title: string;
+ class: string;
+ args: any;
+ description: string;
+ icon?: string;
+ tags?: ModelTag[];
+}
+
+const Div = styled.div<{ color: string }>`
+ border: 1px solid ${lightGray};
+ border-radius: ${defaultBorderRadius};
+ cursor: pointer;
+ padding: 4px 8px;
+ position: relative;
+ width: 100%;
+ transition: all 0.5s;
+
+ &:hover {
+ border: 1px solid ${(props) => props.color};
+ background-color: ${(props) => props.color}22;
+ }
+`;
+
+interface ModelCardProps {
+ modelInfo: ModelInfo;
+}
+
+function ModelCard(props: ModelCardProps) {
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const vscMediaUrl = useSelector(
+ (state: RootStore) => state.config.vscMediaUrl
+ );
+
+ return (
+ <Div
+ color={buttonColor}
+ onClick={(e) => {
+ if ((e.target as any).closest("a")) {
+ return;
+ }
+ client?.addModelForRole(
+ "*",
+ props.modelInfo.class,
+ props.modelInfo.args
+ );
+ dispatch(setShowDialog(false));
+ navigate("/");
+ }}
+ >
+ <div style={{ display: "flex", alignItems: "center" }}>
+ {vscMediaUrl && (
+ <img
+ src={`${vscMediaUrl}/logos/${props.modelInfo.icon}`}
+ height="24px"
+ style={{ marginRight: "10px" }}
+ />
+ )}
+ <h3>{props.modelInfo.title}</h3>
+ </div>
+ {props.modelInfo.tags?.map((tag) => {
+ return (
+ <span
+ style={{
+ backgroundColor: `${MODEL_TAG_COLORS[tag]}55`,
+ color: "white",
+ padding: "2px 4px",
+ borderRadius: defaultBorderRadius,
+ marginRight: "4px",
+ }}
+ >
+ {tag}
+ </span>
+ );
+ })}
+ <p>{props.modelInfo.description}</p>
+
+ <a
+ style={{
+ position: "absolute",
+ right: "8px",
+ top: "8px",
+ }}
+ href={`https://continue.dev/docs/reference/Models/${props.modelInfo.class.toLowerCase()}`}
+ target="_blank"
+ >
+ <HeaderButtonWithText text="Read the docs">
+ <BookOpenIcon width="1.6em" height="1.6em" />
+ </HeaderButtonWithText>
+ </a>
+ </Div>
+ );
+}
+
+export default ModelCard;
diff --git a/extension/react-app/src/components/ModelSelect.tsx b/extension/react-app/src/components/ModelSelect.tsx
index 0b1829f1..29d9250e 100644
--- a/extension/react-app/src/components/ModelSelect.tsx
+++ b/extension/react-app/src/components/ModelSelect.tsx
@@ -10,8 +10,9 @@ import { useContext } from "react";
import { GUIClientContext } from "../App";
import { RootStore } from "../redux/store";
import { useDispatch, useSelector } from "react-redux";
-import { PlusIcon } from "@heroicons/react/24/outline";
+import { ArrowLeftIcon, PlusIcon } from "@heroicons/react/24/outline";
import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice";
+import { useNavigate } from "react-router-dom";
const MODEL_INFO: { title: string; class: string; args: any }[] = [
{
@@ -83,7 +84,7 @@ const MODEL_INFO: { title: string; class: string; args: any }[] = [
},
{
title: "GPT-4 limited free trial",
- class: "MaybeProxyOpenAI",
+ class: "OpenAIFreeTrial",
args: {
model: "gpt-4",
},
@@ -159,10 +160,12 @@ function ModelSelect(props: {}) {
const defaultModel = useSelector(
(state: RootStore) => (state.serverState.config as any)?.models?.default
);
- const unusedModels = useSelector(
- (state: RootStore) => (state.serverState.config as any)?.models?.unused
+ const savedModels = useSelector(
+ (state: RootStore) => (state.serverState.config as any)?.models?.saved
);
+ const navigate = useNavigate();
+
return (
<GridDiv>
<Select
@@ -173,7 +176,7 @@ function ModelSelect(props: {}) {
defaultValue={0}
onChange={(e) => {
const value = JSON.parse(e.target.value);
- if (value.t === "unused") {
+ if (value.t === "saved") {
client?.setModelForRoleFromIndex("*", value.idx);
}
}}
@@ -188,11 +191,11 @@ function ModelSelect(props: {}) {
{modelSelectTitle(defaultModel)}
</option>
)}
- {unusedModels?.map((model: any, idx: number) => {
+ {savedModels?.map((model: any, idx: number) => {
return (
<option
value={JSON.stringify({
- t: "unused",
+ t: "saved",
idx,
})}
>
@@ -206,31 +209,7 @@ function ModelSelect(props: {}) {
width="1.3em"
height="1.3em"
onClick={() => {
- dispatch(
- setDialogMessage(
- <div>
- <div className="text-lg font-bold p-2">
- Setup a new model provider
- </div>
- <br />
- {MODEL_INFO.map((model, idx) => {
- return (
- <NewProviderDiv
- onClick={() => {
- const model = MODEL_INFO[idx];
- client?.addModelForRole("*", model.class, model.args);
- dispatch(setShowDialog(false));
- }}
- >
- {model.title}
- </NewProviderDiv>
- );
- })}
- <br />
- </div>
- )
- );
- dispatch(setShowDialog(true));
+ navigate("/models");
}}
/>
</GridDiv>
diff --git a/extension/react-app/src/components/ModelSettings.tsx b/extension/react-app/src/components/ModelSettings.tsx
index 99200502..06516687 100644
--- a/extension/react-app/src/components/ModelSettings.tsx
+++ b/extension/react-app/src/components/ModelSettings.tsx
@@ -27,7 +27,7 @@ const DefaultModelOptions: {
api_key: "",
model: "gpt-4",
},
- MaybeProxyOpenAI: {
+ OpenAIFreeTrial: {
api_key: "",
model: "gpt-4",
},
diff --git a/extension/react-app/src/components/Onboarding.tsx b/extension/react-app/src/components/Onboarding.tsx
deleted file mode 100644
index 588f7298..00000000
--- a/extension/react-app/src/components/Onboarding.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React, { useState, useEffect } from "react";
-import styled from "styled-components";
-import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
-import { defaultBorderRadius } from ".";
-import Loader from "./Loader";
-
-const StyledDiv = styled.div`
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: #1e1e1e;
- z-index: 200;
-
- color: white;
-`;
-
-const StyledSpan = styled.span`
- padding: 8px;
- border-radius: ${defaultBorderRadius};
- &:hover {
- background-color: #ffffff33;
- }
- white-space: nowrap;
-`;
-
-const Onboarding = () => {
- const [counter, setCounter] = useState(4);
- const gifs = ["intro", "highlight", "question", "help"];
- const topMessages = [
- "Welcome!",
- "Highlight code",
- "Ask a question",
- "Use /help to learn more",
- ];
-
- useEffect(() => {
- const hasVisited = localStorage.getItem("hasVisited");
- if (hasVisited) {
- setCounter(4);
- } else {
- setCounter(0);
- localStorage.setItem("hasVisited", "true");
- }
- }, []);
-
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- setLoading(true);
- }, [counter]);
-
- return (
- <StyledDiv hidden={counter >= 4}>
- <div
- style={{
- display: "grid",
- justifyContent: "center",
- alignItems: "center",
- height: "100%",
- textAlign: "center",
- paddingLeft: "16px",
- paddingRight: "16px",
- }}
- >
- <h1>{topMessages[counter]}</h1>
- <div style={{ display: "flex", justifyContent: "center" }}>
- {loading && (
- <div style={{ margin: "auto", position: "absolute", zIndex: 0 }}>
- <Loader />
- </div>
- )}
- {counter < 4 &&
- (counter % 2 === 0 ? (
- <img
- src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`}
- width="100%"
- key={"even-gif"}
- alt={topMessages[counter]}
- onLoad={() => {
- setLoading(false);
- }}
- style={{ zIndex: 1 }}
- />
- ) : (
- <img
- src={`https://github.com/continuedev/continue/blob/main/media/${gifs[counter]}.gif?raw=true`}
- width="100%"
- key={"odd-gif"}
- alt={topMessages[counter]}
- onLoad={() => {
- setLoading(false);
- }}
- style={{ zIndex: 1 }}
- />
- ))}
- </div>
- <p
- style={{
- paddingLeft: "50px",
- paddingRight: "50px",
- paddingBottom: "50px",
- textAlign: "center",
- cursor: "pointer",
- whiteSpace: "nowrap",
- }}
- >
- <StyledSpan
- hidden={counter === 0}
- onClick={() => setCounter((prev) => Math.max(prev - 1, 0))}
- >
- <ArrowLeftIcon width="18px" strokeWidth="2px" /> Previous
- </StyledSpan>
- <span hidden={counter === 0}>{" | "}</span>
- <StyledSpan onClick={() => setCounter((prev) => prev + 1)}>
- {counter === 0
- ? "Click to learn how to use Continue"
- : counter === 3
- ? "Get Started"
- : "Next"}{" "}
- <ArrowRightIcon width="18px" strokeWidth="2px" />
- </StyledSpan>
- </p>
- </div>
- </StyledDiv>
- );
-};
-
-export default Onboarding;
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 1ffdeeed..4b602619 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useRef, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
@@ -15,13 +15,8 @@ import {
} from "@heroicons/react/24/outline";
import { GUIClientContext } from "../App";
import { useDispatch } from "react-redux";
-import {
- setBottomMessage,
- setBottomMessageCloseTimeout,
-} from "../redux/slices/uiStateSlice";
+import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { ContextItem } from "../../../schema/FullState";
-import { ReactMarkdown } from "react-markdown/lib/react-markdown";
-import StyledMarkdownPreview from "./StyledMarkdownPreview";
const Button = styled.button`
border: none;
@@ -80,33 +75,27 @@ const CircleDiv = styled.div`
interface PillButtonProps {
onHover?: (arg0: boolean) => void;
item: ContextItem;
- warning?: string;
+ editing: boolean;
+ editingAny: boolean;
index: number;
- addingHighlightedCode?: boolean;
areMultipleItems?: boolean;
onDelete?: () => void;
}
interface StyledButtonProps {
- warning: string;
+ borderColor?: string;
editing?: boolean;
- areMultipleItems?: boolean;
}
const StyledButton = styled(Button)<StyledButtonProps>`
position: relative;
- border-color: ${(props) =>
- props.warning
- ? "red"
- : props.editing && props.areMultipleItems
- ? vscForeground
- : "transparent"};
+ border-color: ${(props) => props.borderColor || "transparent"};
border-width: 1px;
border-style: solid;
&:focus {
outline: none;
- border-color: ${vscForeground};
+ border-color: ${lightGray};
border-width: 1px;
border-style: solid;
}
@@ -116,82 +105,56 @@ const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
- const dispatch = useDispatch();
+ const [warning, setWarning] = useState<string | undefined>(undefined);
useEffect(() => {
- if (isHovered) {
- dispatch(setBottomMessageCloseTimeout(undefined));
- dispatch(
- setBottomMessage(
- <>
- <b>{props.item.description.name}</b>:{" "}
- {props.item.description.description}
- <pre>
- <code
- style={{
- fontSize: "12px",
- backgroundColor: "transparent",
- color: vscForeground,
- whiteSpace: "pre-wrap",
- wordWrap: "break-word",
- }}
- >
- {props.item.content}
- </code>
- </pre>
- </>
- )
- );
+ if (props.editing && props.item.content.length > 4000) {
+ setWarning("Editing such a large range may be slow");
} else {
- dispatch(
- setBottomMessageCloseTimeout(
- setTimeout(() => {
- if (!isHovered) {
- dispatch(setBottomMessage(undefined));
- }
- }, 2000)
- )
- );
+ setWarning(undefined);
}
- }, [isHovered]);
+ }, [props.editing, props.item]);
+
+ const dispatch = useDispatch();
return (
- <>
- <div style={{ position: "relative" }}>
- <StyledButton
- areMultipleItems={props.areMultipleItems}
- warning={props.warning || ""}
- editing={props.item.editing}
- onMouseEnter={() => {
- setIsHovered(true);
- if (props.onHover) {
- props.onHover(true);
- }
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- if (props.onHover) {
- props.onHover(false);
- }
- }}
- className="pill-button"
- onKeyDown={(e) => {
- if (e.key === "Backspace") {
- props.onDelete?.();
- }
- }}
- >
- {isHovered && (
- <GridDiv
- style={{
- gridTemplateColumns:
- props.item.editable && props.areMultipleItems
- ? "1fr 1fr"
- : "1fr",
- backgroundColor: vscBackground,
- }}
- >
- {props.item.editable && props.areMultipleItems && (
+ <div style={{ position: "relative" }}>
+ <StyledButton
+ borderColor={props.editing ? (warning ? "red" : undefined) : undefined}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ if (props.onHover) {
+ props.onHover(true);
+ }
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ if (props.onHover) {
+ props.onHover(false);
+ }
+ }}
+ className="pill-button"
+ onKeyDown={(e) => {
+ if (e.key === "Backspace") {
+ props.onDelete?.();
+ }
+ }}
+ >
+ {isHovered && (
+ <GridDiv
+ style={{
+ gridTemplateColumns:
+ props.item.editable &&
+ props.areMultipleItems &&
+ props.editingAny
+ ? "1fr 1fr"
+ : "1fr",
+ backgroundColor: vscBackground,
+ }}
+ >
+ {props.editingAny &&
+ props.item.editable &&
+ props.areMultipleItems && (
<ButtonDiv
data-tooltip-id={`edit-${props.index}`}
backgroundColor={"#8800aa55"}
@@ -205,30 +168,31 @@ const PillButton = (props: PillButtonProps) => {
</ButtonDiv>
)}
- <StyledTooltip id={`pin-${props.index}`}>
- Edit this range
- </StyledTooltip>
- <ButtonDiv
- data-tooltip-id={`delete-${props.index}`}
- backgroundColor={"#cc000055"}
- onClick={() => {
- client?.deleteContextWithIds([props.item.description.id]);
- dispatch(setBottomMessage(undefined));
- }}
- >
- <TrashIcon style={{ margin: "auto" }} width="1.6em" />
- </ButtonDiv>
- </GridDiv>
- )}
- {props.item.description.name}
- </StyledButton>
- <StyledTooltip id={`edit-${props.index}`}>
- {props.item.editing
- ? "Editing this section (with entire file as context)"
- : "Edit this section"}
- </StyledTooltip>
- <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
- {props.warning && (
+ <StyledTooltip id={`pin-${props.index}`}>
+ Edit this range
+ </StyledTooltip>
+ <ButtonDiv
+ data-tooltip-id={`delete-${props.index}`}
+ backgroundColor={"#cc000055"}
+ onClick={() => {
+ client?.deleteContextWithIds([props.item.description.id]);
+ dispatch(setBottomMessage(undefined));
+ }}
+ >
+ <TrashIcon style={{ margin: "auto" }} width="1.6em" />
+ </ButtonDiv>
+ </GridDiv>
+ )}
+ {props.item.description.name}
+ </StyledButton>
+ <StyledTooltip id={`edit-${props.index}`}>
+ {props.item.editing
+ ? "Editing this section (with entire file as context)"
+ : "Edit this section"}
+ </StyledTooltip>
+ <StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
+ {props.editing &&
+ (warning ? (
<>
<CircleDiv
data-tooltip-id={`circle-div-${props.item.description.name}`}
@@ -240,12 +204,32 @@ const PillButton = (props: PillButtonProps) => {
/>
</CircleDiv>
<StyledTooltip id={`circle-div-${props.item.description.name}`}>
- {props.warning}
+ {warning}
</StyledTooltip>
</>
- )}
- </div>
- </>
+ ) : (
+ <>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ style={{
+ backgroundColor: "#8800aa55",
+ border: `0.5px solid ${lightGray}`,
+ padding: "1px",
+ zIndex: 1,
+ }}
+ >
+ <PaintBrushIcon
+ style={{ margin: "auto" }}
+ width="1.0em"
+ strokeWidth={2}
+ />
+ </CircleDiv>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
+ Editing this range
+ </StyledTooltip>
+ </>
+ ))}
+ </div>
);
};
diff --git a/extension/react-app/src/components/ProgressBar.tsx b/extension/react-app/src/components/ProgressBar.tsx
index 4efee776..27972ffc 100644
--- a/extension/react-app/src/components/ProgressBar.tsx
+++ b/extension/react-app/src/components/ProgressBar.tsx
@@ -28,9 +28,12 @@ const GridDiv = styled.div`
const P = styled.p`
margin: 0;
margin-top: 2px;
- font-size: 12px;
+ font-size: 11.5px;
color: ${lightGray};
text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
interface ProgressBarProps {
@@ -45,7 +48,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => {
<>
<a
href="https://continue.dev/docs/customization/models"
- className="no-underline"
+ className="no-underline ml-2"
>
<GridDiv data-tooltip-id="usage_progress_bar">
<ProgressBarWrapper>
@@ -61,7 +64,7 @@ const ProgressBar = ({ completed, total }: ProgressBarProps) => {
/>
</ProgressBarWrapper>
<P>
- Free Usage: {completed} / {total}
+ Free Uses: {completed} / {total}
</P>
</GridDiv>
</a>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index a05aefb0..61529227 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -1,18 +1,9 @@
-import { useContext, useEffect, useRef, useState } from "react";
-import styled, { keyframes } from "styled-components";
-import { secondaryDark, vscBackground } from ".";
-import {
- ChevronDownIcon,
- ChevronRightIcon,
- ArrowPathIcon,
- XMarkIcon,
- MagnifyingGlassIcon,
- StopCircleIcon,
-} from "@heroicons/react/24/outline";
+import { useEffect, useRef, useState } from "react";
+import styled from "styled-components";
+import { defaultBorderRadius, secondaryDark, vscBackground } from ".";
+import { ArrowPathIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
-import { GUIClientContext } from "../App";
import StyledMarkdownPreview from "./StyledMarkdownPreview";
interface StepContainerProps {
@@ -23,11 +14,10 @@ interface StepContainerProps {
onRetry: () => void;
onDelete: () => void;
open: boolean;
- onToggleAll: () => void;
- onToggle: () => void;
isFirst: boolean;
isLast: boolean;
index: number;
+ noUserInputParent: boolean;
}
// #region styled components
@@ -35,74 +25,30 @@ interface StepContainerProps {
const MainDiv = styled.div<{
stepDepth: number;
inFuture: boolean;
-}>`
- opacity: ${(props) => (props.inFuture ? 0.3 : 1)};
- overflow: hidden;
- margin-left: 0px;
- margin-right: 0px;
-`;
+}>``;
-const HeaderDiv = styled.div<{ error: boolean; loading: boolean }>`
- background-color: ${(props) => (props.error ? "#522" : vscBackground)};
- display: grid;
- grid-template-columns: 1fr auto auto;
+const ButtonsDiv = styled.div`
+ display: flex;
+ gap: 2px;
align-items: center;
- padding-right: 8px;
-`;
+ background-color: ${vscBackground};
+ box-shadow: 1px 1px 10px ${vscBackground};
+ border-radius: ${defaultBorderRadius};
-const LeftHeaderSubDiv = styled.div`
- margin: 8px;
- display: grid;
- grid-template-columns: auto 1fr;
- align-items: center;
- grid-gap: 2px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 0;
`;
const ContentDiv = styled.div<{ isUserInput: boolean }>`
- padding-left: 4px;
- padding-right: 2px;
+ padding: 2px;
+ padding-right: 0px;
background-color: ${(props) =>
props.isUserInput ? secondaryDark : vscBackground};
font-size: 13px;
-`;
-
-const gradient = keyframes`
- 0% {
- background-position: 0px 0;
- }
- 100% {
- background-position: 100em 0;
- }
-`;
-
-const GradientBorder = styled.div<{
- borderWidth?: number;
- borderRadius?: string;
- borderColor?: string;
- isFirst: boolean;
- isLast: boolean;
- loading: boolean;
-}>`
- border-radius: ${(props) => props.borderRadius || "0"};
- padding-top: ${(props) =>
- `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`};
- padding-bottom: ${(props) =>
- `${(props.borderWidth || 1) / (props.isLast ? 1 : 2)}px`};
- background: ${(props) =>
- props.borderColor
- ? props.borderColor
- : `repeating-linear-gradient(
- 101.79deg,
- #1BBE84 0%,
- #331BBE 16%,
- #BE1B55 33%,
- #A6BE1B 55%,
- #BE1B55 67%,
- #331BBE 85%,
- #1BBE84 99%
- )`};
- animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite;
- background-size: 200% 200%;
+ border-radius: ${defaultBorderRadius};
+ overflow: hidden;
`;
// #endregion
@@ -112,7 +58,6 @@ function StepContainer(props: StepContainerProps) {
const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null);
const userInputRef = useRef<HTMLInputElement>(null);
const isUserInput = props.historyNode.step.name === "UserInputStep";
- const client = useContext(GUIClientContext);
useEffect(() => {
if (userInputRef?.current) {
@@ -139,91 +84,11 @@ function StepContainer(props: StepContainerProps) {
hidden={props.historyNode.step.hide as any}
>
<div>
- <GradientBorder
- loading={props.historyNode.active as boolean}
- isFirst={props.isFirst}
- isLast={props.isLast}
- borderColor={
- props.historyNode.observation?.error
- ? "#f005"
- : props.historyNode.active
- ? undefined
- : "transparent"
- }
- className="overflow-hidden cursor-pointer"
- onClick={(e) => {
- if (isMetaEquivalentKeyPressed(e)) {
- props.onToggleAll();
- } else {
- props.onToggle();
- }
- }}
- >
- <HeaderDiv
- loading={(props.historyNode.active as boolean) || false}
- error={props.historyNode.observation?.error ? true : false}
- >
- <LeftHeaderSubDiv
- style={
- props.historyNode.observation?.error ? { color: "white" } : {}
- }
- >
- {!isUserInput &&
- (props.open ? (
- <ChevronDownIcon width="1.4em" height="1.4em" />
- ) : (
- <ChevronRightIcon width="1.4em" height="1.4em" />
- ))}
- {props.historyNode.observation?.title ||
- (props.historyNode.step.name as any)}
- </LeftHeaderSubDiv>
- {/* <HeaderButton
- onClick={(e) => {
- e.stopPropagation();
- props.onReverse();
- }}
- >
- <Backward size="1.6em" onClick={props.onReverse}></Backward>
- </HeaderButton> */}
- {(isHovered || (props.historyNode.active as boolean)) && (
- <div className="flex gap-2 items-center">
- {(props.historyNode.logs as any)?.length > 0 && (
- <HeaderButtonWithText
- text="Logs"
- onClick={(e) => {
- e.stopPropagation();
- client?.showLogsAtIndex(props.index);
- }}
- >
- <MagnifyingGlassIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
- )}
- <HeaderButtonWithText
- onClick={(e) => {
- e.stopPropagation();
- props.onDelete();
- }}
- text={
- props.historyNode.active
- ? `Stop (${getMetaKeyLabel()}⌫)`
- : "Delete"
- }
- >
- {props.historyNode.active ? (
- <StopCircleIcon
- width="1.4em"
- height="1.4em"
- onClick={props.onDelete}
- />
- ) : (
- <XMarkIcon
- width="1.4em"
- height="1.4em"
- onClick={props.onDelete}
- />
- )}
- </HeaderButtonWithText>
- {props.historyNode.observation?.error ? (
+ {isHovered &&
+ (props.historyNode.observation?.error || props.noUserInputParent) && (
+ <ButtonsDiv>
+ {props.historyNode.observation?.error &&
+ ((
<HeaderButtonWithText
text="Retry"
onClick={(e) => {
@@ -237,39 +102,33 @@ function StepContainer(props: StepContainerProps) {
onClick={props.onRetry}
/>
</HeaderButtonWithText>
- ) : (
- <></>
- )}
- </div>
- )}
- </HeaderDiv>
- </GradientBorder>
- <ContentDiv hidden={!props.open} isUserInput={isUserInput}>
- {props.open && false && (
- <>
- <pre className="overflow-x-scroll">
- Step Details:
- <br />
- {JSON.stringify(props.historyNode.step, null, 2)}
- </pre>
- </>
- )}
+ ) as any)}
- {props.historyNode.observation?.error ? (
- <details>
- <summary>View Traceback</summary>
- <pre className="overflow-x-scroll">
- {props.historyNode.observation.error as string}
- </pre>
- </details>
- ) : (
- <StyledMarkdownPreview
- source={props.historyNode.step.description || ""}
- wrapperElement={{
- "data-color-mode": "dark",
- }}
- />
+ {props.noUserInputParent && (
+ <HeaderButtonWithText
+ text="Delete"
+ onClick={(e) => {
+ e.stopPropagation();
+ props.onDelete();
+ }}
+ >
+ <XMarkIcon
+ width="1.4em"
+ height="1.4em"
+ onClick={props.onRetry}
+ />
+ </HeaderButtonWithText>
+ )}
+ </ButtonsDiv>
)}
+
+ <ContentDiv hidden={!props.open} isUserInput={isUserInput}>
+ <StyledMarkdownPreview
+ source={props.historyNode.step.description || ""}
+ wrapperElement={{
+ "data-color-mode": "dark",
+ }}
+ />
</ContentDiv>
</div>
</MainDiv>
diff --git a/extension/react-app/src/components/Suggestions.tsx b/extension/react-app/src/components/Suggestions.tsx
new file mode 100644
index 00000000..1709288c
--- /dev/null
+++ b/extension/react-app/src/components/Suggestions.tsx
@@ -0,0 +1,228 @@
+import React, { useCallback, useEffect, useState } from "react";
+import styled from "styled-components";
+import {
+ StyledTooltip,
+ defaultBorderRadius,
+ lightGray,
+ secondaryDark,
+ vscForeground,
+} from ".";
+import {
+ PaperAirplaneIcon,
+ SparklesIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline";
+import { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import HeaderButtonWithText from "./HeaderButtonWithText";
+
+const Div = styled.div<{ isDisabled: boolean }>`
+ border-radius: ${defaultBorderRadius};
+ cursor: ${(props) => (props.isDisabled ? "not-allowed" : "pointer")};
+ padding: 8px 8px;
+ background-color: ${secondaryDark};
+ border: 1px solid transparent;
+
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ color: ${(props) => (props.isDisabled ? lightGray : vscForeground)};
+
+ &:hover {
+ border: ${(props) =>
+ props.isDisabled ? "1px solid transparent" : `1px solid ${lightGray}`};
+ }
+`;
+
+const P = styled.p`
+ font-size: 13px;
+ margin: 0;
+`;
+
+interface SuggestionsDivProps {
+ title: string;
+ description: string;
+ textInput: string;
+ onClick?: () => void;
+ disabled: boolean;
+}
+
+function SuggestionsDiv(props: SuggestionsDivProps) {
+ const [isHovered, setIsHovered] = useState(false);
+
+ return (
+ <>
+ <Div
+ data-tooltip-id={`suggestion-disabled-${props.textInput.replace(
+ " ",
+ ""
+ )}`}
+ onClick={props.onClick}
+ onMouseEnter={() => {
+ if (props.disabled) return;
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => setIsHovered(false)}
+ isDisabled={props.disabled}
+ >
+ <P>{props.description}</P>
+ <PaperAirplaneIcon
+ width="1.6em"
+ height="1.6em"
+ style={{
+ opacity: isHovered ? 1 : 0,
+ backgroundColor: secondaryDark,
+ boxShadow: `1px 1px 10px ${secondaryDark}`,
+ borderRadius: defaultBorderRadius,
+ }}
+ />
+ </Div>
+ <StyledTooltip
+ id={`suggestion-disabled-${props.textInput.replace(" ", "")}`}
+ place="bottom"
+ hidden={!props.disabled}
+ >
+ Must highlight code first
+ </StyledTooltip>
+ </>
+ );
+}
+
+const stageDescriptions = [
+ <p>Ask a question</p>,
+ <ol>
+ <li>Highlight code in the editor</li>
+ <li>Press cmd+M to select the code</li>
+ <li>Ask a question</li>
+ </ol>,
+ <ol>
+ <li>Highlight code in the editor</li>
+ <li>Press cmd+shift+M to select the code</li>
+ <li>Request and edit</li>
+ </ol>,
+];
+
+const suggestionsStages: any[][] = [
+ [
+ {
+ title: stageDescriptions[0],
+ description: "How does merge sort work?",
+ textInput: "How does merge sort work?",
+ },
+ {
+ title: stageDescriptions[0],
+ description: "How do I sum over a column in SQL?",
+ textInput: "How do I sum over a column in SQL?",
+ },
+ ],
+ [
+ {
+ title: stageDescriptions[1],
+ description: "Is there any way to make this code more efficient?",
+ textInput: "Is there any way to make this code more efficient?",
+ },
+ {
+ title: stageDescriptions[1],
+ description: "What does this function do?",
+ textInput: "What does this function do?",
+ },
+ ],
+ [
+ {
+ title: stageDescriptions[2],
+ description: "/edit write comments for this code",
+ textInput: "/edit write comments for this code",
+ },
+ {
+ title: stageDescriptions[2],
+ description: "/edit make this code more efficient",
+ textInput: "/edit make this code more efficient",
+ },
+ ],
+];
+
+const TutorialDiv = styled.div`
+ margin: 4px;
+ position: relative;
+ background-color: #ff02;
+ border-radius: ${defaultBorderRadius};
+ padding: 8px 4px;
+`;
+
+function SuggestionsArea(props: { onClick: (textInput: string) => void }) {
+ const [stage, setStage] = useState(
+ parseInt(localStorage.getItem("stage") || "0")
+ );
+ const timeline = useSelector(
+ (state: RootStore) => state.serverState.history.timeline
+ );
+ const sessionId = useSelector(
+ (state: RootStore) => state.serverState.session_info?.session_id
+ );
+ const codeIsHighlighted = useSelector((state: RootStore) =>
+ state.serverState.selected_context_items.some(
+ (item) => item.description.id.provider_title === "code"
+ )
+ );
+
+ const [hide, setHide] = useState(false);
+
+ useEffect(() => {
+ setHide(false);
+ }, [sessionId]);
+
+ const [numTutorialInputs, setNumTutorialInputs] = useState(0);
+
+ const inputsAreOnlyTutorial = useCallback(() => {
+ const inputs = timeline.filter(
+ (node) => !node.step.hide && node.step.name === "User Input"
+ );
+ return inputs.length - numTutorialInputs === 0;
+ }, [timeline, numTutorialInputs]);
+
+ return (
+ <>
+ {hide || stage > 2 || !inputsAreOnlyTutorial() || (
+ <TutorialDiv>
+ <div className="flex">
+ <SparklesIcon width="1.3em" height="1.3em" color="yellow" />
+ <b className="ml-1">Tutorial</b>
+ </div>
+ <p style={{ color: lightGray }}>
+ {stage < suggestionsStages.length &&
+ suggestionsStages[stage][0]?.title}
+ </p>
+ <HeaderButtonWithText
+ className="absolute right-1 top-1 cursor-pointer"
+ text="Close Tutorial"
+ onClick={() => {
+ console.log("HIDE");
+ setHide(true);
+ }}
+ >
+ <XMarkIcon width="1.2em" height="1.2em" />
+ </HeaderButtonWithText>
+ <div className="grid grid-cols-2 gap-2 mt-2">
+ {suggestionsStages[stage]?.map((suggestion) => (
+ <SuggestionsDiv
+ disabled={stage > 0 && !codeIsHighlighted}
+ {...suggestion}
+ onClick={() => {
+ if (stage > 0 && !codeIsHighlighted) return;
+ props.onClick(suggestion.textInput);
+ setStage(stage + 1);
+ localStorage.setItem("stage", (stage + 1).toString());
+ setHide(true);
+ setNumTutorialInputs((prev) => prev + 1);
+ }}
+ />
+ ))}
+ </div>
+ </TutorialDiv>
+ )}
+ </>
+ );
+}
+
+export default SuggestionsArea;
diff --git a/extension/react-app/src/components/TimelineItem.tsx b/extension/react-app/src/components/TimelineItem.tsx
new file mode 100644
index 00000000..78568890
--- /dev/null
+++ b/extension/react-app/src/components/TimelineItem.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import { lightGray, secondaryDark, vscBackground } from ".";
+import styled from "styled-components";
+import { ChatBubbleOvalLeftIcon, PlusIcon } from "@heroicons/react/24/outline";
+
+const CollapseButton = styled.div`
+ background-color: ${vscBackground};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+ flex-grow: 0;
+ margin-left: 5px;
+ cursor: pointer;
+`;
+
+const CollapsedDiv = styled.div`
+ margin-top: 8px;
+ margin-bottom: 8px;
+ margin-left: 8px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 13px;
+ min-height: 16px;
+`;
+
+interface TimelineItemProps {
+ historyNode: any;
+ open: boolean;
+ onToggle: () => void;
+ children: any;
+ iconElement?: any;
+}
+
+function TimelineItem(props: TimelineItemProps) {
+ return props.open ? (
+ props.children
+ ) : (
+ <CollapsedDiv>
+ <CollapseButton
+ onClick={() => {
+ props.onToggle();
+ }}
+ >
+ {props.iconElement || (
+ <ChatBubbleOvalLeftIcon width="16px" height="16px" />
+ )}
+ </CollapseButton>
+ <span style={{ color: lightGray }}>
+ {props.historyNode.observation?.error
+ ? props.historyNode.observation?.title
+ : props.historyNode.step.name}
+ </span>
+ </CollapsedDiv>
+ );
+}
+
+export default TimelineItem;
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 866fef58..76a3c615 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -1,5 +1,11 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
-import styled from "styled-components";
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import styled, { keyframes } from "styled-components";
import {
defaultBorderRadius,
lightGray,
@@ -8,69 +14,115 @@ import {
vscForeground,
} from ".";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { XMarkIcon, CheckIcon } from "@heroicons/react/24/outline";
+import {
+ XMarkIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronRightIcon,
+ MagnifyingGlassIcon,
+ StopCircleIcon,
+} from "@heroicons/react/24/outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import { GUIClientContext } from "../App";
+import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
+import { RootStore } from "../redux/store";
+import { useSelector } from "react-redux";
interface UserInputContainerProps {
onDelete: () => void;
children: string;
historyNode: HistoryNode;
index: number;
+ onToggle: (arg0: boolean) => void;
+ onToggleAll: (arg0: boolean) => void;
+ isToggleOpen: boolean;
+ active: boolean;
+ groupIndices: number[];
}
-const StyledDiv = styled.div`
- position: relative;
- background-color: ${secondaryDark};
- font-size: 13px;
+const gradient = keyframes`
+ 0% {
+ background-position: 0px 0;
+ }
+ 100% {
+ background-position: 100em 0;
+ }
+`;
+
+const ToggleDiv = styled.div`
display: flex;
align-items: center;
- border-bottom: 1px solid ${vscBackground};
- padding: 8px;
- padding-top: 0px;
- padding-bottom: 0px;
+ justify-content: center;
+ cursor: pointer;
- border-bottom: 0.5px solid ${lightGray};
- border-top: 0.5px solid ${lightGray};
-`;
+ height: 100%;
+ padding: 0 4px;
-const DeleteButtonDiv = styled.div`
- position: absolute;
- top: 8px;
- right: 8px;
+ &:hover {
+ background-color: ${vscBackground};
+ }
`;
-const StyledPre = styled.pre`
- margin-right: 22px;
- margin-left: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
- font-size: 13px;
+const GradientBorder = styled.div<{
+ borderWidth?: number;
+ borderRadius?: string;
+ borderColor?: string;
+ isFirst: boolean;
+ isLast: boolean;
+ loading: boolean;
+}>`
+ border-radius: ${(props) => props.borderRadius || "0"};
+ padding: ${(props) =>
+ `${(props.borderWidth || 1) / (props.isFirst ? 1 : 2)}px`};
+ background: ${(props) =>
+ props.borderColor
+ ? props.borderColor
+ : `repeating-linear-gradient(
+ 101.79deg,
+ #1BBE84 0%,
+ #331BBE 16%,
+ #BE1B55 33%,
+ #A6BE1B 55%,
+ #BE1B55 67%,
+ #331BBE 85%,
+ #1BBE84 99%
+ )`};
+ animation: ${(props) => (props.loading ? gradient : "")} 6s linear infinite;
+ background-size: 200% 200%;
`;
-const TextArea = styled.textarea`
- margin: 8px;
- margin-right: 22px;
- padding: 8px;
- white-space: pre-wrap;
- word-wrap: break-word;
- font-family: "Lexend", sans-serif;
+const StyledDiv = styled.div<{ editing: boolean }>`
font-size: 13px;
- width: 100%;
+ font-family: inherit;
border-radius: ${defaultBorderRadius};
- height: 100%;
- border: none;
- background-color: ${vscBackground};
- resize: none;
- outline: none;
- border: none;
+ height: auto;
+ background-color: ${secondaryDark};
color: ${vscForeground};
+ align-items: center;
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+ display: grid;
+ grid-template-columns: auto 1fr;
- &:focus {
- border: none;
- outline: none;
- }
+ outline: ${(props) => (props.editing ? `1px solid ${lightGray}` : "none")};
+ cursor: text;
+`;
+
+const DeleteButtonDiv = styled.div`
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background-color: ${secondaryDark};
+ box-shadow: 2px 2px 10px ${secondaryDark};
+ border-radius: ${defaultBorderRadius};
+`;
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: 8px;
+ align-items: center;
`;
function stringWithEllipsis(str: string, maxLen: number) {
@@ -84,108 +136,194 @@ const UserInputContainer = (props: UserInputContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
- const textAreaRef = useRef<HTMLTextAreaElement>(null);
+ const divRef = useRef<HTMLDivElement>(null);
const client = useContext(GUIClientContext);
+ const [prevContent, setPrevContent] = useState("");
+
+ const history = useSelector((state: RootStore) => state.serverState.history);
+
useEffect(() => {
- if (isEditing && textAreaRef.current) {
- textAreaRef.current.focus();
- // Select all text
- textAreaRef.current.setSelectionRange(
- 0,
- textAreaRef.current.value.length
- );
- // Change the size to match the contents (up to a max)
- textAreaRef.current.style.height = "auto";
- textAreaRef.current.style.height =
- (textAreaRef.current.scrollHeight > 500
- ? 500
- : textAreaRef.current.scrollHeight) + "px";
+ if (isEditing && divRef.current) {
+ setPrevContent(divRef.current.innerText);
+ divRef.current.focus();
+
+ if (divRef.current.innerText !== "") {
+ const range = document.createRange();
+ const sel = window.getSelection();
+ range.setStart(divRef.current, 0);
+ range.setEnd(divRef.current, 1);
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
}
- }, [isEditing]);
+ }, [isEditing, divRef.current]);
+
+ const onBlur = useCallback(() => {
+ setIsEditing(false);
+ if (divRef.current) {
+ divRef.current.innerText = prevContent;
+ divRef.current.blur();
+ }
+ }, [divRef.current]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
- setIsEditing(false);
+ onBlur();
}
};
- document.addEventListener("keydown", handleKeyDown);
+ divRef.current?.addEventListener("keydown", handleKeyDown);
return () => {
- document.removeEventListener("keydown", handleKeyDown);
+ divRef.current?.removeEventListener("keydown", handleKeyDown);
};
- }, []);
+ }, [prevContent, divRef.current, isEditing, onBlur]);
const doneEditing = (e: any) => {
- if (!textAreaRef.current?.value) {
+ if (!divRef.current?.innerText) {
return;
}
- client?.editStepAtIndex(textAreaRef.current.value, props.index);
+ setPrevContent(divRef.current.innerText);
+ client?.editStepAtIndex(divRef.current.innerText, props.index);
setIsEditing(false);
e.stopPropagation();
+ divRef.current?.blur();
};
return (
- <StyledDiv
- onMouseEnter={() => {
- setIsHovered(true);
- }}
- onMouseLeave={() => {
- setIsHovered(false);
- }}
+ <GradientBorder
+ loading={props.active}
+ isFirst={false}
+ isLast={false}
+ borderColor={props.active ? undefined : vscBackground}
+ borderRadius={defaultBorderRadius}
>
- {isEditing ? (
- <TextArea
- ref={textAreaRef}
- onKeyDown={(e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- doneEditing(e);
+ <StyledDiv
+ editing={isEditing}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ }}
+ onClick={() => {
+ setIsEditing(true);
+ }}
+ >
+ <GridDiv>
+ <ToggleDiv
+ onClick={
+ props.isToggleOpen
+ ? (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(false);
+ } else {
+ props.onToggle(false);
+ }
+ }
+ : (e) => {
+ e.stopPropagation();
+ if (isMetaEquivalentKeyPressed(e)) {
+ props.onToggleAll(true);
+ } else {
+ props.onToggle(true);
+ }
+ }
}
- }}
- defaultValue={props.children}
- onBlur={() => {
- setIsEditing(false);
- }}
- />
- ) : (
- <StyledPre
- onClick={() => {
- setIsEditing(true);
- }}
- className="mr-6 cursor-text w-full"
- >
- {stringWithEllipsis(props.children, 600)}
- </StyledPre>
- )}
- {/* <ReactMarkdown children={props.children} className="w-fit mr-10" /> */}
- <DeleteButtonDiv>
- {(isHovered || isEditing) && (
- <div className="flex">
- {isEditing ? (
- <HeaderButtonWithText
- onClick={(e) => {
- doneEditing(e);
- }}
- text="Done"
- >
- <CheckIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ >
+ {props.isToggleOpen ? (
+ <ChevronDownIcon width="1.4em" height="1.4em" />
) : (
- <HeaderButtonWithText
- onClick={(e) => {
- props.onDelete();
- e.stopPropagation();
- }}
- text="Delete"
- >
- <XMarkIcon width="1.4em" height="1.4em" />
- </HeaderButtonWithText>
+ <ChevronRightIcon width="1.4em" height="1.4em" />
)}
+ </ToggleDiv>
+ <div
+ style={{
+ padding: "8px",
+ paddingTop: "4px",
+ paddingBottom: "4px",
+ }}
+ >
+ <div
+ ref={divRef}
+ onBlur={() => {
+ onBlur();
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ doneEditing(e);
+ }
+ }}
+ contentEditable={true}
+ suppressContentEditableWarning={true}
+ className="mr-6 ml-1 cursor-text w-full py-2 flex items-center content-center outline-none"
+ >
+ {isEditing
+ ? props.children
+ : stringWithEllipsis(props.children, 600)}
+ </div>
+ <DeleteButtonDiv>
+ {(isHovered || isEditing) && (
+ <div className="flex">
+ {isEditing ? (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ doneEditing(e);
+ }}
+ text="Done"
+ >
+ <CheckIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ ) : (
+ <>
+ {history.timeline
+ .filter(
+ (h, i: number) =>
+ props.groupIndices.includes(i) && h.logs
+ )
+ .some((h) => h.logs!.length > 0) && (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ client?.showLogsAtIndex(props.groupIndices[1]);
+ }}
+ text="Context Used"
+ >
+ <MagnifyingGlassIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ if (props.active) {
+ client?.deleteAtIndex(props.groupIndices[1]);
+ } else {
+ props.onDelete();
+ }
+ }}
+ text={
+ props.active
+ ? `Stop (${getMetaKeyLabel()}⌫)`
+ : "Delete"
+ }
+ >
+ {props.active ? (
+ <StopCircleIcon width="1.4em" height="1.4em" />
+ ) : (
+ <XMarkIcon width="1.4em" height="1.4em" />
+ )}
+ </HeaderButtonWithText>
+ </>
+ )}
+ </div>
+ )}
+ </DeleteButtonDiv>
</div>
- )}
- </DeleteButtonDiv>
- </StyledDiv>
+ </GridDiv>
+ </StyledDiv>
+ </GradientBorder>
);
};
export default UserInputContainer;
diff --git a/extension/react-app/src/components/dialogs/FTCDialog.tsx b/extension/react-app/src/components/dialogs/FTCDialog.tsx
new file mode 100644
index 00000000..3ea753bc
--- /dev/null
+++ b/extension/react-app/src/components/dialogs/FTCDialog.tsx
@@ -0,0 +1,72 @@
+import React, { useContext } from "react";
+import styled from "styled-components";
+import { Button, TextInput } from "..";
+import { useNavigate } from "react-router-dom";
+import { GUIClientContext } from "../../App";
+import { useDispatch } from "react-redux";
+import { setShowDialog } from "../../redux/slices/uiStateSlice";
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 8px;
+ align-items: center;
+`;
+
+function FTCDialog() {
+ const navigate = useNavigate();
+ const [apiKey, setApiKey] = React.useState("");
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
+ return (
+ <div className="p-4">
+ <h3>Free Trial Limit Reached</h3>
+ <p>
+ You've reached the free trial limit of 250 free inputs with Continue's
+ OpenAI API key. To keep using Continue, you can either use your own API
+ key, or use a local LLM. To read more about the options, see our{" "}
+ <a
+ href="https://continue.dev/docs/customization/models"
+ target="_blank"
+ >
+ documentation
+ </a>
+ . If you're just looking for fastest way to keep going, type '/config'
+ to open your Continue config file and paste your API key into the
+ OpenAIFreeTrial object.
+ </p>
+
+ <TextInput
+ type="text"
+ placeholder="Enter your OpenAI API key"
+ value={apiKey}
+ onChange={(e) => setApiKey(e.target.value)}
+ />
+ <GridDiv>
+ <Button
+ onClick={() => {
+ navigate("/models");
+ }}
+ >
+ Select model
+ </Button>
+ <Button
+ disabled={!apiKey}
+ onClick={() => {
+ client?.addModelForRole("*", "OpenAI", {
+ model: "gpt-4",
+ api_key: apiKey,
+ title: "GPT-4",
+ });
+ dispatch(setShowDialog(false));
+ }}
+ >
+ Use my API key
+ </Button>
+ </GridDiv>
+ </div>
+ );
+}
+
+export default FTCDialog;
diff --git a/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx
new file mode 100644
index 00000000..2a7b735c
--- /dev/null
+++ b/extension/react-app/src/components/dialogs/KeyboardShortcuts.tsx
@@ -0,0 +1,129 @@
+import React from "react";
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ lightGray,
+ secondaryDark,
+ vscForeground,
+} from "..";
+import { getPlatform } from "../../util";
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-gap: 2rem;
+ padding: 1rem;
+ justify-items: center;
+ align-items: center;
+
+ border-top: 0.5px solid ${lightGray};
+`;
+
+const KeyDiv = styled.div`
+ border: 0.5px solid ${lightGray};
+ border-radius: ${defaultBorderRadius};
+ padding: 4px;
+ color: ${vscForeground};
+
+ width: 16px;
+ height: 16px;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+interface KeyboardShortcutProps {
+ mac: string;
+ windows: string;
+ description: string;
+}
+
+function KeyboardShortcut(props: KeyboardShortcutProps) {
+ const shortcut = getPlatform() === "windows" ? props.windows : props.mac;
+ return (
+ <div className="flex justify-between w-full items-center">
+ <span
+ style={{
+ color: vscForeground,
+ }}
+ >
+ {props.description}
+ </span>
+ <div className="flex gap-2 float-right">
+ {shortcut.split(" ").map((key) => {
+ return <KeyDiv>{key}</KeyDiv>;
+ })}
+ </div>
+ </div>
+ );
+}
+
+const shortcuts: KeyboardShortcutProps[] = [
+ {
+ mac: "⌘ M",
+ windows: "⌃ M",
+ description: "Ask about Highlighted Code",
+ },
+ {
+ mac: "⌘ ⇧ M",
+ windows: "⌃ ⇧ M",
+ description: "Edit Highlighted Code",
+ },
+ {
+ mac: "⌘ ⇧ ↵",
+ windows: "⌃ ⇧ ↵",
+ description: "Accept Diff",
+ },
+ {
+ mac: "⌘ ⇧ ⌫",
+ windows: "⌃ ⇧ ⌫",
+ description: "Reject Diff",
+ },
+ {
+ mac: "⌘ ⇧ L",
+ windows: "⌃ ⇧ L",
+ description: "Quick Text Entry",
+ },
+ {
+ mac: "⌥ ⌘ M",
+ windows: "⌥ ⌃ M",
+ description: "Toggle Auxiliary Bar",
+ },
+ {
+ mac: "⌘ ⇧ R",
+ windows: "⌃ ⇧ R",
+ description: "Debug Terminal",
+ },
+ {
+ mac: "⌥ ⌘ N",
+ windows: "⌥ ⌃ N",
+ description: "New Session",
+ },
+ {
+ mac: "⌘ ⌫",
+ windows: "⌃ ⌫",
+ description: "Stop Active Step",
+ },
+];
+
+function KeyboardShortcutsDialog() {
+ return (
+ <div className="p-2">
+ <h3 className="my-3 mx-auto text-center">Keyboard Shortcuts</h3>
+ <GridDiv>
+ {shortcuts.map((shortcut) => {
+ return (
+ <KeyboardShortcut
+ mac={shortcut.mac}
+ windows={shortcut.windows}
+ description={shortcut.description}
+ />
+ );
+ })}
+ </GridDiv>
+ </div>
+ );
+}
+
+export default KeyboardShortcutsDialog;
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 1f418c94..6f5a2f37 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -7,7 +7,7 @@ export const lightGray = "#646464";
// export const vscBackground = "rgb(30 30 30)";
export const vscBackgroundTransparent = "#1e1e1ede";
export const buttonColor = "#1bbe84";
-export const buttonColorHover = "1bbe84a8";
+export const buttonColorHover = "#1bbe84a8";
export const secondaryDark = "var(--vscode-list-hoverBackground)";
export const vscBackground = "var(--vscode-editor-background)";
@@ -17,7 +17,6 @@ export const Button = styled.button`
padding: 10px 12px;
margin: 8px 0;
border-radius: ${defaultBorderRadius};
- cursor: pointer;
border: none;
color: white;
@@ -28,7 +27,7 @@ export const Button = styled.button`
}
&:hover:enabled {
- background-color: ${buttonColorHover};
+ cursor: pointer;
}
`;
@@ -56,6 +55,8 @@ export const TextArea = styled.textarea`
z-index: 1;
border: 1px solid transparent;
+ resize: vertical;
+
&:focus {
outline: 1px solid ${lightGray};
border: 1px solid transparent;
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 9944f221..d71186d7 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -29,6 +29,8 @@ abstract class AbstractContinueGUIClientProtocol {
abstract showLogsAtIndex(index: number): void;
+ abstract showContextVirtualFile(): void;
+
abstract selectContextItem(id: string, query: string): void;
abstract loadSession(session_id?: string): void;
@@ -52,6 +54,8 @@ abstract class AbstractContinueGUIClientProtocol {
abstract selectContextGroup(id: string): void;
abstract deleteContextGroup(id: string): void;
+
+ abstract setCurrentSessionTitle(title: string): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index fe1b654b..8205a629 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -23,12 +23,8 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
? new VscodeMessenger(serverUrlWithSessionId)
: new WebsocketMessenger(serverUrlWithSessionId);
- this.messenger.onClose(() => {
- console.log("GUI -> IDE websocket closed");
- });
- this.messenger.onError((error) => {
- console.log("GUI -> IDE websocket error", error);
- });
+ this.messenger.onClose(() => {});
+ this.messenger.onError((error) => {});
this.messenger.onMessageType("reconnect_at_session", (data: any) => {
if (data.session_id) {
@@ -52,6 +48,7 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
}
onReconnectAtSession(session_id: string): void {
+ console.log("Reconnecting at session: ", session_id);
this.connectMessenger(
`${this.serverUrlWithSessionId.split("?")[0]}?session_id=${session_id}`,
this.useVscodeMessagePassing
@@ -122,6 +119,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger?.send("show_logs_at_index", { index });
}
+ showContextVirtualFile(): void {
+ this.messenger?.send("show_context_virtual_file", {});
+ }
+
selectContextItem(id: string, query: string): void {
this.messenger?.send("select_context_item", { id, query });
}
@@ -163,6 +164,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
deleteContextGroup(id: string): void {
this.messenger?.send("delete_context_group", { id });
}
+
+ setCurrentSessionTitle(title: string): void {
+ this.messenger?.send("set_current_session_title", { title });
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
index 269da69a..3ecef025 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -11,7 +11,7 @@
--vscode-editor-background: rgb(30, 30, 30);
--vscode-editor-foreground: rgb(197, 200, 198);
- --vscode-textBlockQuote-background: rgba(255, 255, 255, 0.05);
+ --vscode-textBlockQuote-background: rgba(255, 255, 255, 1);
}
html,
@@ -33,3 +33,7 @@ body {
.press-start-2p {
font-family: "Press Start 2P", "Lexend", sans-serif;
}
+
+a:focus {
+ outline: none;
+} \ No newline at end of file
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 9f58c505..78b7a970 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -1,7 +1,5 @@
import styled from "styled-components";
-import { defaultBorderRadius } from "../components";
-import Loader from "../components/Loader";
-import ContinueButton from "../components/ContinueButton";
+import { TextInput, defaultBorderRadius, lightGray } from "../components";
import { FullState } from "../../../schema/FullState";
import {
useEffect,
@@ -9,6 +7,7 @@ import {
useState,
useContext,
useLayoutEffect,
+ useCallback,
} from "react";
import { HistoryNode } from "../../../schema/HistoryNode";
import StepContainer from "../components/StepContainer";
@@ -32,6 +31,19 @@ import {
setServerState,
temporarilyPushToUserInputQueue,
} from "../redux/slices/serverStateReducer";
+import TimelineItem from "../components/TimelineItem";
+import ErrorStepContainer from "../components/ErrorStepContainer";
+import {
+ ChatBubbleOvalLeftIcon,
+ CodeBracketSquareIcon,
+ ExclamationTriangleIcon,
+ FolderIcon,
+ PlusIcon,
+} from "@heroicons/react/24/outline";
+import FTCDialog from "../components/dialogs/FTCDialog";
+import HeaderButtonWithText from "../components/HeaderButtonWithText";
+import { useNavigate } from "react-router-dom";
+import SuggestionsArea from "../components/Suggestions";
const TopGuiDiv = styled.div`
overflow-y: scroll;
@@ -44,6 +56,44 @@ const TopGuiDiv = styled.div`
}
`;
+const TitleTextInput = styled(TextInput)`
+ border: none;
+ outline: none;
+
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0;
+ margin-right: 8px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+
+ &:focus {
+ outline: 1px solid ${lightGray};
+ }
+`;
+
+const StepsDiv = styled.div`
+ position: relative;
+ background-color: transparent;
+ padding-left: 8px;
+ padding-right: 8px;
+
+ & > * {
+ z-index: 1;
+ position: relative;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: calc(100% - 24px);
+ border-left: 2px solid ${lightGray};
+ left: 28px;
+ z-index: 0;
+ bottom: 24px;
+ }
+`;
+
const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
color: gray;
@@ -52,6 +102,16 @@ const UserInputQueueItem = styled.div`
text-align: center;
`;
+const GUIHeaderDiv = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ border-bottom: 0.5px solid ${lightGray};
+`;
+
interface GUIProps {
firstObservation?: any;
}
@@ -61,6 +121,7 @@ function GUI(props: GUIProps) {
const client = useContext(GUIClientContext);
const posthog = usePostHog();
const dispatch = useDispatch();
+ const navigate = useNavigate();
// #endregion
@@ -73,26 +134,16 @@ function GUI(props: GUIProps) {
const user_input_queue = useSelector(
(state: RootStore) => state.serverState.user_input_queue
);
- const adding_highlighted_code = useSelector(
- (state: RootStore) => state.serverState.adding_highlighted_code
- );
- const selected_context_items = useSelector(
- (state: RootStore) => state.serverState.selected_context_items
+
+ const sessionTitle = useSelector(
+ (state: RootStore) => state.serverState.session_info?.title
);
// #endregion
// #region State
const [waitingForSteps, setWaitingForSteps] = useState(false);
- const [availableSlashCommands, setAvailableSlashCommands] = useState<
- { name: string; description: string }[]
- >([]);
- const [stepsOpen, setStepsOpen] = useState<boolean[]>([
- true,
- true,
- true,
- true,
- ]);
+ const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]);
const [waitingForClient, setWaitingForClient] = useState(true);
const [showLoading, setShowLoading] = useState(false);
@@ -150,7 +201,7 @@ function GUI(props: GUIProps) {
topGuiDivRef.current?.scrollTo({
top: topGuiDivRef.current?.scrollHeight,
- behavior: "smooth" as any,
+ behavior: "instant" as any,
});
}, [topGuiDivRef.current?.scrollHeight, history.timeline]);
@@ -160,6 +211,7 @@ function GUI(props: GUIProps) {
if (
e.key === "Backspace" &&
isMetaEquivalentKeyPressed(e) &&
+ !e.shiftKey &&
typeof history?.current_index !== "undefined" &&
history.timeline[history.current_index]?.active
) {
@@ -188,14 +240,6 @@ function GUI(props: GUIProps) {
dispatch(setServerState(state));
setWaitingForSteps(waitingForSteps);
- setAvailableSlashCommands(
- state.slash_commands.map((c: any) => {
- return {
- name: `/${c.name}`,
- description: c.description,
- };
- })
- );
setStepsOpen((prev) => {
const nextStepsOpen = [...prev];
for (
@@ -203,7 +247,7 @@ function GUI(props: GUIProps) {
i < state.history.timeline.length;
i++
) {
- nextStepsOpen.push(true);
+ nextStepsOpen.push(undefined);
}
return nextStepsOpen;
});
@@ -214,7 +258,6 @@ function GUI(props: GUIProps) {
useEffect(() => {
if (client && waitingForClient) {
- console.log("sending user input queue, ", user_input_queue);
setWaitingForClient(false);
for (const input of user_input_queue) {
client.sendMainInput(input);
@@ -244,43 +287,22 @@ function GUI(props: GUIProps) {
return;
}
- // Increment localstorage counter for usage of free trial
if (
- defaultModel === "MaybeProxyOpenAI" &&
+ defaultModel === "OpenAIFreeTrial" &&
(!input.startsWith("/") || input.startsWith("/edit"))
) {
- const freeTrialCounter = localStorage.getItem("freeTrialCounter");
- if (freeTrialCounter) {
- const usages = parseInt(freeTrialCounter);
- localStorage.setItem("freeTrialCounter", (usages + 1).toString());
+ const ftc = localStorage.getItem("ftc");
+ if (ftc) {
+ const u = parseInt(ftc);
+ localStorage.setItem("ftc", (u + 1).toString());
- if (usages >= 250) {
- console.log("Free trial limit reached");
+ if (u >= 250) {
dispatch(setShowDialog(true));
- dispatch(
- setDialogMessage(
- <div className="p-4">
- <h3>Free Trial Limit Reached</h3>
- You've reached the free trial limit of 250 free inputs with
- Continue's OpenAI API key. To keep using Continue, you can
- either use your own API key, or use a local LLM. To read more
- about the options, see our{" "}
- <a
- href="https://continue.dev/docs/customization/models"
- target="_blank"
- >
- documentation
- </a>
- . If you're just looking for fastest way to keep going, type
- '/config' to open your Continue config file and paste your API
- key into the MaybeProxyOpenAI object.
- </div>
- )
- );
+ dispatch(setDialogMessage(<FTCDialog />));
return;
}
} else {
- localStorage.setItem("freeTrialCounter", "1");
+ localStorage.setItem("ftc", "1");
}
}
@@ -391,6 +413,69 @@ function GUI(props: GUIProps) {
client.sendStepUserInput(input, index);
};
+ const getStepsInUserInputGroup = useCallback(
+ (index: number): number[] => {
+ // index is the index in the entire timeline, hidden steps included
+ const stepsInUserInputGroup: number[] = [];
+
+ // First find the closest above UserInputStep
+ let userInputIndex = -1;
+ for (let i = index; i >= 0; i--) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ stepsInUserInputGroup.push(i);
+ userInputIndex = i;
+ break;
+ }
+ }
+ if (stepsInUserInputGroup.length === 0) return [];
+
+ for (let i = userInputIndex + 1; i < history?.timeline.length; i++) {
+ if (
+ history?.timeline.length > i &&
+ history.timeline[i].step.name === "User Input" &&
+ history.timeline[i].step.hide === false
+ ) {
+ break;
+ }
+ stepsInUserInputGroup.push(i);
+ }
+ return stepsInUserInputGroup;
+ },
+ [history.timeline]
+ );
+
+ const onToggleAtIndex = useCallback(
+ (index: number) => {
+ // Check if all steps after the User Input are closed
+ const groupIndices = getStepsInUserInputGroup(index);
+ const userInputIndex = groupIndices[0];
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ nextStepsOpen[index] = !nextStepsOpen[index];
+ const allStepsAfterUserInputAreClosed = !groupIndices.some(
+ (i, j) => j > 0 && nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreClosed) {
+ nextStepsOpen[userInputIndex] = false;
+ } else {
+ const allStepsAfterUserInputAreOpen = !groupIndices.some(
+ (i, j) => j > 0 && !nextStepsOpen[i]
+ );
+ if (allStepsAfterUserInputAreOpen) {
+ nextStepsOpen[userInputIndex] = true;
+ }
+ }
+
+ return nextStepsOpen;
+ });
+ },
+ [getStepsInUserInputGroup]
+ );
+
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoading(true);
@@ -400,6 +485,17 @@ function GUI(props: GUIProps) {
clearTimeout(timeout);
};
}, []);
+
+ useEffect(() => {
+ if (sessionTitle) {
+ setSessionTitleInput(sessionTitle);
+ }
+ }, [sessionTitle]);
+
+ const [sessionTitleInput, setSessionTitleInput] = useState<string>(
+ sessionTitle || "New Session"
+ );
+
return (
<TopGuiDiv
ref={topGuiDivRef}
@@ -409,6 +505,51 @@ function GUI(props: GUIProps) {
}
}}
>
+ <GUIHeaderDiv>
+ <TitleTextInput
+ onClick={(e) => {
+ // Select all text
+ (e.target as any).setSelectionRange(
+ 0,
+ (e.target as any).value.length
+ );
+ }}
+ value={sessionTitleInput}
+ onChange={(e) => setSessionTitleInput(e.target.value)}
+ onBlur={(e) => {
+ client?.setCurrentSessionTitle(e.target.value);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ (e.target as any).blur();
+ }
+ }}
+ />
+ <div className="flex">
+ {history.timeline.filter((n) => !n.step.hide).length > 0 && (
+ <HeaderButtonWithText
+ onClick={() => {
+ if (history.timeline.filter((n) => !n.step.hide).length > 0) {
+ client?.loadSession(undefined);
+ }
+ }}
+ text="New Session (⌥⌘N)"
+ >
+ <PlusIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ )}
+
+ <HeaderButtonWithText
+ onClick={() => {
+ navigate("/history");
+ }}
+ text="History"
+ >
+ <FolderIcon width="1.4em" height="1.4em" />
+ </HeaderButtonWithText>
+ </div>
+ </GUIHeaderDiv>
{showLoading && typeof client === "undefined" && (
<>
<RingLoader />
@@ -478,63 +619,128 @@ function GUI(props: GUIProps) {
</u>
</p>
</div>
-
- <div className="w-3/4 m-auto text-center text-xs">
- {/* Tip: Drag the Continue logo from the far left of the window to the
- right, then toggle Continue using option/alt+command+m. */}
- {/* Tip: If there is an error in the terminal, use COMMAND+D to
- automatically debug */}
- </div>
</>
)}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return node.step.name === "User Input" ? (
- node.step.hide || (
- <UserInputContainer
- index={index}
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- historyNode={node}
- >
- {node.step.description as string}
- </UserInputContainer>
- )
- ) : (
- <StepContainer
- index={index}
- isLast={index === history.timeline.length - 1}
- isFirst={index === 0}
- open={stepsOpen[index]}
- onToggle={() => {
- const nextStepsOpen = [...stepsOpen];
- nextStepsOpen[index] = !nextStepsOpen[index];
- setStepsOpen(nextStepsOpen);
- }}
- onToggleAll={() => {
- const shouldOpen = !stepsOpen[index];
- setStepsOpen((prev) => prev.map(() => shouldOpen));
- }}
- key={index}
- onUserInput={(input: string) => {
- onStepUserInput(input, index);
- }}
- inFuture={index > history?.current_index}
- historyNode={node}
- onReverse={() => {
- client?.reverseToIndex(index);
- }}
- onRetry={() => {
- client?.retryAtIndex(index);
- setWaitingForSteps(true);
- }}
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- />
- );
- })}
- {waitingForSteps && <Loader />}
+ <br />
+ <SuggestionsArea
+ onClick={(textInput) => {
+ client?.sendMainInput(textInput);
+ }}
+ />
+ <StepsDiv>
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ if (node.step.hide) return null;
+ return (
+ <>
+ {node.step.name === "User Input" ? (
+ node.step.hide || (
+ <UserInputContainer
+ active={getStepsInUserInputGroup(index).some((i) => {
+ return history.timeline[i].active;
+ })}
+ groupIndices={getStepsInUserInputGroup(index)}
+ onToggle={(isOpen: boolean) => {
+ // Collapse all steps in the section
+ setStepsOpen((prev) => {
+ const nextStepsOpen = [...prev];
+ getStepsInUserInputGroup(index).forEach((i) => {
+ nextStepsOpen[i] = isOpen;
+ });
+ return nextStepsOpen;
+ });
+ }}
+ onToggleAll={(isOpen: boolean) => {
+ // Collapse _all_ steps
+ setStepsOpen((prev) => {
+ return prev.map((_) => isOpen);
+ });
+ }}
+ isToggleOpen={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ index={index}
+ onDelete={() => {
+ // Delete the input and all steps until the next user input
+ getStepsInUserInputGroup(index).forEach((i) => {
+ client?.deleteAtIndex(i);
+ });
+ }}
+ historyNode={node}
+ >
+ {node.step.description as string}
+ </UserInputContainer>
+ )
+ ) : (
+ <TimelineItem
+ historyNode={node}
+ iconElement={
+ node.step.class_name === "DefaultModelEditCodeStep" ? (
+ <CodeBracketSquareIcon width="16px" height="16px" />
+ ) : node.observation?.error ? (
+ <ExclamationTriangleIcon
+ width="16px"
+ height="16px"
+ color="red"
+ />
+ ) : (
+ <ChatBubbleOvalLeftIcon width="16px" height="16px" />
+ )
+ }
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? node.observation?.error
+ ? false
+ : true
+ : stepsOpen[index]!
+ }
+ onToggle={() => onToggleAtIndex(index)}
+ >
+ {node.observation?.error ? (
+ <ErrorStepContainer
+ onClose={() => onToggleAtIndex(index)}
+ historyNode={node}
+ onDelete={() => client?.deleteAtIndex(index)}
+ />
+ ) : (
+ <StepContainer
+ index={index}
+ isLast={index === history.timeline.length - 1}
+ isFirst={index === 0}
+ open={
+ typeof stepsOpen[index] === "undefined"
+ ? true
+ : stepsOpen[index]!
+ }
+ key={index}
+ onUserInput={(input: string) => {
+ onStepUserInput(input, index);
+ }}
+ inFuture={index > history?.current_index}
+ historyNode={node}
+ onReverse={() => {
+ client?.reverseToIndex(index);
+ }}
+ onRetry={() => {
+ client?.retryAtIndex(index);
+ setWaitingForSteps(true);
+ }}
+ onDelete={() => {
+ client?.deleteAtIndex(index);
+ }}
+ noUserInputParent={
+ getStepsInUserInputGroup(index).length === 0
+ }
+ />
+ )}
+ </TimelineItem>
+ )}
+ {/* <div className="h-2"></div> */}
+ </>
+ );
+ })}
+ </StepsDiv>
<div>
{user_input_queue?.map?.((input) => {
@@ -547,18 +753,14 @@ function GUI(props: GUIProps) {
ref={mainTextInputRef}
onEnter={(e) => {
onMainTextInput(e);
- e.stopPropagation();
- e.preventDefault();
+ e?.stopPropagation();
+ e?.preventDefault();
}}
onInputValueChange={() => {}}
- items={availableSlashCommands}
- selectedContextItems={selected_context_items}
onToggleAddContext={() => {
client?.toggleAddingHighlightedCode();
}}
- addingHighlightedCode={adding_highlighted_code}
/>
- <ContinueButton onClick={onMainTextInput} />
</TopGuiDiv>
);
}
diff --git a/extension/react-app/src/pages/help.tsx b/extension/react-app/src/pages/help.tsx
new file mode 100644
index 00000000..3e2e93d2
--- /dev/null
+++ b/extension/react-app/src/pages/help.tsx
@@ -0,0 +1,98 @@
+import { useNavigate } from "react-router-dom";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts";
+import { buttonColor, lightGray, vscBackground } from "../components";
+import styled from "styled-components";
+
+const IconDiv = styled.div<{ backgroundColor?: string }>`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+
+ height: 100%;
+ padding: 0 4px;
+
+ &:hover {
+ background-color: ${(props) => props.backgroundColor || lightGray};
+ }
+`;
+
+function HelpPage() {
+ const navigate = useNavigate();
+
+ return (
+ <div className="overflow-scroll">
+ <div
+ className="items-center flex m-0 p-0 sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Help Center</h3>
+ </div>
+
+ <div className="grid grid-cols-2 grid-rows-2">
+ <IconDiv backgroundColor="rgb(234, 51, 35)">
+ <a href="https://youtu.be/3Ocrc-WX4iQ?si=eDLYtkc6CXQoHsEc">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-5.2 -4.5 60 60"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv backgroundColor={buttonColor}>
+ <a href="https://continue.dev/docs/how-to-use-continue">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-2.2 -2 28 28"
+ fill="white"
+ className="w-full h-full flex items-center justify-center"
+ >
+ <path d="M11.25 4.533A9.707 9.707 0 006 3a9.735 9.735 0 00-3.25.555.75.75 0 00-.5.707v14.25a.75.75 0 001 .707A8.237 8.237 0 016 18.75c1.995 0 3.823.707 5.25 1.886V4.533zM12.75 20.636A8.214 8.214 0 0118 18.75c.966 0 1.89.166 2.75.47a.75.75 0 001-.708V4.262a.75.75 0 00-.5-.707A9.735 9.735 0 0018 3a9.707 9.707 0 00-5.25 1.533v16.103z" />
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv backgroundColor="rgb(88, 98, 227)">
+ <a href="https://discord.gg/vapESyrFmJ">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-5 -5.5 60 60"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ <IconDiv>
+ <a href="https://github.com/continuedev/continue/issues/new/choose">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="-1.2 -1.2 32 32"
+ fill="white"
+ className="w-full h-full"
+ >
+ <path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"></path>
+ </svg>
+ </a>
+ </IconDiv>
+ </div>
+
+ <KeyboardShortcutsDialog></KeyboardShortcutsDialog>
+ </div>
+ );
+}
+
+export default HelpPage;
diff --git a/extension/react-app/src/pages/history.tsx b/extension/react-app/src/pages/history.tsx
index b901dd55..b6de0520 100644
--- a/extension/react-app/src/pages/history.tsx
+++ b/extension/react-app/src/pages/history.tsx
@@ -1,13 +1,14 @@
import React, { useContext, useEffect, useState } from "react";
import { SessionInfo } from "../../../schema/SessionInfo";
import { GUIClientContext } from "../App";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useNavigate } from "react-router-dom";
-import { secondaryDark, vscBackground } from "../components";
+import { lightGray, secondaryDark, vscBackground } from "../components";
import styled from "styled-components";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import CheckDiv from "../components/CheckDiv";
+import { temporarilyClearSession } from "../redux/slices/serverStateReducer";
const Tr = styled.tr`
&:hover {
@@ -41,6 +42,7 @@ function lastPartOfPath(path: string): string {
function History() {
const navigate = useNavigate();
+ const dispatch = useDispatch();
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const client = useContext(GUIClientContext);
const apiUrl = useSelector((state: RootStore) => state.config.apiUrl);
@@ -67,78 +69,106 @@ function History() {
fetchSessions();
}, [client]);
- console.log(sessions.map((session) => session.date_created));
-
return (
- <div className="w-full">
- <div className="items-center flex">
- <ArrowLeftIcon
- width="1.4em"
- height="1.4em"
- onClick={() => navigate("/")}
- className="inline-block ml-4 cursor-pointer"
- />
- <h1 className="text-xl font-bold m-4 inline-block">History</h1>
+ <div className="overflow-y-scroll">
+ <div className="sticky top-0" style={{ backgroundColor: vscBackground }}>
+ <div
+ className="items-center flex m-0 p-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">History</h3>
+ </div>
+ {workspacePaths && workspacePaths.length > 0 && (
+ <CheckDiv
+ checked={filteringByWorkspace}
+ onClick={() => setFilteringByWorkspace((prev) => !prev)}
+ title={`Show only sessions from ${lastPartOfPath(
+ workspacePaths[workspacePaths.length - 1]
+ )}/`}
+ />
+ )}
</div>
- {workspacePaths && workspacePaths.length > 0 && (
- <CheckDiv
- checked={filteringByWorkspace}
- onClick={() => setFilteringByWorkspace((prev) => !prev)}
- title={`Show only sessions from ${lastPartOfPath(
- workspacePaths[workspacePaths.length - 1]
- )}/`}
- />
+
+ {sessions.filter((session) => {
+ if (
+ !filteringByWorkspace ||
+ typeof workspacePaths === "undefined" ||
+ typeof session.workspace_directory === "undefined"
+ ) {
+ return true;
+ }
+ return workspacePaths.includes(session.workspace_directory);
+ }).length === 0 && (
+ <div className="text-center my-4">
+ No past sessions found. To start a new session, either click the "+"
+ button or use the keyboard shortcut: <b>Option + Command + N</b>
+ </div>
)}
- <table className="w-full">
- <tbody>
- {sessions
- .filter((session) => {
- if (
- !filteringByWorkspace ||
- typeof workspacePaths === "undefined" ||
- typeof session.workspace_directory === "undefined"
- ) {
- return true;
- }
- return workspacePaths.includes(session.workspace_directory);
- })
- .sort(
- (a, b) =>
- parseDate(b.date_created).getTime() -
- parseDate(a.date_created).getTime()
- )
- .map((session, index) => (
- <Tr key={index}>
- <td>
- <TdDiv
- onClick={() => {
- client?.loadSession(session.session_id);
- navigate("/");
- }}
- >
- <div className="text-md">{session.title}</div>
- <div className="text-gray-400">
- {parseDate(session.date_created).toLocaleString("en-US", {
- weekday: "short",
- year: "numeric",
- month: "long",
- day: "numeric",
- hour: "numeric",
- minute: "numeric",
- })}
- {" | "}
- {lastPartOfPath(session.workspace_directory || "")}/
- </div>
- </TdDiv>
- </td>
- </Tr>
- ))}
- </tbody>
- </table>
- <br />
- <i className="text-sm ml-4">
- All session data is saved in ~/.continue/sessions
- </i>
+
+ <div>
+ <table className="w-full">
+ <tbody>
+ {sessions
+ .filter((session) => {
+ if (
+ !filteringByWorkspace ||
+ typeof workspacePaths === "undefined" ||
+ typeof session.workspace_directory === "undefined"
+ ) {
+ return true;
+ }
+ return workspacePaths.includes(session.workspace_directory);
+ })
+ .sort(
+ (a, b) =>
+ parseDate(b.date_created).getTime() -
+ parseDate(a.date_created).getTime()
+ )
+ .map((session, index) => (
+ <Tr key={index}>
+ <td>
+ <TdDiv
+ onClick={() => {
+ client?.loadSession(session.session_id);
+ dispatch(temporarilyClearSession());
+ navigate("/");
+ }}
+ >
+ <div className="text-md">{session.title}</div>
+ <div className="text-gray-400">
+ {parseDate(session.date_created).toLocaleString(
+ "en-US",
+ {
+ year: "2-digit",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ }
+ )}
+ {" | "}
+ {lastPartOfPath(session.workspace_directory || "")}/
+ </div>
+ </TdDiv>
+ </td>
+ </Tr>
+ ))}
+ </tbody>
+ </table>
+ <br />
+ <i className="text-sm ml-4">
+ All session data is saved in ~/.continue/sessions
+ </i>
+ </div>
</div>
);
}
diff --git a/extension/react-app/src/pages/models.tsx b/extension/react-app/src/pages/models.tsx
new file mode 100644
index 00000000..1a6f275b
--- /dev/null
+++ b/extension/react-app/src/pages/models.tsx
@@ -0,0 +1,167 @@
+import React from "react";
+import ModelCard, { ModelInfo, ModelTag } from "../components/ModelCard";
+import styled from "styled-components";
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import { lightGray, vscBackground } from "../components";
+import { useNavigate } from "react-router-dom";
+
+const MODEL_INFO: ModelInfo[] = [
+ {
+ title: "OpenAI",
+ class: "OpenAI",
+ description: "Use gpt-4, gpt-3.5-turbo, or any other OpenAI model",
+ args: {
+ model: "gpt-4",
+ api_key: "",
+ title: "OpenAI",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag["Requires API Key"]],
+ },
+ {
+ title: "Anthropic",
+ class: "AnthropicLLM",
+ description:
+ "Claude-2 is a highly capable model with a 100k context length",
+ args: {
+ model: "claude-2",
+ api_key: "<ANTHROPIC_API_KEY>",
+ title: "Anthropic",
+ },
+ icon: "anthropic.png",
+ tags: [ModelTag["Requires API Key"]],
+ },
+ {
+ title: "Ollama",
+ class: "Ollama",
+ description:
+ "One of the fastest ways to get started with local models on Mac",
+ args: {
+ model: "codellama",
+ title: "Ollama",
+ },
+ icon: "ollama.png",
+ tags: [ModelTag["Local"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "TogetherAI",
+ class: "TogetherLLM",
+ description:
+ "Use the TogetherAI API for extremely fast streaming of open-source models",
+ args: {
+ model: "togethercomputer/CodeLlama-13b-Instruct",
+ api_key: "<TOGETHER_API_KEY>",
+ title: "TogetherAI",
+ },
+ icon: "together.png",
+ tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "LM Studio",
+ class: "GGML",
+ description:
+ "One of the fastest ways to get started with local models on Mac or Windows",
+ args: {
+ server_url: "http://localhost:1234",
+ title: "LM Studio",
+ },
+ icon: "lmstudio.png",
+ tags: [ModelTag["Local"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "Replicate",
+ class: "ReplicateLLM",
+ description: "Use the Replicate API to run open-source models",
+ args: {
+ model:
+ "replicate/llama-2-70b-chat:58d078176e02c219e11eb4da5a02a7830a283b14cf8f94537af893ccff5ee781",
+ api_key: "<REPLICATE_API_KEY>",
+ title: "Replicate",
+ },
+ icon: "replicate.png",
+ tags: [ModelTag["Requires API Key"], ModelTag["Open-Source"]],
+ },
+ {
+ title: "llama.cpp",
+ class: "LlamaCpp",
+ description: "If you are running the llama.cpp server from source",
+ args: {
+ title: "llama.cpp",
+ },
+ icon: "llamacpp.png",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "HuggingFace TGI",
+ class: "HuggingFaceTGI",
+ description:
+ "HuggingFace Text Generation Inference is an advanced, highly performant option for serving open-source models to multiple people",
+ args: {
+ title: "HuggingFace TGI",
+ },
+ icon: "hf.png",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "Other OpenAI-compatible API",
+ class: "GGML",
+ description:
+ "If you are using any other OpenAI-compatible API, for example text-gen-webui, FastChat, LocalAI, or llama-cpp-python, you can simply enter your server URL",
+ args: {
+ server_url: "<SERVER_URL>",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag.Local, ModelTag["Open-Source"]],
+ },
+ {
+ title: "GPT-4 limited free trial",
+ class: "OpenAIFreeTrial",
+ description:
+ "New users can try out Continue with GPT-4 using a proxy server that securely makes calls to OpenAI using our API key",
+ args: {
+ model: "gpt-4",
+ title: "GPT-4 Free Trial",
+ },
+ icon: "openai.svg",
+ tags: [ModelTag.Free],
+ },
+];
+
+const GridDiv = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-gap: 2rem;
+ padding: 1rem;
+ justify-items: center;
+ align-items: center;
+`;
+
+function Models() {
+ const navigate = useNavigate();
+ return (
+ <div className="overflow-y-scroll">
+ <div
+ className="items-center flex m-0 p-0 sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={() => navigate("/")}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Add a new model</h3>
+ </div>
+ <GridDiv>
+ {MODEL_INFO.map((model) => (
+ <ModelCard modelInfo={model} />
+ ))}
+ </GridDiv>
+ </div>
+ );
+}
+
+export default Models;
diff --git a/extension/react-app/src/pages/settings.tsx b/extension/react-app/src/pages/settings.tsx
index 8b3d9c5b..4bd51163 100644
--- a/extension/react-app/src/pages/settings.tsx
+++ b/extension/react-app/src/pages/settings.tsx
@@ -1,15 +1,23 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext } from "react";
import { GUIClientContext } from "../App";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { useNavigate } from "react-router-dom";
import { ContinueConfig } from "../../../schema/ContinueConfig";
-import { Button, TextArea, lightGray, secondaryDark } from "../components";
+import {
+ Button,
+ TextArea,
+ lightGray,
+ secondaryDark,
+ vscBackground,
+} from "../components";
import styled from "styled-components";
-import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import { ArrowLeftIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import Loader from "../components/Loader";
import InfoHover from "../components/InfoHover";
import { FormProvider, useForm } from "react-hook-form";
+import { setDialogMessage, setShowDialog } from "../redux/slices/uiStateSlice";
+import KeyboardShortcutsDialog from "../components/dialogs/KeyboardShortcuts";
const Hr = styled.hr`
border: 0.5px solid ${lightGray};
@@ -70,7 +78,7 @@ const Slider = styled.input.attrs({ type: "range" })`
border: none;
}
`;
-const ALL_MODEL_ROLES = ["default", "small", "medium", "large", "edit", "chat"];
+const ALL_MODEL_ROLES = ["default", "summarize", "edit", "chat"];
function Settings() {
const formMethods = useForm<ContinueConfig>();
@@ -79,6 +87,7 @@ function Settings() {
const navigate = useNavigate();
const client = useContext(GUIClientContext);
const config = useSelector((state: RootStore) => state.serverState.config);
+ const dispatch = useDispatch();
const submitChanges = () => {
if (!client) return;
@@ -106,17 +115,23 @@ function Settings() {
return (
<FormProvider {...formMethods}>
- <div className="w-full">
+ <div className="overflow-scroll">
+ <div
+ className="items-center flex sticky top-0"
+ style={{
+ borderBottom: `0.5px solid ${lightGray}`,
+ backgroundColor: vscBackground,
+ }}
+ >
+ <ArrowLeftIcon
+ width="1.2em"
+ height="1.2em"
+ onClick={submitAndLeave}
+ className="inline-block ml-4 cursor-pointer"
+ />
+ <h3 className="text-lg font-bold m-2 inline-block">Settings</h3>
+ </div>
<form onSubmit={formMethods.handleSubmit(onSubmit)}>
- <div className="items-center flex">
- <ArrowLeftIcon
- width="1.4em"
- height="1.4em"
- onClick={submitAndLeave}
- className="inline-block ml-4 cursor-pointer"
- />
- <h1 className="text-2xl font-bold m-4 inline-block">Settings</h1>
- </div>
{config ? (
<div className="p-2">
<h3 className="flex gap-1">
diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts
index 904b0e76..3a2e455a 100644
--- a/extension/react-app/src/redux/slices/serverStateReducer.ts
+++ b/extension/react-app/src/redux/slices/serverStateReducer.ts
@@ -1,6 +1,74 @@
import { createSlice } from "@reduxjs/toolkit";
import { FullState } from "../../../../schema/FullState";
+const TEST_TIMELINE = [
+ {
+ step: {
+ description: "Hi, please write bubble sort in python",
+ name: "User Input",
+ },
+ },
+ {
+ step: {
+ description: `\`\`\`python
+def bubble_sort(arr):
+ n = len(arr)
+ for i in range(n):
+ for j in range(0, n - i - 1):
+ if arr[j] > arr[j + 1]:
+ arr[j], arr[j + 1] = arr[j + 1], arr[j]
+ return arr
+\`\`\``,
+ name: "Bubble Sort in Python",
+ },
+ },
+ {
+ step: {
+ description: "Now write it in Rust",
+ name: "User Input",
+ },
+ },
+ {
+ step: {
+ description: "Hello! This is a test...\n\n1, 2, 3, testing...",
+ name: "Testing",
+ },
+ },
+ {
+ step: {
+ description: `Sure, here's bubble sort written in rust: \n\`\`\`rust
+fn bubble_sort<T: Ord>(values: &mut[T]) {
+ let len = values.len();
+ for i in 0..len {
+ for j in 0..(len - i - 1) {
+ if values[j] > values[j + 1] {
+ values.swap(j, j + 1);
+ }
+ }
+ }
+}
+\`\`\`\nIs there anything else I can answer?`,
+ name: "Rust Bubble Sort",
+ },
+ active: true,
+ },
+];
+
+const TEST_SLASH_COMMANDS = [
+ {
+ name: "edit",
+ description: "Edit the code",
+ },
+ {
+ name: "cmd",
+ description: "Generate a command",
+ },
+ {
+ name: "help",
+ description: "Get help using Continue",
+ },
+];
+
const initialState: FullState = {
history: {
timeline: [],
@@ -30,9 +98,21 @@ export const serverStateSlice = createSlice({
temporarilyPushToUserInputQueue: (state, action) => {
state.user_input_queue = [...state.user_input_queue, action.payload];
},
+ temporarilyClearSession: (state) => {
+ state.history.timeline = [];
+ state.selected_context_items = [];
+ state.session_info = {
+ title: "Loading session...",
+ session_id: "",
+ date_created: "",
+ };
+ },
},
});
-export const { setServerState, temporarilyPushToUserInputQueue } =
- serverStateSlice.actions;
+export const {
+ setServerState,
+ temporarilyPushToUserInputQueue,
+ temporarilyClearSession,
+} = serverStateSlice.actions;
export default serverStateSlice.reducer;
diff --git a/extension/schema/ContinueConfig.d.ts b/extension/schema/ContinueConfig.d.ts
index 5341056f..92f6e047 100644
--- a/extension/schema/ContinueConfig.d.ts
+++ b/extension/schema/ContinueConfig.d.ts
@@ -9,6 +9,7 @@ export type ContinueConfig = ContinueConfig1;
export type Name = string;
export type Hide = boolean;
export type Description = string;
+export type ClassName = string;
export type SystemMessage = string;
export type Role = "assistant" | "user" | "system" | "function";
export type Content = string;
@@ -18,36 +19,146 @@ export type Name2 = string;
export type Arguments = string;
export type ChatContext = ChatMessage[];
export type ManageOwnChatContext = boolean;
+/**
+ * Steps that will be automatically run at the beginning of a new session
+ */
export type StepsOnStartup = Step[];
+/**
+ * Steps that are not allowed to be run, and will be skipped if attempted
+ */
export type DisallowedSteps = string[];
+/**
+ * If this field is set to True, we will collect anonymous telemetry as described in the documentation page on telemetry. If set to False, we will not collect any data.
+ */
export type AllowAnonymousTelemetry = boolean;
+/**
+ * Configuration for the models used by Continue. Read more about how to configure models in the documentation.
+ */
export type Models = Models1;
-export type RequiresApiKey = string;
-export type RequiresUniqueId = boolean;
-export type RequiresWriteLog = boolean;
+/**
+ * A title that will identify this model in the model selection dropdown
+ */
+export type Title = string;
+/**
+ * A system message that will always be followed by the LLM
+ */
export type SystemMessage1 = string;
+/**
+ * The maximum context length of the LLM in tokens, as counted by count_tokens.
+ */
+export type ContextLength = number;
+/**
+ * The unique ID of the user.
+ */
+export type UniqueId = string;
+/**
+ * The name of the model to be used (e.g. gpt-4, codellama)
+ */
+export type Model = string;
+/**
+ * Tokens that will stop the completion.
+ */
+export type StopTokens = string[];
+/**
+ * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.
+ */
+export type Timeout = number;
+/**
+ * Whether to verify SSL certificates for requests.
+ */
+export type VerifySsl = boolean;
+/**
+ * Path to a custom CA bundle to use when making the HTTP request
+ */
+export type CaBundlePath = string;
+/**
+ * The API key for the LLM provider.
+ */
+export type ApiKey = string;
+export type Unused = LLM[];
+/**
+ * The temperature parameter for sampling from the LLM. Higher temperatures will result in more random output, while lower temperatures will result in more predictable output. This value ranges from 0 to 1.
+ */
export type Temperature = number;
export type Name3 = string;
export type Prompt = string;
export type Description1 = string;
+/**
+ * An array of custom commands that allow you to reuse prompts. Each has name, description, and prompt properties. When you enter /<name> in the text input, it will act as a shortcut to the prompt.
+ */
export type CustomCommands = CustomCommand[];
export type Name4 = string;
export type Description2 = string;
+/**
+ * An array of slash commands that let you map custom Steps to a shortcut.
+ */
export type SlashCommands = SlashCommand[];
+/**
+ * The step that will be run when a traceback is detected (when you use the shortcut cmd+shift+R)
+ */
+export type OnTraceback = Step;
+/**
+ * A system message that will always be followed by the LLM
+ */
export type SystemMessage2 = string;
-export type Title = string;
-export type Name5 = string;
+/**
+ * A Policy object that can be used to override the default behavior of Continue, for example in order to build custom agents that take multiple steps at a time.
+ */
+export type PolicyOverride = Policy;
+/**
+ * The title of the ContextProvider. This is what must be typed in the input to trigger the ContextProvider.
+ */
+export type Title1 = string;
+/**
+ * The ContinueSDK instance accessible by the ContextProvider
+ */
+export type Sdk = ContinueSDK1;
+/**
+ * The display title of the ContextProvider shown in the dropdown menu
+ */
+export type DisplayTitle = string;
+/**
+ * A description of the ContextProvider displayed in the dropdown menu
+ */
export type Description3 = string;
+/**
+ * Indicates whether the ContextProvider is dynamic
+ */
+export type Dynamic = boolean;
+/**
+ * Indicates whether the ContextProvider requires a query. For example, the SearchContextProvider requires you to type '@search <STRING_TO_SEARCH>'. This will change the behavior of the UI so that it can indicate the expectation for a query.
+ */
+export type RequiresQuery = boolean;
+export type Name5 = string;
+export type Description4 = string;
export type ProviderTitle = string;
export type ItemId = string;
export type Content1 = string;
export type Editing = boolean;
export type Editable = boolean;
+/**
+ * List of selected items in the ContextProvider
+ */
export type SelectedItems = ContextItem[];
+/**
+ * A list of ContextProvider objects that can be used to provide context to the LLM by typing '@'. Read more about ContextProviders in the documentation.
+ */
export type ContextProviders = ContextProvider[];
+/**
+ * An optional token to identify the user.
+ */
+export type UserToken = string;
+/**
+ * The URL of the server where development data is sent. No data is sent unless a valid user token is provided.
+ */
+export type DataServerUrl = string;
+/**
+ * If set to `True`, Continue will not generate summaries for each Step. This can be useful if you want to save on compute.
+ */
+export type DisableSummaries = boolean;
/**
- * A pydantic class for the continue config file.
+ * Continue can be deeply customized by editing the `ContinueConfig` object in `~/.continue/config.py` (`%userprofile%\.continue\config.py` for Windows) on your machine. This class is instantiated from the config file for every new session.
*/
export interface ContinueConfig1 {
steps_on_startup?: StepsOnStartup;
@@ -57,16 +168,20 @@ export interface ContinueConfig1 {
temperature?: Temperature;
custom_commands?: CustomCommands;
slash_commands?: SlashCommands;
- on_traceback?: Step;
+ on_traceback?: OnTraceback;
system_message?: SystemMessage2;
- policy_override?: Policy;
+ policy_override?: PolicyOverride;
context_providers?: ContextProviders;
+ user_token?: UserToken;
+ data_server_url?: DataServerUrl;
+ disable_summaries?: DisableSummaries;
[k: string]: unknown;
}
export interface Step {
name?: Name;
hide?: Hide;
description?: Description;
+ class_name?: ClassName;
system_message?: SystemMessage;
chat_context?: ChatContext;
manage_own_chat_context?: ManageOwnChatContext;
@@ -95,14 +210,28 @@ export interface Models1 {
large?: LLM;
edit?: LLM;
chat?: LLM;
+ unused?: Unused;
sdk?: ContinueSDK;
[k: string]: unknown;
}
export interface LLM {
- requires_api_key?: RequiresApiKey;
- requires_unique_id?: RequiresUniqueId;
- requires_write_log?: RequiresWriteLog;
+ title?: Title;
system_message?: SystemMessage1;
+ context_length?: ContextLength;
+ unique_id?: UniqueId;
+ model: Model;
+ stop_tokens?: StopTokens;
+ timeout?: Timeout;
+ verify_ssl?: VerifySsl;
+ ca_bundle_path?: CaBundlePath;
+ prompt_templates?: PromptTemplates;
+ api_key?: ApiKey;
+ [k: string]: unknown;
+}
+/**
+ * A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the "edit" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.
+ */
+export interface PromptTemplates {
[k: string]: unknown;
}
export interface ContinueSDK {
@@ -140,8 +269,12 @@ export interface Policy {
* When you hit enter on an option, the context provider will add that item to the autopilot's list of context (which is all stored in the ContextManager object).
*/
export interface ContextProvider {
- title: Title;
- sdk?: ContinueSDK1;
+ title: Title1;
+ sdk?: Sdk;
+ display_title: DisplayTitle;
+ description: Description3;
+ dynamic: Dynamic;
+ requires_query?: RequiresQuery;
selected_items?: SelectedItems;
[k: string]: unknown;
}
@@ -168,7 +301,7 @@ export interface ContextItem {
*/
export interface ContextItemDescription {
name: Name5;
- description: Description3;
+ description: Description4;
id: ContextItemId;
[k: string]: unknown;
}
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
index a847a608..5d5a5444 100644
--- a/extension/schema/FullState.d.ts
+++ b/extension/schema/FullState.d.ts
@@ -9,6 +9,7 @@ export type FullState = FullState1;
export type Name = string;
export type Hide = boolean;
export type Description = string;
+export type ClassName = string;
export type SystemMessage = string;
export type Role = "assistant" | "user" | "system" | "function";
export type Content = string;
@@ -44,6 +45,12 @@ export type DateCreated = string;
export type WorkspaceDirectory = string;
export type SystemMessage1 = string;
export type Temperature = number;
+export type Title1 = string;
+export type DisplayTitle = string;
+export type Description3 = string;
+export type Dynamic = boolean;
+export type RequiresQuery = boolean;
+export type ContextProviders = ContextProviderDescription[];
/**
* A full state of the program, including the history
@@ -58,6 +65,7 @@ export interface FullState1 {
session_info?: SessionInfo;
config: ContinueConfig;
saved_context_groups?: SavedContextGroups;
+ context_providers?: ContextProviders;
[k: string]: unknown;
}
/**
@@ -84,6 +92,7 @@ export interface Step {
name?: Name;
hide?: Hide;
description?: Description;
+ class_name?: ClassName;
system_message?: SystemMessage;
chat_context?: ChatContext;
manage_own_chat_context?: ManageOwnChatContext;
@@ -154,3 +163,11 @@ export interface ContinueConfig {
export interface SavedContextGroups {
[k: string]: ContextItem[];
}
+export interface ContextProviderDescription {
+ title: Title1;
+ display_title: DisplayTitle;
+ description: Description3;
+ dynamic: Dynamic;
+ requires_query: RequiresQuery;
+ [k: string]: unknown;
+}
diff --git a/extension/schema/History.d.ts b/extension/schema/History.d.ts
index 90124f4a..b00a1505 100644
--- a/extension/schema/History.d.ts
+++ b/extension/schema/History.d.ts
@@ -9,6 +9,7 @@ export type History = History1;
export type Name = string;
export type Hide = boolean;
export type Description = string;
+export type ClassName = string;
export type SystemMessage = string;
export type Role = "assistant" | "user" | "system" | "function";
export type Content = string;
@@ -49,6 +50,7 @@ export interface Step {
name?: Name;
hide?: Hide;
description?: Description;
+ class_name?: ClassName;
system_message?: SystemMessage;
chat_context?: ChatContext;
manage_own_chat_context?: ManageOwnChatContext;
diff --git a/extension/schema/HistoryNode.d.ts b/extension/schema/HistoryNode.d.ts
index 5ad32061..08424d75 100644
--- a/extension/schema/HistoryNode.d.ts
+++ b/extension/schema/HistoryNode.d.ts
@@ -9,6 +9,7 @@ export type HistoryNode = HistoryNode1;
export type Name = string;
export type Hide = boolean;
export type Description = string;
+export type ClassName = string;
export type SystemMessage = string;
export type Role = "assistant" | "user" | "system" | "function";
export type Content = string;
@@ -39,6 +40,7 @@ export interface Step {
name?: Name;
hide?: Hide;
description?: Description;
+ class_name?: ClassName;
system_message?: SystemMessage;
chat_context?: ChatContext;
manage_own_chat_context?: ManageOwnChatContext;
diff --git a/extension/schema/LLM.d.ts b/extension/schema/LLM.d.ts
index 255c752e..31d38456 100644
--- a/extension/schema/LLM.d.ts
+++ b/extension/schema/LLM.d.ts
@@ -6,15 +6,64 @@
*/
export type LLM = LLM1;
-export type RequiresApiKey = string;
-export type RequiresUniqueId = boolean;
-export type RequiresWriteLog = boolean;
+/**
+ * A title that will identify this model in the model selection dropdown
+ */
+export type Title = string;
+/**
+ * A system message that will always be followed by the LLM
+ */
export type SystemMessage = string;
+/**
+ * The maximum context length of the LLM in tokens, as counted by count_tokens.
+ */
+export type ContextLength = number;
+/**
+ * The unique ID of the user.
+ */
+export type UniqueId = string;
+/**
+ * The name of the model to be used (e.g. gpt-4, codellama)
+ */
+export type Model = string;
+/**
+ * Tokens that will stop the completion.
+ */
+export type StopTokens = string[];
+/**
+ * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.
+ */
+export type Timeout = number;
+/**
+ * Whether to verify SSL certificates for requests.
+ */
+export type VerifySsl = boolean;
+/**
+ * Path to a custom CA bundle to use when making the HTTP request
+ */
+export type CaBundlePath = string;
+/**
+ * The API key for the LLM provider.
+ */
+export type ApiKey = string;
export interface LLM1 {
- requires_api_key?: RequiresApiKey;
- requires_unique_id?: RequiresUniqueId;
- requires_write_log?: RequiresWriteLog;
+ title?: Title;
system_message?: SystemMessage;
+ context_length?: ContextLength;
+ unique_id?: UniqueId;
+ model: Model;
+ stop_tokens?: StopTokens;
+ timeout?: Timeout;
+ verify_ssl?: VerifySsl;
+ ca_bundle_path?: CaBundlePath;
+ prompt_templates?: PromptTemplates;
+ api_key?: ApiKey;
+ [k: string]: unknown;
+}
+/**
+ * A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the "edit" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.
+ */
+export interface PromptTemplates {
[k: string]: unknown;
}
diff --git a/extension/schema/Models.d.ts b/extension/schema/Models.d.ts
index 33e068b5..9005c08c 100644
--- a/extension/schema/Models.d.ts
+++ b/extension/schema/Models.d.ts
@@ -6,10 +6,47 @@
*/
export type Models = Models1;
-export type RequiresApiKey = string;
-export type RequiresUniqueId = boolean;
-export type RequiresWriteLog = boolean;
+/**
+ * A title that will identify this model in the model selection dropdown
+ */
+export type Title = string;
+/**
+ * A system message that will always be followed by the LLM
+ */
export type SystemMessage = string;
+/**
+ * The maximum context length of the LLM in tokens, as counted by count_tokens.
+ */
+export type ContextLength = number;
+/**
+ * The unique ID of the user.
+ */
+export type UniqueId = string;
+/**
+ * The name of the model to be used (e.g. gpt-4, codellama)
+ */
+export type Model = string;
+/**
+ * Tokens that will stop the completion.
+ */
+export type StopTokens = string[];
+/**
+ * Set the timeout for each request to the LLM. If you are running a local LLM that takes a while to respond, you might want to set this to avoid timeouts.
+ */
+export type Timeout = number;
+/**
+ * Whether to verify SSL certificates for requests.
+ */
+export type VerifySsl = boolean;
+/**
+ * Path to a custom CA bundle to use when making the HTTP request
+ */
+export type CaBundlePath = string;
+/**
+ * The API key for the LLM provider.
+ */
+export type ApiKey = string;
+export type Unused = LLM[];
/**
* Main class that holds the current model configuration
@@ -21,14 +58,28 @@ export interface Models1 {
large?: LLM;
edit?: LLM;
chat?: LLM;
+ unused?: Unused;
sdk?: ContinueSDK;
[k: string]: unknown;
}
export interface LLM {
- requires_api_key?: RequiresApiKey;
- requires_unique_id?: RequiresUniqueId;
- requires_write_log?: RequiresWriteLog;
+ title?: Title;
system_message?: SystemMessage;
+ context_length?: ContextLength;
+ unique_id?: UniqueId;
+ model: Model;
+ stop_tokens?: StopTokens;
+ timeout?: Timeout;
+ verify_ssl?: VerifySsl;
+ ca_bundle_path?: CaBundlePath;
+ prompt_templates?: PromptTemplates;
+ api_key?: ApiKey;
+ [k: string]: unknown;
+}
+/**
+ * A dictionary of prompt templates that can be used to customize the behavior of the LLM in certain situations. For example, set the "edit" key in order to change the prompt that is used for the /edit slash command. Each value in the dictionary is a string templated in mustache syntax, and filled in at runtime with the variables specific to the situation. See the documentation for more information.
+ */
+export interface PromptTemplates {
[k: string]: unknown;
}
export interface ContinueSDK {
diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts
index 7481c211..0dce250c 100644
--- a/extension/src/activation/activate.ts
+++ b/extension/src/activation/activate.ts
@@ -93,4 +93,6 @@ export async function activateExtension(context: vscode.ExtensionContext) {
}
)
);
+
+ vscode.commands.executeCommand("continue.focusContinueInput");
}
diff --git a/extension/src/commands.ts b/extension/src/commands.ts
index 479e8db0..4e2f4571 100644
--- a/extension/src/commands.ts
+++ b/extension/src/commands.ts
@@ -9,6 +9,36 @@ import { ideProtocolClient } from "./activation/activate";
let focusedOnContinueInput = false;
+function addHighlightedCodeToContext(edit: boolean) {
+ focusedOnContinueInput = !focusedOnContinueInput;
+ const editor = vscode.window.activeTextEditor;
+ if (editor) {
+ const selection = editor.selection;
+ if (selection.isEmpty) return;
+ const range = new vscode.Range(selection.start, selection.end);
+ const contents = editor.document.getText(range);
+ ideProtocolClient?.sendHighlightedCode(
+ [
+ {
+ filepath: editor.document.uri.fsPath,
+ contents,
+ range: {
+ start: {
+ line: selection.start.line,
+ character: selection.start.character,
+ },
+ end: {
+ line: selection.end.line,
+ character: selection.end.character,
+ },
+ },
+ },
+ ],
+ edit
+ );
+ }
+}
+
export const setFocusedOnContinueInput = (value: boolean) => {
focusedOnContinueInput = value;
};
@@ -32,11 +62,11 @@ const commandsMap: { [command: string]: (...args: any) => any } = {
debugPanelWebview?.postMessage({
type: "focusContinueInput",
});
-
- focusedOnContinueInput = !focusedOnContinueInput;
+ addHighlightedCodeToContext(false);
},
"continue.focusContinueInputWithEdit": async () => {
vscode.commands.executeCommand("continue.continueGUIView.focus");
+ addHighlightedCodeToContext(true);
debugPanelWebview?.postMessage({
type: "focusContinueInputWithEdit",
});
@@ -47,8 +77,7 @@ const commandsMap: { [command: string]: (...args: any) => any } = {
},
"continue.quickTextEntry": async () => {
const text = await vscode.window.showInputBox({
- placeHolder:
- "Ask a question or enter a slash command",
+ placeHolder: "Ask a question or enter a slash command",
title: "Continue Quick Input",
});
if (text) {
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index 5c04e351..e2c86bdf 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -176,37 +176,37 @@ class IdeProtocolClient {
});
// Setup listeners for any selection changes in open editors
- vscode.window.onDidChangeTextEditorSelection((event) => {
- if (!this.editorIsCode(event.textEditor)) {
- return;
- }
- if (this._highlightDebounce) {
- clearTimeout(this._highlightDebounce);
- }
- this._highlightDebounce = setTimeout(() => {
- const highlightedCode = event.textEditor.selections
- .filter((s) => !s.isEmpty)
- .map((selection) => {
- const range = new vscode.Range(selection.start, selection.end);
- const contents = event.textEditor.document.getText(range);
- return {
- filepath: event.textEditor.document.uri.fsPath,
- contents,
- range: {
- start: {
- line: selection.start.line,
- character: selection.start.character,
- },
- end: {
- line: selection.end.line,
- character: selection.end.character,
- },
- },
- };
- });
- this.sendHighlightedCode(highlightedCode);
- }, 100);
- });
+ // vscode.window.onDidChangeTextEditorSelection((event) => {
+ // if (!this.editorIsCode(event.textEditor)) {
+ // return;
+ // }
+ // if (this._highlightDebounce) {
+ // clearTimeout(this._highlightDebounce);
+ // }
+ // this._highlightDebounce = setTimeout(() => {
+ // const highlightedCode = event.textEditor.selections
+ // .filter((s) => !s.isEmpty)
+ // .map((selection) => {
+ // const range = new vscode.Range(selection.start, selection.end);
+ // const contents = event.textEditor.document.getText(range);
+ // return {
+ // filepath: event.textEditor.document.uri.fsPath,
+ // contents,
+ // range: {
+ // start: {
+ // line: selection.start.line,
+ // character: selection.start.character,
+ // },
+ // end: {
+ // line: selection.end.line,
+ // character: selection.end.character,
+ // },
+ // },
+ // };
+ // });
+ // this.sendHighlightedCode(highlightedCode);
+ // }, 100);
+ // });
// Register a content provider for the readonly virtual documents
const documentContentProvider = new (class
@@ -659,6 +659,11 @@ class IdeProtocolClient {
);
const terminalContents = await vscode.env.clipboard.readText();
await vscode.env.clipboard.writeText(tempCopyBuffer);
+
+ if (tempCopyBuffer === terminalContents) {
+ // This means there is no terminal open to select text from
+ return "";
+ }
return terminalContents;
}
@@ -729,16 +734,19 @@ class IdeProtocolClient {
this.messenger?.send("commandOutput", { output });
}
- sendHighlightedCode(highlightedCode: (RangeInFile & { contents: string })[]) {
- this.messenger?.send("highlightedCodePush", { highlightedCode });
+ sendHighlightedCode(
+ highlightedCode: (RangeInFile & { contents: string })[],
+ edit?: boolean
+ ) {
+ this.messenger?.send("highlightedCodePush", { highlightedCode, edit });
}
sendAcceptRejectSuggestion(accepted: boolean) {
this.messenger?.send("acceptRejectSuggestion", { accepted });
}
- sendAcceptRejectDiff(accepted: boolean) {
- this.messenger?.send("acceptRejectDiff", { accepted });
+ sendAcceptRejectDiff(accepted: boolean, stepIndex: number) {
+ this.messenger?.send("acceptRejectDiff", { accepted, stepIndex });
}
sendMainUserInput(input: string) {
diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts
index 4c077a25..426415fc 100644
--- a/extension/src/diffs.ts
+++ b/extension/src/diffs.ts
@@ -3,7 +3,7 @@ import * as path from "path";
import * as fs from "fs";
import * as vscode from "vscode";
import { extensionContext, ideProtocolClient } from "./activation/activate";
-import { getMetaKeyLabel } from "./util/util";
+import { getMetaKeyLabel, getPlatform } from "./util/util";
import { devDataPath } from "./activation/environmentSetup";
import { uriFromFilePath } from "./util/vscode";
@@ -194,10 +194,15 @@ class DiffManager {
this.diffs.set(newFilepath, diffInfo);
}
- vscode.commands.executeCommand(
- "workbench.action.files.revert",
- uriFromFilePath(newFilepath)
- );
+ if (getPlatform() === "windows") {
+ // Just a matter of how it renders
+ // Lags on windows without this
+ // Flashes too much on mac with it
+ vscode.commands.executeCommand(
+ "workbench.action.files.revert",
+ uriFromFilePath(newFilepath)
+ );
+ }
return newFilepath;
}
@@ -271,6 +276,8 @@ class DiffManager {
});
await recordAcceptReject(true, diffInfo);
+
+ ideProtocolClient.sendAcceptRejectDiff(true, diffInfo.step_index);
}
async rejectDiff(newFilepath?: string) {
@@ -302,6 +309,8 @@ class DiffManager {
});
await recordAcceptReject(false, diffInfo);
+
+ ideProtocolClient.sendAcceptRejectDiff(false, diffInfo.step_index);
}
}
@@ -339,10 +348,8 @@ async function recordAcceptReject(accepted: boolean, diffInfo: DiffInfo) {
export async function acceptDiffCommand(newFilepath?: string) {
await diffManager.acceptDiff(newFilepath);
- ideProtocolClient.sendAcceptRejectDiff(true);
}
export async function rejectDiffCommand(newFilepath?: string) {
await diffManager.rejectDiff(newFilepath);
- ideProtocolClient.sendAcceptRejectDiff(false);
}
diff --git a/extension/src/util/util.ts b/extension/src/util/util.ts
index 38c955e7..1ce4a8aa 100644
--- a/extension/src/util/util.ts
+++ b/extension/src/util/util.ts
@@ -65,7 +65,7 @@ export function debounced(delay: number, fn: Function) {
type Platform = "mac" | "linux" | "windows" | "unknown";
-function getPlatform(): Platform {
+export function getPlatform(): Platform {
const platform = os.platform();
if (platform === "darwin") {
return "mac";