summaryrefslogtreecommitdiff
path: root/extension
diff options
context:
space:
mode:
Diffstat (limited to 'extension')
-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.tsx174
-rw-r--r--extension/react-app/src/components/PillButton.tsx87
-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.ts8
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts15
-rw-r--r--extension/react-app/src/pages/gui.tsx81
-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
-rw-r--r--extension/schema/ContextItem.d.ts45
-rw-r--r--extension/schema/FullState.d.ts65
15 files changed, 471 insertions, 206 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 f327e3a3..22a6d000 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,9 @@
-import React, { useEffect, useImperativeHandle, useState } from "react";
+import React, {
+ useContext,
+ useEffect,
+ useImperativeHandle,
+ useState,
+} from "react";
import { useCombobox } from "downshift";
import styled from "styled-components";
import {
@@ -8,13 +13,15 @@ import {
vscBackground,
vscForeground,
} from ".";
-import CodeBlock from "./CodeBlock";
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
import { DocumentPlus } from "@styled-icons/heroicons-outline";
-import { HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
-import { getMetaKeyLabel } from "../util";
+import { GUIClientContext } from "../App";
+import { MeiliSearch } from "meilisearch";
+
+const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
const mainInputFontSize = 13;
@@ -64,14 +71,14 @@ const Ul = styled.ul<{
hidden: boolean;
showAbove: boolean;
ulHeightPixels: number;
+ inputBoxHeight?: string;
}>`
${(props) =>
props.showAbove
? `transform: translateY(-${props.ulHeightPixels + 8}px);`
- : `transform: translateY(${2 * mainInputFontSize}px);`}
+ : `transform: translateY(${5 * mainInputFontSize}px);`}
position: absolute;
background: ${vscBackground};
- background-color: ${secondaryDark};
color: ${vscForeground};
max-height: ${UlMaxHeight}px;
width: calc(100% - 16px);
@@ -80,15 +87,9 @@ const Ul = styled.ul<{
padding: 0;
${({ hidden }) => hidden && "display: none;"}
border-radius: ${defaultBorderRadius};
- outline: 0.5px solid gray;
+ outline: 1px solid ${lightGray};
z-index: 2;
- // Get rid of scrollbar and its padding
- scrollbar-width: none;
-ms-overflow-style: none;
- &::-webkit-scrollbar {
- width: 0px;
- background: transparent; /* make scrollbar transparent */
- }
`;
const Li = styled.li<{
@@ -96,49 +97,90 @@ const Li = styled.li<{
selected: boolean;
isLastItem: boolean;
}>`
- background-color: ${secondaryDark};
- ${({ highlighted }) => highlighted && "background: #ff000066;"}
+ background-color: ${({ highlighted }) =>
+ highlighted ? lightGray : secondaryDark};
+ ${({ highlighted }) => highlighted && `background: ${vscBackground};`}
${({ selected }) => selected && "font-weight: bold;"}
padding: 0.5rem 0.75rem;
display: flex;
flex-direction: column;
${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
- border-top: 1px solid gray;
+ /* border-top: 1px solid gray; */
cursor: pointer;
`;
// #endregion
interface ComboBoxProps {
- items: { name: string; description: string }[];
+ items: { name: string; description: string; id?: string }[];
onInputValueChange: (inputValue: string) => void;
disabled?: boolean;
onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
- highlightedCodeSections: HighlightedRangeContext[];
- deleteContextItems: (indices: number[]) => void;
- onTogglePin: () => void;
+ selectedContextItems: ContextItem[];
onToggleAddContext: () => void;
addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+ const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+ const client = useContext(GUIClientContext);
+
const [history, setHistory] = React.useState<string[]>([]);
// The position of the current command you are typing now, so the one that will be appended to history once you press enter
const [positionInHistory, setPositionInHistory] = React.useState<number>(0);
const [items, setItems] = React.useState(props.items);
- const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
- props.highlightedCodeSections || []
- );
+
const inputRef = React.useRef<HTMLInputElement>(null);
+ const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
+ undefined
+ );
- useEffect(() => {
- setHighlightedCodeSections(props.highlightedCodeSections || []);
- }, [props.highlightedCodeSections]);
+ // Whether the current input follows an '@' and should be treated as context query
+ const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
const { getInputProps, ...downshiftProps } = useCombobox({
- onInputValueChange({ inputValue }) {
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (selectedItem?.id) {
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+ const restOfInput = segs.splice(0, segs.length - 1).join("@");
+
+ // Tell server the context item was selected
+ client?.selectContextItem(selectedItem.id, query);
+
+ // Remove the '@' and the context query from the input
+ if (downshiftProps.inputValue.includes("@")) {
+ downshiftProps.setInputValue(restOfInput);
+ }
+ }
+ },
+ onInputValueChange({ inputValue, highlightedIndex }) {
if (!inputValue) return;
props.onInputValueChange(inputValue);
+
+ if (inputValue.endsWith("@") || currentlyInContextQuery) {
+ setCurrentlyInContextQuery(true);
+
+ const segs = inputValue.split("@");
+ const providerAndQuery = segs[segs.length - 1];
+ const [provider, query] = providerAndQuery.split(" ");
+ searchClient
+ .index(SEARCH_INDEX_NAME)
+ .search(providerAndQuery)
+ .then((res) => {
+ setItems(
+ res.hits.map((hit) => {
+ return {
+ name: hit.name,
+ description: hit.description,
+ id: hit.id,
+ };
+ })
+ );
+ });
+ return;
+ }
setItems(
props.items.filter((item) =>
item.name.toLowerCase().startsWith(inputValue.toLowerCase())
@@ -151,6 +193,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
},
});
+ useEffect(() => {
+ console.log(
+ "downshiftProps.highlightedIndex",
+ downshiftProps.highlightedIndex
+ );
+ if (downshiftProps.highlightedIndex < 0) {
+ downshiftProps.setHighlightedIndex(0);
+ }
+ }, [downshiftProps.inputValue]);
+
useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
@@ -199,50 +251,22 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
return (
<>
<div className="px-2 flex gap-2 items-center flex-wrap mt-2">
- {/* {highlightedCodeSections.length > 1 && (
- <>
- <HeaderButtonWithText
- text="Clear Context"
- onClick={() => {
- props.deleteContextItems(
- highlightedCodeSections.map((_, idx) => idx)
- );
- }}
- >
- <Trash size="1.6em" />
- </HeaderButtonWithText>
- </>
- )} */}
- {highlightedCodeSections.map((section, idx) => (
- <PillButton
- warning={
- section.range.contents.length > 4000 && section.editing
- ? "Editing such a large range may be slow"
- : undefined
- }
- onlyShowDelete={
- highlightedCodeSections.length <= 1 || section.editing
- }
- editing={section.editing}
- pinned={section.pinned}
- index={idx}
- key={`${section.display_name}${idx}`}
- title={`${section.display_name} (${
- section.range.range.start.line + 1
- }-${section.range.range.end.line + 1})`}
- onDelete={() => {
- if (props.deleteContextItems) {
- props.deleteContextItems([idx]);
+ {props.selectedContextItems.map((item, idx) => {
+ return (
+ <PillButton
+ key={`${item.description.id.item_id}${idx}`}
+ item={item}
+ warning={
+ item.content.length > 4000 && item.editing
+ ? "Editing such a large range may be slow"
+ : undefined
}
- setHighlightedCodeSections((prev) => {
- const newSections = [...prev];
- newSections.splice(idx, 1);
- return newSections;
- });
- }}
- />
- ))}
- {props.highlightedCodeSections.length > 0 &&
+ addingHighlightedCode={props.addingHighlightedCode}
+ index={idx}
+ />
+ );
+ })}
+ {props.selectedContextItems.length > 0 &&
(props.addingHighlightedCode ? (
<EmptyPillDiv
onClick={() => {
@@ -275,6 +299,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
target.scrollHeight,
300
).toString()}px`;
+ setInputBoxHeight(target.style.height);
// setShowContextDropdown(target.value.endsWith("@"));
},
@@ -289,6 +314,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (event.key === "Enter" && event.shiftKey) {
// Prevent Downshift's default 'Enter' behavior.
(event.nativeEvent as any).preventDownshiftDefault = true;
+ setCurrentlyInContextQuery(false);
} else if (
event.key === "Enter" &&
(!downshiftProps.isOpen || items.length === 0)
@@ -302,6 +328,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
(event.nativeEvent as any).preventDownshiftDefault = true;
if (props.onEnter) props.onEnter(event);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -321,6 +348,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}
downshiftProps.setInputValue(history[positionInHistory - 1]);
setPositionInHistory((prev) => prev - 1);
+ setCurrentlyInContextQuery(false);
} else if (event.key === "ArrowDown") {
if (positionInHistory < history.length) {
downshiftProps.setInputValue(history[positionInHistory + 1]);
@@ -328,6 +356,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setPositionInHistory((prev) =>
Math.min(prev + 1, history.length)
);
+ setCurrentlyInContextQuery(false);
}
},
ref: inputRef,
@@ -351,13 +380,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
selected={downshiftProps.selectedItem === item}
>
<span>
- {item.name}: {item.description}
+ {item.name}:{" "}
+ <span style={{ color: lightGray }}>{item.description}</span>
</span>
</Li>
))}
</Ul>
</div>
- {highlightedCodeSections.length === 0 &&
+ {props.selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
(focused &&
metaKeyPressed &&
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index c24dba83..af4263af 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,9 +1,10 @@
-import { useContext, useState } from "react";
+import { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
defaultBorderRadius,
secondaryDark,
+ vscBackground,
vscForeground,
} from ".";
import {
@@ -12,6 +13,14 @@ import {
ExclamationTriangle,
} from "@styled-icons/heroicons-outline";
import { GUIClientContext } from "../App";
+import { useDispatch } from "react-redux";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+} from "../redux/slices/uiStateSlice";
+import { ContextItem } from "../../../schema/FullState";
+import { ReactMarkdown } from "react-markdown/lib/react-markdown";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
const Button = styled.button`
border: none;
@@ -67,19 +76,48 @@ const CircleDiv = styled.div`
interface PillButtonProps {
onHover?: (arg0: boolean) => void;
- onDelete?: () => void;
- title: string;
- index: number;
- editing: boolean;
- pinned: boolean;
+ item: ContextItem;
warning?: string;
- onlyShowDelete?: boolean;
+ index: number;
+ addingHighlightedCode?: boolean;
}
const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (isHovered) {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ dispatch(
+ setBottomMessage(
+ <>
+ <b>{props.item.description.name}</b>:{" "}
+ {props.item.description.description}
+ <StyledMarkdownPreview
+ source={`\`\`\`\n${props.item.content}\n\`\`\``}
+ wrapperElement={{
+ "data-color-mode": "dark",
+ }}
+ />
+ </>
+ )
+ );
+ } else {
+ dispatch(
+ setBottomMessageCloseTimeout(
+ setTimeout(() => {
+ if (!isHovered) {
+ dispatch(setBottomMessage(undefined));
+ }
+ }, 2000)
+ )
+ );
+ }
+ }, [isHovered]);
+
return (
<>
<div style={{ position: "relative" }}>
@@ -88,10 +126,8 @@ const PillButton = (props: PillButtonProps) => {
position: "relative",
borderColor: props.warning
? "red"
- : props.editing
+ : props.item.editing
? "#8800aa"
- : props.pinned
- ? "#ffff0099"
: "transparent",
borderWidth: "1px",
borderStyle: "solid",
@@ -112,10 +148,14 @@ const PillButton = (props: PillButtonProps) => {
{isHovered && (
<GridDiv
style={{
- gridTemplateColumns: props.onlyShowDelete ? "1fr" : "1fr 1fr",
+ gridTemplateColumns:
+ props.item.editable && props.addingHighlightedCode
+ ? "1fr 1fr"
+ : "1fr",
+ backgroundColor: vscBackground,
}}
>
- {props.onlyShowDelete || (
+ {props.item.editable && props.addingHighlightedCode && (
<ButtonDiv
data-tooltip-id={`edit-${props.index}`}
backgroundColor={"#8800aa55"}
@@ -130,15 +170,6 @@ const PillButton = (props: PillButtonProps) => {
</ButtonDiv>
)}
- {/* <ButtonDiv
- data-tooltip-id={`pin-${props.index}`}
- backgroundColor={"#ffff0055"}
- onClick={() => {
- client?.setPinnedAtIndices([props.index]);
- }}
- >
- <MapPin style={{ margin: "auto" }} width="1.6em"></MapPin>
- </ButtonDiv> */}
<StyledTooltip id={`pin-${props.index}`}>
Edit this range
</StyledTooltip>
@@ -146,33 +177,33 @@ const PillButton = (props: PillButtonProps) => {
data-tooltip-id={`delete-${props.index}`}
backgroundColor={"#cc000055"}
onClick={() => {
- if (props.onDelete) {
- props.onDelete();
- }
+ client?.deleteContextWithIds([props.item.description.id]);
}}
>
<Trash style={{ margin: "auto" }} width="1.6em"></Trash>
</ButtonDiv>
</GridDiv>
)}
- {props.title}
+ {props.item.description.name}
</Button>
<StyledTooltip id={`edit-${props.index}`}>
- {props.editing
+ {props.item.editing
? "Editing this section (with entire file as context)"
: "Edit this section"}
</StyledTooltip>
<StyledTooltip id={`delete-${props.index}`}>Delete</StyledTooltip>
{props.warning && (
<>
- <CircleDiv data-tooltip-id={`circle-div-${props.title}`}>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ >
<ExclamationTriangle
style={{ margin: "auto" }}
width="1.0em"
strokeWidth={2}
/>
</CircleDiv>
- <StyledTooltip id={`circle-div-${props.title}`}>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
{props.warning}
</StyledTooltip>
</>
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index bc8665fd..2cfe7ecd 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -18,9 +18,9 @@ import {
import { StopCircle } from "@styled-icons/heroicons-solid";
import { HistoryNode } from "../../../schema/HistoryNode";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import MarkdownPreview from "@uiw/react-markdown-preview";
import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../util";
import { GUIClientContext } from "../App";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
interface StepContainerProps {
historyNode: HistoryNode;
@@ -109,33 +109,6 @@ const GradientBorder = styled.div<{
background-size: 200% 200%;
`;
-const StyledMarkdownPreview = styled(MarkdownPreview)`
- pre {
- background-color: ${secondaryDark};
- padding: 1px;
- border-radius: ${defaultBorderRadius};
- border: 0.5px solid white;
- }
-
- code {
- color: #f78383;
- word-wrap: break-word;
- border-radius: ${defaultBorderRadius};
- background-color: ${secondaryDark};
- }
-
- pre > code {
- background-color: ${secondaryDark};
- color: ${vscForeground};
- }
-
- background-color: ${vscBackground};
- font-family: "Lexend", sans-serif;
- font-size: 13px;
- padding: 8px;
- color: ${vscForeground};
-`;
-
// #endregion
function StepContainer(props: StepContainerProps) {
diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx
new file mode 100644
index 00000000..9c2ecb62
--- /dev/null
+++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx
@@ -0,0 +1,37 @@
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ secondaryDark,
+ vscBackground,
+ vscForeground,
+} from ".";
+import MarkdownPreview from "@uiw/react-markdown-preview";
+
+const StyledMarkdownPreview = styled(MarkdownPreview)`
+ pre {
+ background-color: ${secondaryDark};
+ padding: 1px;
+ border-radius: ${defaultBorderRadius};
+ border: 0.5px solid white;
+ }
+
+ code {
+ color: #f78383;
+ word-wrap: break-word;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${secondaryDark};
+ }
+
+ pre > code {
+ background-color: ${secondaryDark};
+ color: ${vscForeground};
+ }
+
+ background-color: ${vscBackground};
+ font-family: "Lexend", sans-serif;
+ font-size: 13px;
+ padding: 8px;
+ color: ${vscForeground};
+`;
+
+export default StyledMarkdownPreview;
diff --git a/extension/react-app/src/components/TextDialog.tsx b/extension/react-app/src/components/TextDialog.tsx
index cba3852d..7c6ba052 100644
--- a/extension/react-app/src/components/TextDialog.tsx
+++ b/extension/react-app/src/components/TextDialog.tsx
@@ -5,7 +5,7 @@ import { Button, secondaryDark, vscBackground, vscForeground } from ".";
import { isMetaEquivalentKeyPressed } from "../util";
const ScreenCover = styled.div`
- position: absolute;
+ position: fixed;
width: 100%;
height: 100%;
background-color: rgba(168, 168, 168, 0.5);
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index 6c0df8fc..ddf65272 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -1,3 +1,5 @@
+import { ContextItemId } from "../../../schema/FullState";
+
abstract class AbstractContinueGUIClientProtocol {
abstract sendMainInput(input: string): void;
@@ -21,15 +23,15 @@ abstract class AbstractContinueGUIClientProtocol {
abstract deleteAtIndex(index: number): void;
- abstract deleteContextAtIndices(indices: number[]): void;
+ abstract deleteContextWithIds(ids: ContextItemId[]): void;
abstract setEditingAtIndices(indices: number[]): void;
- abstract setPinnedAtIndices(indices: number[]): void;
-
abstract toggleAddingHighlightedCode(): void;
abstract showLogsAtIndex(index: number): void;
+
+ abstract selectContextItem(id: string, query: string): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 7d6c2a71..1048e956 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -1,3 +1,4 @@
+import { ContextItemId } from "../../../schema/FullState";
import AbstractContinueGUIClientProtocol from "./AbstractContinueGUIClientProtocol";
import { Messenger, WebsocketMessenger } from "./messenger";
import { VscodeMessenger } from "./vscodeMessenger";
@@ -68,18 +69,16 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger.send("delete_at_index", { index });
}
- deleteContextAtIndices(indices: number[]) {
- this.messenger.send("delete_context_at_indices", { indices });
+ deleteContextWithIds(ids: ContextItemId[]) {
+ this.messenger.send("delete_context_with_ids", {
+ ids: ids.map((id) => `${id.provider_title}-${id.item_id}`),
+ });
}
setEditingAtIndices(indices: number[]) {
this.messenger.send("set_editing_at_indices", { indices });
}
- setPinnedAtIndices(indices: number[]) {
- this.messenger.send("set_pinned_at_indices", { indices });
- }
-
toggleAddingHighlightedCode(): void {
this.messenger.send("toggle_adding_highlighted_code", {});
}
@@ -87,6 +86,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
showLogsAtIndex(index: number): void {
this.messenger.send("show_logs_at_index", { index });
}
+
+ selectContextItem(id: string, query: string): void {
+ this.messenger.send("select_context_item", { id, query });
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index fccc9b4b..a1ba1c33 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -6,7 +6,7 @@ import {
} from "../components";
import Loader from "../components/Loader";
import ContinueButton from "../components/ContinueButton";
-import { FullState, HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem, FullState } from "../../../schema/FullState";
import { useCallback, useEffect, useRef, useState, useContext } from "react";
import { History } from "../../../schema/History";
import { HistoryNode } from "../../../schema/HistoryNode";
@@ -22,12 +22,16 @@ import TextDialog from "../components/TextDialog";
import HeaderButtonWithText from "../components/HeaderButtonWithText";
import ReactSwitch from "react-switch";
import { usePostHog } from "posthog-js/react";
-import { useSelector } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { postVscMessage } from "../vscode";
import UserInputContainer from "../components/UserInputContainer";
import Onboarding from "../components/Onboarding";
import { isMetaEquivalentKeyPressed } from "../util";
+import {
+ setBottomMessage,
+ setBottomMessageCloseTimeout,
+} from "../redux/slices/uiStateSlice";
const TopGUIDiv = styled.div`
overflow: hidden;
@@ -78,15 +82,13 @@ function GUI(props: GUIProps) {
const [usingFastModel, setUsingFastModel] = useState(false);
const [waitingForSteps, setWaitingForSteps] = useState(false);
const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
- const [highlightedRanges, setHighlightedRanges] = useState<
- HighlightedRangeContext[]
- >([]);
const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
+ const [selectedContextItems, setSelectedContextItems] = useState<
+ ContextItem[]
+ >([]);
const [availableSlashCommands, setAvailableSlashCommands] = useState<
{ name: string; description: string }[]
>([]);
- const [pinned, setPinned] = useState(false);
- const [showDataSharingInfo, setShowDataSharingInfo] = useState(false);
const [stepsOpen, setStepsOpen] = useState<boolean[]>([
true,
true,
@@ -118,6 +120,11 @@ function GUI(props: GUIProps) {
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
const [feedbackDialogMessage, setFeedbackDialogMessage] = useState("");
+ const dispatch = useDispatch();
+ const bottomMessage = useSelector(
+ (state: RootStore) => state.uiState.bottomMessage
+ );
+
const topGuiDivRef = useRef<HTMLDivElement>(null);
const [scrollTimeout, setScrollTimeout] = useState<NodeJS.Timeout | null>(
@@ -179,7 +186,8 @@ function GUI(props: GUIProps) {
setWaitingForSteps(waitingForSteps);
setHistory(state.history);
- setHighlightedRanges(state.highlighted_ranges);
+ console.log((state as any).selected_context_items);
+ setSelectedContextItems(state.selected_context_items);
setUserInputQueue(state.user_input_queue);
setAddingHighlightedCode(state.adding_highlighted_code);
setAvailableSlashCommands(
@@ -214,13 +222,6 @@ function GUI(props: GUIProps) {
const mainTextInputRef = useRef<HTMLInputElement>(null);
- const deleteContextItems = useCallback(
- (indices: number[]) => {
- client?.deleteContextAtIndices(indices);
- },
- [client]
- );
-
const onMainTextInput = (event?: any) => {
if (mainTextInputRef.current) {
let input = (mainTextInputRef.current as any).inputValue;
@@ -360,11 +361,7 @@ function GUI(props: GUIProps) {
}}
onInputValueChange={() => {}}
items={availableSlashCommands}
- highlightedCodeSections={highlightedRanges}
- deleteContextItems={deleteContextItems}
- onTogglePin={() => {
- setPinned((prev: boolean) => !prev);
- }}
+ selectedContextItems={selectedContextItems}
onToggleAddContext={() => {
client?.toggleAddingHighlightedCode();
}}
@@ -373,29 +370,30 @@ function GUI(props: GUIProps) {
<ContinueButton onClick={onMainTextInput} />
</TopGUIDiv>
<div
+ onMouseEnter={() => {
+ dispatch(setBottomMessageCloseTimeout(undefined));
+ }}
+ onMouseLeave={() => {
+ dispatch(setBottomMessage(undefined));
+ }}
style={{
position: "fixed",
bottom: "50px",
+ left: "0",
+ right: "0",
+ margin: "16px",
backgroundColor: vscBackground,
color: vscForeground,
borderRadius: defaultBorderRadius,
padding: "16px",
- margin: "16px",
zIndex: 100,
boxShadow: `0px 0px 10px 0px ${vscForeground}`,
+ maxHeight: "50vh",
+ overflow: "scroll",
}}
- hidden={!showDataSharingInfo}
+ hidden={!bottomMessage}
>
- By turning on this switch, you will begin collecting accepted and
- rejected suggestions in .continue/suggestions.json. This data is stored
- locally on your machine and not sent anywhere.
- <br />
- <br />
- <b>
- {dataSwitchChecked
- ? "👍 Data is being collected"
- : "👎 No data is being collected"}
- </b>
+ {bottomMessage}
</div>
<Footer dataSwitchChecked={dataSwitchChecked}>
<div
@@ -406,10 +404,25 @@ function GUI(props: GUIProps) {
alignItems: "center",
}}
onMouseEnter={() => {
- setShowDataSharingInfo(true);
+ dispatch(
+ setBottomMessage(
+ <>
+ By turning on this switch, you will begin collecting accepted
+ and rejected suggestions in .continue/suggestions.json. This
+ data is stored locally on your machine and not sent anywhere.
+ <br />
+ <br />
+ <b>
+ {dataSwitchChecked
+ ? "👍 Data is being collected"
+ : "👎 No data is being collected"}
+ </b>
+ </>
+ )
+ );
}}
onMouseLeave={() => {
- setShowDataSharingInfo(false);
+ dispatch(setBottomMessage(undefined));
}}
>
<ReactSwitch
diff --git a/extension/react-app/src/redux/selectors/uiStateSelectors.ts b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
new file mode 100644
index 00000000..7ebc9338
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/uiStateSelectors.ts
@@ -0,0 +1,5 @@
+import { RootStore } from "../store";
+
+const selectBottomMessage = (state: RootStore) => state.uiState.bottomMessage;
+
+export { selectBottomMessage };
diff --git a/extension/react-app/src/redux/slices/uiStateSlice.ts b/extension/react-app/src/redux/slices/uiStateSlice.ts
new file mode 100644
index 00000000..837d19e9
--- /dev/null
+++ b/extension/react-app/src/redux/slices/uiStateSlice.ts
@@ -0,0 +1,24 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+export const uiStateSlice = createSlice({
+ name: "uiState",
+ initialState: {
+ bottomMessage: undefined,
+ bottomMessageCloseTimeout: undefined,
+ },
+ reducers: {
+ setBottomMessage: (state, action) => {
+ state.bottomMessage = action.payload;
+ },
+ setBottomMessageCloseTimeout: (state, action) => {
+ if (state.bottomMessageCloseTimeout) {
+ clearTimeout(state.bottomMessageCloseTimeout);
+ }
+ state.bottomMessageCloseTimeout = action.payload;
+ },
+ },
+});
+
+export const { setBottomMessage, setBottomMessageCloseTimeout } =
+ uiStateSlice.actions;
+export default uiStateSlice.reducer;
diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts
index b6eb55b3..d49513e5 100644
--- a/extension/react-app/src/redux/store.ts
+++ b/extension/react-app/src/redux/store.ts
@@ -3,6 +3,7 @@ import debugStateReducer from "./slices/debugContexSlice";
import chatReducer from "./slices/chatSlice";
import configReducer from "./slices/configSlice";
import miscReducer from "./slices/miscSlice";
+import uiStateReducer from "./slices/uiStateSlice";
import { RangeInFile, SerializedDebugContext } from "../../../src/client";
export interface ChatMessage {
@@ -31,6 +32,10 @@ export interface RootStore {
misc: {
highlightedCode: RangeInFile | undefined;
};
+ uiState: {
+ bottomMessage: JSX.Element | undefined;
+ bottomMessageCloseTimeout: NodeJS.Timeout | undefined;
+ };
}
const store = configureStore({
@@ -39,6 +44,7 @@ const store = configureStore({
chat: chatReducer,
config: configReducer,
misc: miscReducer,
+ uiState: uiStateReducer,
},
});
diff --git a/extension/schema/ContextItem.d.ts b/extension/schema/ContextItem.d.ts
new file mode 100644
index 00000000..62d2c32f
--- /dev/null
+++ b/extension/schema/ContextItem.d.ts
@@ -0,0 +1,45 @@
+/* eslint-disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type ContextItem = ContextItem1;
+export type Name = string;
+export type Description = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content = string;
+export type Editing = boolean;
+export type Editable = boolean;
+
+/**
+ * A ContextItem is a single item that is stored in the ContextManager.
+ */
+export interface ContextItem1 {
+ description: ContextItemDescription;
+ content: Content;
+ editing?: Editing;
+ editable?: Editable;
+ [k: string]: unknown;
+}
+/**
+ * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
+ *
+ * The id can be used to retrieve the ContextItem from the ContextManager.
+ */
+export interface ContextItemDescription {
+ name: Name;
+ description: Description;
+ id: ContextItemId;
+ [k: string]: unknown;
+}
+/**
+ * A ContextItemId is a unique identifier for a ContextItem.
+ */
+export interface ContextItemId {
+ provider_title: ProviderTitle;
+ item_id: ItemId;
+ [k: string]: unknown;
+}
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
index 1b7b1f3b..0095f41b 100644
--- a/extension/schema/FullState.d.ts
+++ b/extension/schema/FullState.d.ts
@@ -27,18 +27,18 @@ export type CurrentIndex = number;
export type Active1 = boolean;
export type UserInputQueue = string[];
export type DefaultModel = string;
-export type Filepath = string;
-export type Line = number;
-export type Character = number;
-export type Contents = string;
-export type Editing = boolean;
-export type Pinned = boolean;
-export type DisplayName = string;
-export type HighlightedRanges = HighlightedRangeContext[];
export type Name3 = string;
export type Description1 = string;
export type SlashCommands = SlashCommandDescription[];
export type AddingHighlightedCode = boolean;
+export type Name4 = string;
+export type Description2 = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content1 = string;
+export type Editing = boolean;
+export type Editable = boolean;
+export type SelectedContextItems = ContextItem[];
/**
* A full state of the program, including the history
@@ -48,9 +48,9 @@ export interface FullState1 {
active: Active1;
user_input_queue: UserInputQueue;
default_model: DefaultModel;
- highlighted_ranges: HighlightedRanges;
slash_commands: SlashCommands;
adding_highlighted_code: AddingHighlightedCode;
+ selected_context_items: SelectedContextItems;
[k: string]: unknown;
}
/**
@@ -98,40 +98,37 @@ export interface FunctionCall {
export interface Observation {
[k: string]: unknown;
}
-/**
- * Context for a highlighted range
- */
-export interface HighlightedRangeContext {
- range: RangeInFileWithContents;
- editing: Editing;
- pinned: Pinned;
- display_name: DisplayName;
+export interface SlashCommandDescription {
+ name: Name3;
+ description: Description1;
[k: string]: unknown;
}
/**
- * A range in a file with the contents of the range.
+ * A ContextItem is a single item that is stored in the ContextManager.
*/
-export interface RangeInFileWithContents {
- filepath: Filepath;
- range: Range;
- contents: Contents;
+export interface ContextItem {
+ description: ContextItemDescription;
+ content: Content1;
+ editing?: Editing;
+ editable?: Editable;
[k: string]: unknown;
}
/**
- * A range in a file. 0-indexed.
+ * A ContextItemDescription is a description of a ContextItem that is displayed to the user when they type '@'.
+ *
+ * The id can be used to retrieve the ContextItem from the ContextManager.
*/
-export interface Range {
- start: Position;
- end: Position;
+export interface ContextItemDescription {
+ name: Name4;
+ description: Description2;
+ id: ContextItemId;
[k: string]: unknown;
}
-export interface Position {
- line: Line;
- character: Character;
- [k: string]: unknown;
-}
-export interface SlashCommandDescription {
- name: Name3;
- description: Description1;
+/**
+ * A ContextItemId is a unique identifier for a ContextItem.
+ */
+export interface ContextItemId {
+ provider_title: ProviderTitle;
+ item_id: ItemId;
[k: string]: unknown;
}