summaryrefslogtreecommitdiff
path: root/extension/react-app
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app')
-rw-r--r--extension/react-app/package-lock.json98
-rw-r--r--extension/react-app/package.json1
-rw-r--r--extension/react-app/src/components/ComboBox.tsx176
-rw-r--r--extension/react-app/src/components/PillButton.tsx94
-rw-r--r--extension/react-app/src/components/StepContainer.tsx29
-rw-r--r--extension/react-app/src/components/StyledMarkdownPreview.tsx37
-rw-r--r--extension/react-app/src/components/TextDialog.tsx2
-rw-r--r--extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts10
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts19
-rw-r--r--extension/react-app/src/main.tsx6
-rw-r--r--extension/react-app/src/pages/gui.tsx107
-rw-r--r--extension/react-app/src/redux/selectors/uiStateSelectors.ts5
-rw-r--r--extension/react-app/src/redux/slices/uiStateSlice.ts24
-rw-r--r--extension/react-app/src/redux/store.ts6
14 files changed, 426 insertions, 188 deletions
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index 13e02e86..fa7834f1 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -13,6 +13,7 @@
"@types/vscode-webview": "^1.57.1",
"@uiw/react-markdown-preview": "^4.1.13",
"downshift": "^7.6.0",
+ "meilisearch": "^0.33.0",
"posthog-js": "^1.58.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
@@ -1423,6 +1424,14 @@
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
},
+ "node_modules/cross-fetch": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -2500,6 +2509,14 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/meilisearch": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz",
+ "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==",
+ "dependencies": {
+ "cross-fetch": "^3.1.6"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3092,6 +3109,25 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
@@ -4115,6 +4151,11 @@
"node": ">=8.0"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -4423,6 +4464,20 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -5325,6 +5380,14 @@
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
},
+ "cross-fetch": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+ "requires": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
@@ -6098,6 +6161,14 @@
"@types/mdast": "^3.0.0"
}
},
+ "meilisearch": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.33.0.tgz",
+ "integrity": "sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==",
+ "requires": {
+ "cross-fetch": "^3.1.6"
+ }
+ },
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -6434,6 +6505,14 @@
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true
},
+ "node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
"node-releases": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
@@ -7129,6 +7208,11 @@
"is-number": "^7.0.0"
}
},
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
"trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -7315,6 +7399,20 @@
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="
},
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index 704f520a..e5f5a329 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -14,6 +14,7 @@
"@types/vscode-webview": "^1.57.1",
"@uiw/react-markdown-preview": "^4.1.13",
"downshift": "^7.6.0",
+ "meilisearch": "^0.33.0",
"posthog-js": "^1.58.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index bf07cb93..4a1cdbc0 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,9 @@
-import React, { useEffect, useImperativeHandle, useState } from "react";
+import React, {
+ useContext,
+ useEffect,
+ useImperativeHandle,
+ useState,
+} from "react";
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
@@ -8,13 +13,17 @@ import {
vscBackground,
vscForeground,
} from ".";
-import CodeBlock from "./CodeBlock";
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
import { DocumentPlus } from "@styled-icons/heroicons-outline";
-import { HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
-import { getMetaKeyLabel } from "../util";
+import { GUIClientContext } from "../App";
+import { MeiliSearch } from "meilisearch";
+import { setBottomMessageCloseTimeout } from "../redux/slices/uiStateSlice";
+import { useDispatch } from "react-redux";
+
+const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
const mainInputFontSize = 13;
@@ -64,6 +73,7 @@ const Ul = styled.ul<{
hidden: boolean;
showAbove: boolean;
ulHeightPixels: number;
+ inputBoxHeight?: string;
}>`
${(props) =>
props.showAbove
@@ -104,35 +114,79 @@ const Li = styled.li<{
// #endregion
interface ComboBoxProps {
- items: { name: string; description: string }[];
+ items: { name: string; description: string; id?: string }[];
onInputValueChange: (inputValue: string) => void;
disabled?: boolean;
onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
- highlightedCodeSections: HighlightedRangeContext[];
- deleteContextItems: (indices: number[]) => void;
- onTogglePin: () => void;
+ selectedContextItems: ContextItem[];
onToggleAddContext: () => void;
addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+ const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+ const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
const [history, setHistory] = React.useState<string[]>([]);
// The position of the current command you are typing now, so the one that will be appended to history once you press enter
const [positionInHistory, setPositionInHistory] = React.useState<number>(0);
const [items, setItems] = React.useState(props.items);
- const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
- props.highlightedCodeSections || []
- );
+
const inputRef = React.useRef<HTMLInputElement>(null);
+ const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
+ undefined
+ );
- useEffect(() => {
- setHighlightedCodeSections(props.highlightedCodeSections || []);
- }, [props.highlightedCodeSections]);
+ // Whether the current input follows an '@' and should be treated as context query
+ const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
const { getInputProps, ...downshiftProps } = useCombobox({
- onInputValueChange({ inputValue }) {
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (selectedItem?.id) {
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+ const restOfInput = segs.splice(0, segs.length - 1).join("@");
+
+ // Tell server the context item was selected
+ client?.selectContextItem(selectedItem.id, query);
+
+ // Remove the '@' and the context query from the input
+ if (downshiftProps.inputValue.includes("@")) {
+ downshiftProps.setInputValue(restOfInput);
+ }
+ }
+ },
+ onInputValueChange({ inputValue, highlightedIndex }) {
if (!inputValue) return;
props.onInputValueChange(inputValue);
+
+ if (inputValue.endsWith("@") || currentlyInContextQuery) {
+ setCurrentlyInContextQuery(true);
+
+ const segs = inputValue.split("@");
+ const providerAndQuery = segs[segs.length - 1];
+ const [provider, query] = providerAndQuery.split(" ");
+ searchClient
+ .index(SEARCH_INDEX_NAME)
+ .search(providerAndQuery)
+ .then((res) => {
+ setItems(
+ res.hits.map((hit) => {
+ return {
+ name: hit.name,
+ description: hit.description,
+ id: hit.id,
+ };
+ })
+ );
+ })
+ .catch(() => {
+ // Swallow errors, because this simply is not supported on Windows at the moment
+ });
+ return;
+ }
setItems(
props.items.filter((item) =>
item.name.toLowerCase().startsWith(inputValue.toLowerCase())
@@ -145,6 +199,18 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
},
});
+ useEffect(() => {
+ if (downshiftProps.highlightedIndex < 0) {
+ downshiftProps.setHighlightedIndex(0);
+ }
+ }, [downshiftProps.inputValue]);
+
+ const divRef = React.useRef<HTMLDivElement>(null);
+ const ulRef = React.useRef<HTMLUListElement>(null);
+ const showAbove = () => {
+ return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight;
+ };
+
useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
@@ -184,59 +250,25 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
};
}, [inputRef.current]);
- const divRef = React.useRef<HTMLDivElement>(null);
- const ulRef = React.useRef<HTMLUListElement>(null);
- const showAbove = () => {
- return (divRef.current?.getBoundingClientRect().top || 0) > UlMaxHeight;
- };
-
return (
<>
<div className="px-2 flex gap-2 items-center flex-wrap mt-2">
- {/* {highlightedCodeSections.length > 1 && (
- <>
- <HeaderButtonWithText
- text="Clear Context"
- onClick={() => {
- props.deleteContextItems(
- highlightedCodeSections.map((_, idx) => idx)
- );
- }}
- >
- <Trash size="1.6em" />
- </HeaderButtonWithText>
- </>
- )} */}
- {highlightedCodeSections.map((section, idx) => (
- <PillButton
- warning={
- section.range.contents.length > 4000 && section.editing
- ? "Editing such a large range may be slow"
- : undefined
- }
- onlyShowDelete={
- highlightedCodeSections.length <= 1 || section.editing
- }
- editing={section.editing}
- pinned={section.pinned}
- index={idx}
- key={`${section.display_name}${idx}`}
- title={`${section.display_name} (${
- section.range.range.start.line + 1
- }-${section.range.range.end.line + 1})`}
- onDelete={() => {
- if (props.deleteContextItems) {
- props.deleteContextItems([idx]);
+ {props.selectedContextItems.map((item, idx) => {
+ return (
+ <PillButton
+ key={`${item.description.id.item_id}${idx}`}
+ item={item}
+ warning={
+ item.content.length > 4000 && item.editing
+ ? "Editing such a large range may be slow"
+ : undefined
}
- setHighlightedCodeSections((prev) => {
- const newSections = [...prev];
- newSections.splice(idx, 1);
- return newSections;
- });
- }}
- />
- ))}
- {props.highlightedCodeSections.length > 0 &&
+ addingHighlightedCode={props.addingHighlightedCode}
+ index={idx}
+ />
+ );
+ })}
+ {props.selectedContextItems.length > 0 &&
(props.addingHighlightedCode ? (
<EmptyPillDiv
onClick={() => {
@@ -259,7 +291,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
<div className="flex px-2" ref={divRef} hidden={!downshiftProps.isOpen}>
<MainTextInput
disabled={props.disabled}
- placeholder={`Ask a question, give instructions, or type '/' to see slash commands`}
+ placeholder={`Ask a question, give instructions, type '/' for slash commands, or '@' to add context`}
{...getInputProps({
onChange: (e) => {
const target = e.target as HTMLTextAreaElement;
@@ -269,11 +301,13 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
target.scrollHeight,
300
).toString()}px`;
+ setInputBoxHeight(target.style.height);
// setShowContextDropdown(target.value.endsWith("@"));
},
onFocus: (e) => {
setFocused(true);
+ dispatch(setBottomMessageCloseTimeout(undefined));
},
onBlur: (e) => {
setFocused(false);
@@ -283,6 +317,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (event.key === "Enter" && event.shiftKey) {
// Prevent Downshift's default 'Enter' behavior.
(event.nativeEvent as any).preventDownshiftDefault = true;
+ setCurrentlyInContextQuery(false);
} else if (
event.key === "Enter" &&
(!downshiftProps.isOpen || items.length === 0)
@@ -296,6 +331,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
(event.nativeEvent as any).preventDownshiftDefault = true;
if (props.onEnter) props.onEnter(event);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -315,6 +351,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
downshiftProps.setInputValue(history[positionInHistory - 1]);
setPositionInHistory((prev) => prev - 1);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "ArrowDown") {
if (positionInHistory < history.length) {
downshiftProps.setInputValue(history[positionInHistory + 1]);
@@ -322,8 +359,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setPositionInHistory((prev) =>
Math.min(prev + 1, history.length)
);
+ setCurrentlyInContextQuery(false);
}
},
+ onClick: () => {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ },
ref: inputRef,
})}
/>
@@ -345,13 +386,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
selected={downshiftProps.selectedItem === item}
>
<span>
- {item.name}: {item.description}
+ {item.name}:{" "}
+ <span style={{ color: lightGray }}>{item.description}</span>
</span>
</Li>
))}
</Ul>
</div>
- {highlightedCodeSections.length === 0 &&
+ {props.selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
(focused &&
metaKeyPressed &&
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 5929d06a..548fdf9d 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,8 +1,9 @@
-import { useContext, useState } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
defaultBorderRadius,
+ lightGray,
secondaryDark,
vscBackground,
vscForeground,
@@ -13,6 +14,14 @@ import {
ExclamationTriangle,
} from "@styled-icons/heroicons-outline";
import { GUIClientContext } from "../App";
+import { useDispatch } from "react-redux";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+} from "../redux/slices/uiStateSlice";
+import { ContextItem } from "../../../schema/FullState";
+import { ReactMarkdown } from "react-markdown/lib/react-markdown";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
const Button = styled.button`
border: none;
@@ -68,19 +77,55 @@ const CircleDiv = styled.div`
interface PillButtonProps {
onHover?: (arg0: boolean) => void;
- onDelete?: () => void;
- title: string;
- index: number;
- editing: boolean;
- pinned: boolean;
+ item: ContextItem;
warning?: string;
- onlyShowDelete?: boolean;
+ index: number;
+ addingHighlightedCode?: boolean;
}
const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (isHovered) {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ dispatch(
+ setBottomMessage(
+ <>
+ <b>{props.item.description.name}</b>:{" "}
+ {props.item.description.description}
+ <pre>
+ <code
+ style={{
+ fontSize: "11px",
+ backgroundColor: vscBackground,
+ color: vscForeground,
+ whiteSpace: "pre-wrap",
+ wordWrap: "break-word",
+ }}
+ >
+ {props.item.content}
+ </code>
+ </pre>
+ </>
+ )
+ );
+ } else {
+ dispatch(
+ setBottomMessageCloseTimeout(
+ setTimeout(() => {
+ if (!isHovered) {
+ dispatch(setBottomMessage(undefined));
+ }
+ }, 2000)
+ )
+ );
+ }
+ }, [isHovered]);
+
return (
<>
<div style={{ position: "relative" }}>
@@ -89,10 +134,8 @@ const PillButton = (props: PillButtonProps) => {
position: "relative",
borderColor: props.warning
? "red"
- : props.editing
+ : props.item.editing
? "#8800aa"
- : props.pinned
- ? "#ffff0099"
: "transparent",
borderWidth: "1px",
borderStyle: "solid",
@@ -113,11 +156,14 @@ const PillButton = (props: PillButtonProps) => {
{isHovered && (
<GridDiv
style={{
- gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr",
+ gridTemplateColumns:
+ props.item.editable && props.addingHighlightedCode
+ ? "1fr 1fr"
+ : "1fr",
backgroundColor: vscBackground,
}}
>
- {props.onlyShowDelete || (
+ {props.item.editable && props.addingHighlightedCode && (
<ButtonDiv
data-tooltip-id={`edit-${props.index}`}
backgroundColor={"#8800aa55"}
@@ -132,15 +178,6 @@ const PillButton = (props: PillButtonProps) => {
</ButtonDiv>
)}
- {/* <ButtonDiv
- data-tooltip-id={`pin-${props.index}`}
- backgroundColor={"#ffff0055"}
- onClick={() => {
- client?.setPinnedAtIndices([props.index]);
- }}
- >
- <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>
- </ButtonDiv> */}
<StyledTooltip id={`pin-${props.index}`}>
Edit this range
</StyledTooltip>
@@ -148,33 +185,34 @@ const PillButton = (props: PillButtonProps) => {
data-tooltip-id={`delete-${props.index}`}
backgroundColor={"#cc000055"}
onClick={() => {
- if (props.onDelete) {
- props.onDelete();
- }
+ client?.deleteContextWithIds([props.item.description.id]);
+ dispatch(setBottomMessageCloseTimeout(undefined));
}}
>
<Trash style={{ margin: "auto" }} width="1.6em"></Trash>
</ButtonDiv>
</GridDiv>
)}
- {props.title}
+ {props.item.description.name}
</Button>
<StyledTooltip id={`edit-${props.index}`}>
- {props.editing
+ {props.item.editing
? "Editing this section (with entire file as context)"
: "Edit this section"}
</StyledTooltip>
<StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
{props.warning && (
<>
- <CircleDiv data-tooltip-id={`circle-div-${props.title}`}>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ >
<ExclamationTriangle
style={{ margin: "auto" }}
width="1.0em"
strokeWidth={2}
/>
</CircleDiv>
- <StyledTooltip id={`circle-div-${props.title}`}>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
{props.warning}
</StyledTooltip>
</>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index bc8665fd..2cfe7ecd 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -18,9 +18,9 @@ import {
import { StopCircle } from "@styled-icons/heroicons-solid";
import { HistoryNode } from "../../../schema/HistoryNode";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import MarkdownPreview from "@uiw/react-markdown-preview";
import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
import { GUIClientContext } from "../App";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
interface StepContainerProps {
historyNode: HistoryNode;
@@ -109,33 +109,6 @@ const GradientBorder = styled.div<{
background-size: 200% 200%;
`;
-const StyledMarkdownPreview = styled(MarkdownPreview)`
- pre {
- background-color: ${secondaryDark};
- padding: 1px;
- border-radius: ${defaultBorderRadius};
- border: 0.5px solid white;
- }
-
- code {
- color: #f78383;
- word-wrap: break-word;
- border-radius: ${defaultBorderRadius};
- background-color: ${secondaryDark};
- }
-
- pre > code {
- background-color: ${secondaryDark};
- color: ${vscForeground};
- }
-
- background-color: ${vscBackground};
- font-family: "Lexend", sans-serif;
- font-size: 13px;
- padding: 8px;
- color: ${vscForeground};
-`;
-
// #endregion
function StepContainer(props: StepContainerProps) {
diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx
new file mode 100644
index 00000000..9c2ecb62
--- /dev/null
+++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx
@@ -0,0 +1,37 @@
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ secondaryDark,
+ vscBackground,
+ vscForeground,
+} from ".";
+import MarkdownPreview from "@uiw/react-markdown-preview";
+
+const StyledMarkdownPreview = styled(MarkdownPreview)`
+ pre {
+ background-color: ${secondaryDark};
+ padding: 1px;
+ border-radius: ${defaultBorderRadius};
+ border: 0.5px solid white;
+ }
+
+ code {
+ color: #f78383;
+ word-wrap: break-word;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${secondaryDark};
+ }
+
+ pre > code {
+ background-color: ${secondaryDark};
+ color: ${vscForeground};
+ }
+
+ background-color: ${vscBackground};
+ font-family: "Lexend", sans-serif;
+ font-size: 13px;
+ padding: 8px;
+ color: ${vscForeground};
+`;
+
+export default StyledMarkdownPreview;
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index 9597b578..7d8e9920 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -6,7 +6,7 @@ import { isMetaEquivalentKeyPressed } from "../util";
import { ReactMarkdown } from "react-markdown/lib/react-markdown";
const ScreenCover = styled.div`
- position: absolute;
+ position: fixed;
width: 100%;
height: 100%;
background-color: rgba(168, 168, 168, 0.5);
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 6c0df8fc..8e3735ec 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -1,3 +1,5 @@
+import { ContextItemId } from "../../../schema/FullState";
+
abstract class AbstractContinueGUIClientProtocol {
abstract sendMainInput(input: string): void;
@@ -13,23 +15,21 @@ abstract class AbstractContinueGUIClientProtocol {
callback: (commands: { name: string; description: string }[]) => void
): void;
- abstract changeDefaultModel(model: string): void;
-
abstract sendClear(): void;
abstract retryAtIndex(index: number): void;
abstract deleteAtIndex(index: number): void;
- abstract deleteContextAtIndices(indices: number[]): void;
+ abstract deleteContextWithIds(ids: ContextItemId[]): void;
abstract setEditingAtIndices(indices: number[]): void;
- abstract setPinnedAtIndices(indices: number[]): void;
-
abstract toggleAddingHighlightedCode(): void;
abstract showLogsAtIndex(index: number): void;
+
+ abstract selectContextItem(id: string, query: string): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 7d6c2a71..b8019664 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -1,3 +1,4 @@
+import { ContextItemId } from "../../../schema/FullState";
import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol";
import { Messenger, WebsocketMessenger } from "./messenger";
import { VscodeMessenger } from "./vscodeMessenger";
@@ -52,10 +53,6 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
});
}
- changeDefaultModel(model: string) {
- this.messenger.send("change_default_model", { model });
- }
-
sendClear() {
this.messenger.send("clear_history", {});
}
@@ -68,18 +65,16 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger.send("delete_at_index", { index });
}
- deleteContextAtIndices(indices: number[]) {
- this.messenger.send("delete_context_at_indices", { indices });
+ deleteContextWithIds(ids: ContextItemId[]) {
+ this.messenger.send("delete_context_with_ids", {
+ ids: ids.map((id) => `${id.provider_title}-${id.item_id}`),
+ });
}
setEditingAtIndices(indices: number[]) {
this.messenger.send("set_editing_at_indices", { indices });
}
- setPinnedAtIndices(indices: number[]) {
- this.messenger.send("set_pinned_at_indices", { indices });
- }
-
toggleAddingHighlightedCode(): void {
this.messenger.send("toggle_adding_highlighted_code", {});
}
@@ -87,6 +82,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
showLogsAtIndex(index: number): void {
this.messenger.send("show_logs_at_index", { index });
}
+
+ selectContextItem(id: string, query: string): void {
+ this.messenger.send("select_context_item", { id, query });
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/main.tsx b/extension/react-app/src/main.tsx
index e29a7d5f..1776490c 100644
--- a/extension/react-app/src/main.tsx
+++ b/extension/react-app/src/main.tsx
@@ -8,13 +8,11 @@ import "./index.css";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
+console.log("Starting React");
+
posthog.init("phc_JS6XFROuNbhJtVCEdTSYk6gl5ArRrTNMpCcguAXlSPs", {
api_host: "https://app.posthog.com",
disable_session_recording: true,
- session_recording: {
- // WARNING: Only enable this if you understand the security implications
- // recordCrossOriginIframes: true,
- } as any,
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index 70031d40..5d893de9 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -6,7 +6,7 @@ import {
} from "../components";
import Loader from "../components/Loader";
import ContinueButton from "../components/ContinueButton";
-import { FullState, HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem, FullState } from "../../../schema/FullState";
import { useCallback, useEffect, useRef, useState, useContext } from "react";
import { History } from "../../../schema/History";
import { HistoryNode } from "../../../schema/HistoryNode";
@@ -22,12 +22,16 @@ import TextDialog from "../components/TextDialog";
import HeaderButtonWithText from "../components/HeaderButtonWithText";
import ReactSwitch from "react-switch";
import { usePostHog } from "posthog-js/react";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { postVscMessage } from "../vscode";
import UserInputContainer from "../components/UserInputContainer";
import Onboarding from "../components/Onboarding";
import { isMetaEquivalentKeyPressed } from "../util";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+} from "../redux/slices/uiStateSlice";
const TopGUIDiv = styled.div`
overflow: hidden;
@@ -64,9 +68,6 @@ function GUI(props: GUIProps) {
const vscMachineId = useSelector(
(state: RootStore) => state.config.vscMachineId
);
- const vscMediaUrl = useSelector(
- (state: RootStore) => state.config.vscMediaUrl
- );
const [dataSwitchChecked, setDataSwitchChecked] = useState(false);
const dataSwitchOn = useSelector(
(state: RootStore) => state.config.dataSwitchOn
@@ -80,15 +81,13 @@ function GUI(props: GUIProps) {
const [waitingForSteps, setWaitingForSteps] = useState(false);
const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
- const [highlightedRanges, setHighlightedRanges] = useState<
- HighlightedRangeContext[]
- >([]);
const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
+ const [selectedContextItems, setSelectedContextItems] = useState<
+ ContextItem[]
+ >([]);
const [availableSlashCommands, setAvailableSlashCommands] = useState<
{ name: string; description: string }[]
>([]);
- const [pinned, setPinned] = useState(false);
- const [showDataSharingInfo, setShowDataSharingInfo] = useState(false);
const [stepsOpen, setStepsOpen] = useState<boolean[]>([
true,
true,
@@ -117,10 +116,36 @@ function GUI(props: GUIProps) {
current_index: 3,
} as any);
+ const vscMediaUrl = useSelector(
+ (state: RootStore) => state.config.vscMediaUrl
+ );
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
const [feedbackEntryOn, setFeedbackEntryOn] = useState(true);
+ const dispatch = useDispatch();
+ const bottomMessage = useSelector(
+ (state: RootStore) => state.uiState.bottomMessage
+ );
+
+ const [displayBottomMessageOnBottom, setDisplayBottomMessageOnBottom] =
+ useState<boolean>(true);
+ const mainTextInputRef = useRef<HTMLInputElement>(null);
+
+ const aboveComboBoxDivRef = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ if (!aboveComboBoxDivRef.current) return;
+ if (
+ aboveComboBoxDivRef.current.getBoundingClientRect().top >
+ window.innerHeight / 2
+ ) {
+ setDisplayBottomMessageOnBottom(false);
+ } else {
+ setDisplayBottomMessageOnBottom(true);
+ }
+ }, [bottomMessage, aboveComboBoxDivRef.current]);
+
const topGuiDivRef = useRef<HTMLDivElement>(null);
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
@@ -152,6 +177,8 @@ function GUI(props: GUIProps) {
history.timeline[history.current_index]?.active
) {
client?.deleteAtIndex(history.current_index);
+ } else if (e.key === "Escape") {
+ dispatch(setBottomMessageCloseTimeout(undefined));
}
};
window.addEventListener("keydown", listener);
@@ -178,7 +205,7 @@ function GUI(props: GUIProps) {
setWaitingForSteps(waitingForSteps);
setHistory(state.history);
- setHighlightedRanges(state.highlighted_ranges);
+ setSelectedContextItems(state.selected_context_items || []);
setUserInputQueue(state.user_input_queue);
setAddingHighlightedCode(state.adding_highlighted_code);
setAvailableSlashCommands(
@@ -211,15 +238,6 @@ function GUI(props: GUIProps) {
scrollToBottom();
}, [waitingForSteps]);
- const mainTextInputRef = useRef<HTMLInputElement>(null);
-
- const deleteContextItems = useCallback(
- (indices: number[]) => {
- client?.deleteContextAtIndices(indices);
- },
- [client]
- );
-
const onMainTextInput = (event?: any) => {
if (mainTextInputRef.current) {
let input = (mainTextInputRef.current as any).inputValue;
@@ -351,6 +369,7 @@ function GUI(props: GUIProps) {
})}
</div>
+ <div ref={aboveComboBoxDivRef} />
<ComboBox
ref={mainTextInputRef}
onEnter={(e) => {
@@ -360,11 +379,7 @@ function GUI(props: GUIProps) {
}}
onInputValueChange={() => {}}
items={availableSlashCommands}
- highlightedCodeSections={highlightedRanges}
- deleteContextItems={deleteContextItems}
- onTogglePin={() => {
- setPinned((prev: boolean) => !prev);
- }}
+ selectedContextItems={selectedContextItems}
onToggleAddContext={() => {
client?.toggleAddingHighlightedCode();
}}
@@ -373,40 +388,42 @@ function GUI(props: GUIProps) {
<ContinueButton onClick={onMainTextInput} />
</TopGUIDiv>
<div
+ onMouseEnter={() => {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ }}
+ onMouseLeave={(e) => {
+ if (!e.buttons) {
+ dispatch(setBottomMessage(undefined));
+ }
+ }}
style={{
position: "fixed",
- bottom: "50px",
+ bottom: displayBottomMessageOnBottom ? "50px" : undefined,
+ top: displayBottomMessageOnBottom ? undefined : "50px",
+ left: "0",
+ right: "0",
+ margin: "8px",
+ marginTop: "0px",
backgroundColor: vscBackground,
color: vscForeground,
borderRadius: defaultBorderRadius,
- padding: "16px",
- margin: "16px",
+ padding: "12px",
zIndex: 100,
- boxShadow: `0px 0px 10px 0px ${vscForeground}`,
+ boxShadow: `0px 0px 6px 0px ${vscForeground}`,
+ maxHeight: "50vh",
+ overflow: "scroll",
}}
- hidden={!showDataSharingInfo}
+ hidden={!bottomMessage}
>
- By turning on this switch, you will begin collecting accepted and
- rejected suggestions in .continue/suggestions.json. This data is stored
- locally on your machine and not sent anywhere.
- <br />
- <br />
- <b>
- {dataSwitchChecked
- ? "👍 Data is being collected"
- : "👎 No data is being collected"}
- </b>
+ {bottomMessage}
</div>
<Footer dataSwitchChecked={dataSwitchChecked}>
{vscMediaUrl && (
- <a
- href="https://github.com/continuedev/continue"
- style={{ marginRight: "auto" }}
- >
+ <a href="https://github.com/continuedev/continue">
<img src={`${vscMediaUrl}/continue-dev-square.png`} width="22px" />
</a>
)}
- {/* <p style={{ margin: "0", marginRight: "auto" }}>Continue</p> */}
+ <p style={{ margin: "0", marginRight: "auto" }}>Continue</p>
<HeaderButtonWithText
onClick={() => {
// Show the dialog
diff --git a/extension/react-app/src/redux/selectors/uiStateSelectors.ts b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
new file mode 100644
index 00000000..7ebc9338
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
@@ -0,0 +1,5 @@
+import { RootStore } from "../store";
+
+const selectBottomMessage = (state: RootStore) => state.uiState.bottomMessage;
+
+export { selectBottomMessage };
diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts
new file mode 100644
index 00000000..837d19e9
--- /dev/null
+++ b/extension/react-app/src/redux/slices/uiStateSlice.ts
@@ -0,0 +1,24 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+export const uiStateSlice = createSlice({
+ name: "uiState",
+ initialState: {
+ bottomMessage: undefined,
+ bottomMessageCloseTimeout: undefined,
+ },
+ reducers: {
+ setBottomMessage: (state, action) => {
+ state.bottomMessage = action.payload;
+ },
+ setBottomMessageCloseTimeout: (state, action) => {
+ if (state.bottomMessageCloseTimeout) {
+ clearTimeout(state.bottomMessageCloseTimeout);
+ }
+ state.bottomMessageCloseTimeout = action.payload;
+ },
+ },
+});
+
+export const { setBottomMessage, setBottomMessageCloseTimeout } =
+ uiStateSlice.actions;
+export default uiStateSlice.reducer;
diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts
index b6eb55b3..d49513e5 100644
--- a/extension/react-app/src/redux/store.ts
+++ b/extension/react-app/src/redux/store.ts
@@ -3,6 +3,7 @@ import debugStateReducer from "./slices/debugContexSlice";
import chatReducer from "./slices/chatSlice";
import configReducer from "./slices/configSlice";
import miscReducer from "./slices/miscSlice";
+import uiStateReducer from "./slices/uiStateSlice";
import { RangeInFile, SerializedDebugContext } from "../../../src/client";
export interface ChatMessage {
@@ -31,6 +32,10 @@ export interface RootStore {
misc: {
highlightedCode: RangeInFile | undefined;
};
+ uiState: {
+ bottomMessage: JSX.Element | undefined;
+ bottomMessageCloseTimeout: NodeJS.Timeout | undefined;
+ };
}
const store = configureStore({
@@ -39,6 +44,7 @@ const store = configureStore({
chat: chatReducer,
config: configReducer,
misc: miscReducer,
+ uiState: uiStateReducer,
},
});