summaryrefslogtreecommitdiff
path: root/extension
diff options
context:
space:
mode:
authorNate Sesti <33237525+sestinj@users.noreply.github.com>2023-09-28 01:02:52 -0700
committerGitHub <noreply@github.com>2023-09-28 01:02:52 -0700
commit95363a5b52f3bf73531ac76b00178fa79ca97661 (patch)
tree9b9c1614556f1f0d21f363e6a9fe950069affb5d /extension
parentd4acf4bb11dbd7d3d6210e2949d21143d721e81e (diff)
downloadsncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.tar.gz
sncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.tar.bz2
sncontinue-95363a5b52f3bf73531ac76b00178fa79ca97661.zip
Past input (#513)
* feat: :construction: use ComboBox in place of UserInputContainer * feat: :construction: adding context to previous inputs steps * feat: :sparkles: preview context items on click * feat: :construction: more work on context items ui * style: :construction: working out the details of ctx item buttons * feat: :sparkles: getting the final details * fix: :bug: fix height of ctx items bar * fix: :bug: last couple of details * fix: :bug: pass model param through to hf inference api * fix: :loud_sound: better logging for timeout * feat: :sparkles: option to set the meilisearch url * fix: :bug: fix height of past inputs
Diffstat (limited to 'extension')
-rw-r--r--extension/package.json2
-rw-r--r--extension/react-app/src/components/ComboBox.tsx776
-rw-r--r--extension/react-app/src/components/ErrorStepContainer.tsx5
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx2
-rw-r--r--extension/react-app/src/components/PillButton.tsx257
-rw-r--r--extension/react-app/src/components/StepContainer.tsx6
-rw-r--r--extension/react-app/src/components/StyledMarkdownPreview.tsx12
-rw-r--r--extension/react-app/src/components/Suggestions.tsx2
-rw-r--r--extension/react-app/src/components/TimelineItem.tsx2
-rw-r--r--extension/react-app/src/components/UserInputContainer.tsx1
-rw-r--r--extension/react-app/src/components/index.ts7
-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.tsx32
-rw-r--r--extension/react-app/src/redux/slices/serverStateReducer.ts16
-rw-r--r--extension/react-app/src/util/index.ts54
-rw-r--r--extension/schema/ContinueConfig.d.ts13
-rw-r--r--extension/schema/FullState.d.ts34
-rw-r--r--extension/schema/History.d.ts38
-rw-r--r--extension/schema/HistoryNode.d.ts38
-rw-r--r--extension/schema/LLM.d.ts5
-rw-r--r--extension/schema/Models.d.ts13
-rw-r--r--extension/src/continueIdeClient.ts3
23 files changed, 955 insertions, 386 deletions
diff --git a/extension/package.json b/extension/package.json
index 3792146d..4cf39677 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -51,7 +51,7 @@
"continue.serverUrl": {
"type": "string",
"default": "http://localhost:65432",
- "description": "The URL of the Continue server. Only change this if you are running the server manually. If you want to use an LLM hosted at a custom URL, please see https://continue.dev/docs/customization#change-the-default-llm. All other configuration is done in `~/.continue/config.py`, which you can access by using the '/config' slash command."
+ "description": "The URL of the Continue server if you are running Continue manually. NOTE: This is NOT the URL of the LLM server. If you want to use an LLM hosted at a custom URL, please see https://continue.dev/docs/customization#change-the-default-llm and complete configuration in `~/.continue/config.py`, which you can access by using the '/config' slash command."
},
"continue.manuallyRunningServer": {
"type": "boolean",
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 934b7337..6c99a650 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -7,7 +7,7 @@ import React, {
useState,
} from "react";
import { useCombobox } from "downshift";
-import styled from "styled-components";
+import styled, { keyframes } from "styled-components";
import {
buttonColor,
defaultBorderRadius,
@@ -21,8 +21,10 @@ import HeaderButtonWithText from "./HeaderButtonWithText";
import {
ArrowLeftIcon,
ArrowRightIcon,
- MagnifyingGlassIcon,
+ ArrowUpLeftIcon,
+ StopCircleIcon,
TrashIcon,
+ XMarkIcon,
} from "@heroicons/react/24/outline";
import { postVscMessage } from "../vscode";
import { GUIClientContext } from "../App";
@@ -31,12 +33,58 @@ import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import ContinueButton from "./ContinueButton";
-import { getFontSize } from "../util";
+import {
+ getFontSize,
+ getMarkdownLanguageTagForFile,
+ getMetaKeyLabel,
+} from "../util";
+import { ContextItem } from "../../../schema/FullState";
+import StyledMarkdownPreview from "./StyledMarkdownPreview";
const SEARCH_INDEX_NAME = "continue_context_items";
// #region styled components
+const gradient = keyframes`
+ 0% {
+ background-position: 0px 0;
+ }
+ 100% {
+ background-position: 100em 0;
+ }
+`;
+
+const GradientBorder = styled.div<{
+ borderRadius?: string;
+ borderColor?: string;
+ isFirst: boolean;
+ isLast: boolean;
+ loading: boolean;
+}>`
+ border-radius: ${(props) => props.borderRadius || "0"};
+ padding: 1px;
+ 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%;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-top: 8px;
+`;
+
const HiddenHeaderButtonWithText = styled.button`
opacity: 0;
background-color: transparent;
@@ -75,7 +123,7 @@ const MainTextInput = styled.textarea<{
font-size: ${(props) => props.fontSize || mainInputFontSize}px;
font-family: inherit;
border-radius: ${defaultBorderRadius};
- margin: 8px auto;
+ margin: 0;
height: auto;
width: 100%;
background-color: ${secondaryDark};
@@ -98,6 +146,15 @@ const MainTextInput = styled.textarea<{
}
`;
+const DeleteButtonDiv = styled.div`
+ position: absolute;
+ top: 14px;
+ right: 12px;
+ background-color: ${secondaryDark};
+ border-radius: ${defaultBorderRadius};
+ z-index: 100;
+`;
+
const DynamicQueryTitleDiv = styled.div`
position: absolute;
right: 0px;
@@ -119,12 +176,14 @@ const Ul = styled.ul<{
ulHeightPixels: number;
inputBoxHeight?: string;
fontSize?: number;
+ isMainInput: boolean;
}>`
${(props) =>
props.showAbove
? `transform: translateY(-${props.ulHeightPixels + 8}px);`
: `transform: translateY(${
- 5 * (props.fontSize || mainInputFontSize) - 2
+ (props.isMainInput ? 5 : 4) * (props.fontSize || mainInputFontSize) -
+ (props.isMainInput ? 2 : 4)
}px);`}
position: absolute;
background: ${vscBackground};
@@ -137,11 +196,11 @@ const Ul = styled.ul<{
${({ hidden }) => hidden && "display: none;"}
border-radius: ${defaultBorderRadius};
outline: 0.5px solid ${lightGray};
- z-index: 2;
-ms-overflow-style: none;
font-size: ${(props) => props.fontSize || mainInputFontSize}px;
scrollbar-width: none; /* Firefox */
+ z-index: 500;
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
@@ -165,6 +224,7 @@ const Li = styled.li<{
${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
/* border-top: 1px solid gray; */
cursor: pointer;
+ z-index: 500;
`;
// #endregion
@@ -176,14 +236,39 @@ interface ComboBoxItem {
content?: string;
}
interface ComboBoxProps {
- onInputValueChange: (inputValue: string) => void;
+ onInputValueChange?: (inputValue: string) => void;
disabled?: boolean;
- onEnter: (e?: React.KeyboardEvent<HTMLInputElement>) => void;
- onToggleAddContext: () => void;
+ onEnter?: (e?: React.KeyboardEvent<HTMLInputElement>, value?: string) => void;
+ onToggleAddContext?: () => void;
+
+ isMainInput: boolean;
+ value?: string;
+ active?: boolean;
+ groupIndices?: number[];
+ onToggle?: (arg0: boolean) => void;
+ onToggleAll?: (arg0: boolean) => void;
+ isToggleOpen?: boolean;
+ index?: number;
+ onDelete?: () => void;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
- const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+ const meilisearchUrl = useSelector(
+ (state: RootStore) =>
+ state.serverState.meilisearch_url || "http://127.0.0.1:7700"
+ );
+
+ const [searchClient, setSearchClient] = useState<MeiliSearch | undefined>(
+ undefined
+ );
+
+ useEffect(() => {
+ const client = new MeiliSearch({
+ host: meilisearchUrl,
+ });
+ setSearchClient(client);
+ }, [meilisearchUrl]);
+
const client = useContext(GUIClientContext);
const dispatch = useDispatch();
const workspacePaths = useSelector(
@@ -197,6 +282,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
+ useEffect(() => {
+ if (!inputRef.current) return;
+ if (inputRef.current.scrollHeight > inputRef.current.clientHeight) {
+ inputRef.current.style.height = "auto";
+ inputRef.current.style.height = inputRef.current.scrollHeight + "px";
+ }
+ }, [inputRef.current, props.value]);
+
// Whether the current input follows an '@' and should be treated as context query
const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
const [nestedContextProvider, setNestedContextProvider] = useState<
@@ -206,9 +299,6 @@ 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) => {
@@ -217,15 +307,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
description: cmd.description,
};
});
- const selectedContextItems = useSelector(
- (state: RootStore) => state.serverState.selected_context_items
- );
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
+ const selectedContextItems = useSelector((state: RootStore) => {
+ if (props.index) {
+ return state.serverState.history.timeline[props.index].context_used || [];
+ } else {
+ return state.serverState.selected_context_items;
}
- }, [sessionId, inputRef.current]);
+ });
+ const timeline = useSelector(
+ (state: RootStore) => state.serverState.history.timeline
+ );
useEffect(() => {
if (!currentlyInContextQuery) {
@@ -287,7 +378,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
setInQueryForContextProvider(undefined);
}
- props.onInputValueChange(inputValue);
+ props.onInputValueChange?.(inputValue);
// Handle context selection
if (inputValue.endsWith("@") || currentlyInContextQuery) {
@@ -365,7 +456,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
.join(", ")} ] AND provider_name = '${provider}'`
: undefined;
try {
- const res = await searchClient.index(SEARCH_INDEX_NAME).search(query, {
+ const res = await searchClient?.index(SEARCH_INDEX_NAME).search(query, {
filter: workspaceFilter,
});
return (
@@ -410,13 +501,15 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
const contextItemsDivRef = React.useRef<HTMLDivElement>(null);
- const handleTabPressed = () => {
+ const handleTabPressed = useCallback(() => {
+ setShowContextItemsIfNotMain(true);
// Set the focus to the next item in the context items div
if (!contextItemsDivRef.current) {
return;
}
- const focusableItems =
- contextItemsDivRef.current.querySelectorAll(".pill-button");
+ const focusableItems = contextItemsDivRef.current.querySelectorAll(
+ `.pill-button-${props.index || "main"}`
+ );
const focusableItemsArray = Array.from(focusableItems);
const focusedItemIndex = focusableItemsArray.findIndex(
(item) => item === document.activeElement
@@ -433,22 +526,30 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const firstItem = focusableItemsArray[0];
(firstItem as any)?.focus();
}
- };
+ }, [props.index]);
useEffect(() => {
- if (typeof window !== "undefined") {
+ if (inputRef.current) {
const listener = (e: any) => {
if (e.key === "Tab") {
e.preventDefault();
handleTabPressed();
}
};
- window.addEventListener("keydown", listener);
+ inputRef.current.addEventListener("keydown", listener);
return () => {
- window.removeEventListener("keydown", listener);
+ inputRef.current?.removeEventListener("keydown", listener);
};
}
- }, []);
+ }, [inputRef.current]);
+
+ useEffect(() => {
+ if (props.value) {
+ downshiftProps.setInputValue(props.value);
+ }
+ }, [props.value, downshiftProps.setInputValue]);
+
+ const [isHovered, setIsHovered] = useState(false);
useLayoutEffect(() => {
if (!ulRef.current) {
@@ -458,7 +559,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}, [items, downshiftProps.setHighlightedIndex, ulRef.current]);
const [metaKeyPressed, setMetaKeyPressed] = useState(false);
- const [focused, setFocused] = useState(false);
+ const [inputFocused, setInputFocused] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Meta") {
@@ -479,10 +580,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
}, []);
useEffect(() => {
- if (!inputRef.current) {
+ if (!inputRef.current || !props.isMainInput) {
return;
}
- inputRef.current.focus();
+ if (props.isMainInput) {
+ inputRef.current.focus();
+ }
const handler = (event: any) => {
if (event.data.type === "focusContinueInput") {
inputRef.current!.focus();
@@ -498,7 +601,20 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
return () => {
window.removeEventListener("message", handler);
};
- }, [inputRef.current]);
+ }, [inputRef.current, props.isMainInput]);
+
+ const deleteButtonDivRef = React.useRef<HTMLDivElement>(null);
+
+ const selectContextItem = useCallback(
+ (id: string, query: string) => {
+ if (props.isMainInput) {
+ client?.selectContextItem(id, query);
+ } else if (props.index) {
+ client?.selectContextItemAtIndex(id, query, props.index);
+ }
+ },
+ [client, props.index]
+ );
const selectContextItemFromDropdown = useCallback(
(event: any) => {
@@ -511,7 +627,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
if (!newProvider) {
if (nestedContextProvider && newItem.id) {
// Tell server the context item was selected
- client?.selectContextItem(newItem.id, "");
+ selectContextItem(newItem.id, "");
// Clear the input
downshiftProps.setInputValue("");
@@ -542,7 +658,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const query = segs[segs.length - 1];
// Tell server the context item was selected
- client?.selectContextItem(newItem.id, query);
+ selectContextItem(newItem.id, query);
if (downshiftProps.inputValue.includes("@")) {
const selectedNestedContextProvider = contextProviders.find(
(provider) => provider.title === newItem.id
@@ -582,221 +698,428 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
contextProviders,
nestedContextProvider,
downshiftProps.inputValue,
+ selectContextItem,
]
);
const [isComposing, setIsComposing] = useState(false);
+ const [previewingContextItem, setPreviewingContextItem] = useState<
+ ContextItem | undefined
+ >(undefined);
+
+ const [focusedContextItem, setFocusedContextItem] = useState<
+ ContextItem | undefined
+ >(undefined);
+
+ const topRef = React.useRef<HTMLDivElement>(null);
+
+ const [showContextItemsIfNotMain, setShowContextItemsIfNotMain] =
+ useState(false);
+
+ useEffect(() => {
+ if (!inputFocused) {
+ setShowContextItemsIfNotMain(false);
+ }
+ }, [inputFocused]);
+
return (
- <>
- <div
- className="px-2 flex gap-2 items-center flex-wrap mt-2"
- ref={contextItemsDivRef}
- >
- <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") {
+ <div ref={topRef}>
+ {props.isMainInput ||
+ (selectedContextItems.length > 0 && showContextItemsIfNotMain) ? (
+ <div
+ className="px-2 flex gap-2 items-center flex-wrap"
+ ref={contextItemsDivRef}
+ style={{ backgroundColor: vscBackground }}
+ >
+ <HiddenHeaderButtonWithText
+ className={
+ selectedContextItems.length > 0
+ ? `pill-button-${props.index || "main"}`
+ : ""
+ }
+ onClick={() => {
client?.deleteContextWithIds(
- selectedContextItems.map((item) => item.description.id)
+ selectedContextItems.map((item) => item.description.id),
+ props.index
);
inputRef.current?.focus();
- }
- }}
- >
- <TrashIcon width="1.4em" height="1.4em" />
- </HiddenHeaderButtonWithText>
- {selectedContextItems.map((item, idx) => {
- return (
- <PillButton
- areMultipleItems={selectedContextItems.length > 1}
- key={`${item.description.id.item_id}${idx}`}
- item={item}
- editing={
- item.editing &&
- (inputRef.current as any)?.value?.startsWith("/edit")
- }
- editingAny={(inputRef.current as any)?.value?.startsWith("/edit")}
- index={idx}
- onDelete={() => {
- client?.deleteContextWithIds([item.description.id]);
+ }}
+ onKeyDown={(e: any) => {
+ if (e.key === "Backspace") {
+ client?.deleteContextWithIds(
+ selectedContextItems.map((item) => item.description.id),
+ props.index
+ );
inputRef.current?.focus();
- }}
- />
- );
- })}
+ setPreviewingContextItem(undefined);
+ setFocusedContextItem(undefined);
+ }
+ }}
+ >
+ <TrashIcon width="1.4em" height="1.4em" />
+ </HiddenHeaderButtonWithText>
+ {(props.isMainInput
+ ? selectedContextItems
+ : timeline[props.index!].context_used || []
+ ).map((item, idx) => {
+ return (
+ <PillButton
+ areMultipleItems={selectedContextItems.length > 1}
+ key={`${item.description.id.item_id}${idx}`}
+ item={item}
+ editing={
+ item.editing &&
+ (inputRef.current as any)?.value?.startsWith("/edit")
+ }
+ editingAny={(inputRef.current as any)?.value?.startsWith(
+ "/edit"
+ )}
+ stepIndex={props.index}
+ index={idx}
+ onDelete={() => {
+ client?.deleteContextWithIds(
+ [item.description.id],
+ props.index
+ );
+ inputRef.current?.focus();
+ if (
+ (item.description.id.item_id ===
+ focusedContextItem?.description.id.item_id &&
+ focusedContextItem?.description.id.provider_name ===
+ item.description.id.provider_name) ||
+ (item.description.id.item_id ===
+ previewingContextItem?.description.id.item_id &&
+ previewingContextItem?.description.id.provider_name ===
+ item.description.id.provider_name)
+ ) {
+ setPreviewingContextItem(undefined);
+ setFocusedContextItem(undefined);
+ }
+ }}
+ onClick={(e) => {
+ if (
+ item.description.id.item_id ===
+ focusedContextItem?.description.id.item_id &&
+ focusedContextItem?.description.id.provider_name ===
+ item.description.id.provider_name
+ ) {
+ setFocusedContextItem(undefined);
+ } else {
+ setFocusedContextItem(item);
+ }
+ }}
+ onBlur={() => {
+ setFocusedContextItem(undefined);
+ }}
+ toggleViewContent={() => {
+ setPreviewingContextItem((prev) => {
+ if (!prev) return item;
+ if (
+ prev.description.id.item_id ===
+ item.description.id.item_id &&
+ prev.description.id.provider_name ===
+ item.description.id.provider_name
+ ) {
+ return undefined;
+ } else {
+ return item;
+ }
+ });
+ }}
+ previewing={
+ item.description.id.item_id ===
+ previewingContextItem?.description.id.item_id &&
+ previewingContextItem?.description.id.provider_name ===
+ item.description.id.provider_name
+ }
+ focusing={
+ item.description.id.item_id ===
+ focusedContextItem?.description.id.item_id &&
+ focusedContextItem?.description.id.provider_name ===
+ item.description.id.provider_name
+ }
+ />
+ );
+ })}
- {selectedContextItems.length > 0 && (
+ {/* {selectedContextItems.length > 0 && (
<HeaderButtonWithText
onClick={() => {
- client?.showContextVirtualFile();
+ client?.showContextVirtualFile(props.index);
}}
text="View Current Context"
>
<MagnifyingGlassIcon width="1.4em" height="1.4em" />
</HeaderButtonWithText>
- )}
- </div>
+ )} */}
+ </div>
+ ) : (
+ selectedContextItems.length > 0 && (
+ <div
+ onClick={() => {
+ inputRef.current?.focus();
+ setShowContextItemsIfNotMain(true);
+ }}
+ style={{
+ color: lightGray,
+ fontSize: "10px",
+ backgroundColor: vscBackground,
+ paddingLeft: "12px",
+ cursor: "default",
+ paddingTop: getFontSize(),
+ }}
+ >
+ {props.active ? "Using" : "Used"} {selectedContextItems.length}{" "}
+ context item
+ {selectedContextItems.length === 1 ? "" : "s"}
+ </div>
+ )
+ )}
+ {previewingContextItem && (
+ <pre className="m-0">
+ <StyledMarkdownPreview
+ fontSize={getFontSize()}
+ source={`\`\`\`${getMarkdownLanguageTagForFile(
+ previewingContextItem.description.description
+ )}\n${previewingContextItem.content}\n\`\`\``}
+ wrapperElement={{
+ "data-color-mode": "dark",
+ }}
+ maxHeight={200}
+ />
+ </pre>
+ )}
<div
className="flex px-2 relative"
+ style={{
+ backgroundColor: vscBackground,
+ }}
ref={divRef}
- hidden={!downshiftProps.isOpen}
>
- <MainTextInput
- inQueryForDynamicProvider={
- typeof inQueryForContextProvider !== "undefined"
- }
- fontSize={getFontSize()}
- disabled={props.disabled}
- placeholder={`Ask a question, '/' for slash commands, '@' to add context`}
- {...getInputProps({
- onCompositionStart: () => setIsComposing(true),
- onCompositionEnd: () => setIsComposing(false),
- onChange: (e) => {
- const target = e.target as HTMLTextAreaElement;
- // Update the height of the textarea to match the content, up to a max of 200px.
- target.style.height = "auto";
- target.style.height = `${Math.min(
- target.scrollHeight,
- 300
- ).toString()}px`;
-
- // setShowContextDropdown(target.value.endsWith("@"));
- },
- onFocus: (e) => {
- setFocused(true);
- dispatch(setBottomMessage(undefined));
- },
- onKeyDown: (event) => {
- dispatch(setBottomMessage(undefined));
- 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) &&
- !isComposing
+ <GradientBorder
+ loading={props.active || false}
+ isFirst={false}
+ isLast={false}
+ borderColor={props.active ? undefined : vscBackground}
+ borderRadius={defaultBorderRadius}
+ >
+ <MainTextInput
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={(e) => {
+ console.log("left");
+ if (
+ e.relatedTarget === deleteButtonDivRef.current ||
+ deleteButtonDivRef.current?.contains(e.relatedTarget as Node)
) {
- const value = downshiftProps.inputValue;
- if (inQueryForContextProvider) {
- const segs = value.split("@");
- client?.selectContextItem(
- inQueryForContextProvider.title,
- segs[segs.length - 1]
- );
- setCurrentlyInContextQuery(false);
- downshiftProps.setInputValue("");
+ return;
+ }
+ setIsHovered(false);
+ }}
+ rows={props.isMainInput ? undefined : 1}
+ inQueryForDynamicProvider={
+ typeof inQueryForContextProvider !== "undefined"
+ }
+ fontSize={getFontSize()}
+ disabled={props.disabled}
+ placeholder={`Ask a question, '/' for slash commands, '@' to add context`}
+ {...getInputProps({
+ onCompositionStart: () => setIsComposing(true),
+ onCompositionEnd: () => setIsComposing(false),
+ onChange: (e) => {
+ const target = e.target as HTMLTextAreaElement;
+ // Update the height of the textarea to match the content, up to a max of 200px.
+ target.style.height = "auto";
+ target.style.height = `${Math.min(
+ target.scrollHeight,
+ 300
+ ).toString()}px`;
+
+ // setShowContextDropdown(target.value.endsWith("@"));
+ },
+ onFocus: (e) => {
+ setInputFocused(true);
+ dispatch(setBottomMessage(undefined));
+ },
+ onBlur: (e) => {
+ if (topRef.current?.contains(e.relatedTarget as Node)) {
return;
- } else {
- if (value !== "") {
- setPositionInHistory(history.length + 1);
- setHistory([...history, value]);
- }
+ }
+ setInputFocused(false);
+ },
+ onKeyDown: (event) => {
+ dispatch(setBottomMessage(undefined));
+ if (event.key === "Enter" && event.shiftKey) {
// Prevent Downshift's default 'Enter' behavior.
(event.nativeEvent as any).preventDownshiftDefault = true;
-
- if (props.onEnter) {
- props.onEnter(event);
+ setCurrentlyInContextQuery(false);
+ } else if (
+ event.key === "Enter" &&
+ (!downshiftProps.isOpen || items.length === 0) &&
+ !isComposing
+ ) {
+ const value = downshiftProps.inputValue;
+ if (inQueryForContextProvider) {
+ const segs = value.split("@");
+ selectContextItem(
+ inQueryForContextProvider.title,
+ segs[segs.length - 1]
+ );
+ setCurrentlyInContextQuery(false);
+ downshiftProps.setInputValue("");
+ return;
+ } else {
+ if (value !== "") {
+ setPositionInHistory(history.length + 1);
+ setHistory([...history, value]);
+ }
+ // Prevent Downshift's default 'Enter' behavior.
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+
+ if (props.onEnter) {
+ props.onEnter(event, value);
+ }
}
- }
- setCurrentlyInContextQuery(false);
- } else if (event.key === "Enter" && currentlyInContextQuery) {
- // Handle "Enter" for Context Providers
- selectContextItemFromDropdown(event);
- } else if (
- event.key === "Tab" &&
- downshiftProps.isOpen &&
- items.length > 0 &&
- items[downshiftProps.highlightedIndex]?.name.startsWith("/")
- ) {
- downshiftProps.setInputValue(items[0].name);
- event.preventDefault();
- } else if (event.key === "Tab") {
- (event.nativeEvent as any).preventDownshiftDefault = true;
- } else if (
- (event.key === "ArrowUp" || event.key === "ArrowDown") &&
- items.length > 0
- ) {
- return;
- } else if (event.key === "ArrowUp") {
- // Only go back in history if selectionStart is 0
- // (i.e. the cursor is at the beginning of the input)
- if (
- positionInHistory == 0 ||
- event.currentTarget.selectionStart !== 0
+ setCurrentlyInContextQuery(false);
+ } else if (event.key === "Enter" && currentlyInContextQuery) {
+ // Handle "Enter" for Context Providers
+ selectContextItemFromDropdown(event);
+ } else if (
+ event.key === "Tab" &&
+ downshiftProps.isOpen &&
+ items.length > 0 &&
+ items[downshiftProps.highlightedIndex]?.name.startsWith("/")
) {
+ downshiftProps.setInputValue(items[0].name);
+ event.preventDefault();
+ } else if (event.key === "Tab") {
(event.nativeEvent as any).preventDownshiftDefault = true;
- return;
} else if (
- positionInHistory == history.length &&
- (history.length === 0 ||
- history[history.length - 1] !== event.currentTarget.value)
+ (event.key === "ArrowUp" || event.key === "ArrowDown") &&
+ items.length > 0
) {
- setHistory([...history, event.currentTarget.value]);
- }
- downshiftProps.setInputValue(history[positionInHistory - 1]);
- setPositionInHistory((prev) => prev - 1);
- setCurrentlyInContextQuery(false);
- } else if (event.key === "ArrowDown") {
- if (
- positionInHistory === history.length ||
- event.currentTarget.selectionStart !==
- event.currentTarget.value.length
- ) {
- (event.nativeEvent as any).preventDownshiftDefault = true;
return;
- }
+ } else if (event.key === "ArrowUp") {
+ // Only go back in history if selectionStart is 0
+ // (i.e. the cursor is at the beginning of the input)
+ if (
+ positionInHistory == 0 ||
+ event.currentTarget.selectionStart !== 0
+ ) {
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ } else if (
+ positionInHistory == history.length &&
+ (history.length === 0 ||
+ history[history.length - 1] !== event.currentTarget.value)
+ ) {
+ setHistory([...history, event.currentTarget.value]);
+ }
+ downshiftProps.setInputValue(history[positionInHistory - 1]);
+ setPositionInHistory((prev) => prev - 1);
+ setCurrentlyInContextQuery(false);
+ } else if (event.key === "ArrowDown") {
+ if (
+ positionInHistory === history.length ||
+ event.currentTarget.selectionStart !==
+ event.currentTarget.value.length
+ ) {
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ }
- if (positionInHistory < history.length) {
- downshiftProps.setInputValue(history[positionInHistory + 1]);
- }
- setPositionInHistory((prev) =>
- Math.min(prev + 1, history.length)
- );
- setCurrentlyInContextQuery(false);
- } else if (event.key === "Escape") {
- if (nestedContextProvider) {
- goBackToContextProviders();
- (event.nativeEvent as any).preventDownshiftDefault = true;
- return;
- } else if (inQueryForContextProvider) {
- goBackToContextProviders();
- (event.nativeEvent as any).preventDownshiftDefault = true;
- return;
- }
+ if (positionInHistory < history.length) {
+ downshiftProps.setInputValue(
+ history[positionInHistory + 1]
+ );
+ }
+ setPositionInHistory((prev) =>
+ Math.min(prev + 1, history.length)
+ );
+ setCurrentlyInContextQuery(false);
+ } else if (event.key === "Escape") {
+ if (nestedContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ } else if (inQueryForContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ }
- setCurrentlyInContextQuery(false);
- if (downshiftProps.isOpen && items.length > 0) {
- downshiftProps.closeMenu();
+ setCurrentlyInContextQuery(false);
+ if (downshiftProps.isOpen && items.length > 0) {
+ downshiftProps.closeMenu();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ } else {
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ // Remove focus from the input
+ inputRef.current?.blur();
+ // Move cursor back over to the editor
+ postVscMessage("focusEditor", {});
+ }
+ }
+ // Home and end keys
+ else if (event.key === "Home") {
(event.nativeEvent as any).preventDownshiftDefault = true;
- } else {
+ } else if (event.key === "End") {
(event.nativeEvent as any).preventDownshiftDefault = true;
- // Remove focus from the input
- inputRef.current?.blur();
- // Move cursor back over to the editor
- postVscMessage("focusEditor", {});
}
- }
- // Home and end keys
- else if (event.key === "Home") {
- (event.nativeEvent as any).preventDownshiftDefault = true;
- } else if (event.key === "End") {
- (event.nativeEvent as any).preventDownshiftDefault = true;
- }
- },
- onClick: () => {
- dispatch(setBottomMessage(undefined));
- },
- ref: inputRef,
- })}
- />
+ },
+ onClick: () => {
+ dispatch(setBottomMessage(undefined));
+ },
+ ref: inputRef,
+ })}
+ />
+ {props.isMainInput || (
+ <DeleteButtonDiv ref={deleteButtonDivRef}>
+ {isHovered && (
+ <div className="flex">
+ <>
+ {timeline
+ .filter(
+ (h, i: number) =>
+ props.groupIndices?.includes(i) && h.logs
+ )
+ .some((h) => h.logs!.length > 0) && (
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ if (props.groupIndices)
+ client?.showLogsAtIndex(props.groupIndices[1]);
+ }}
+ text="Inspect Prompt"
+ >
+ <ArrowUpLeftIcon width="1.3em" height="1.3em" />
+ </HeaderButtonWithText>
+ )}
+ <HeaderButtonWithText
+ onClick={(e) => {
+ e.stopPropagation();
+ if (props.active && props.groupIndices) {
+ 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>
+ )}
+ </GradientBorder>
{inQueryForContextProvider && (
<DynamicQueryTitleDiv>
Enter {inQueryForContextProvider.display_title} Query
@@ -807,6 +1130,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
{...downshiftProps.getMenuProps({
ref: ulRef,
})}
+ isMainInput={props.isMainInput}
showAbove={showAbove()}
ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
hidden={
@@ -832,8 +1156,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
width="1.4em"
height="1.4em"
className="cursor-pointer"
- onClick={() => {
+ onClick={(e) => {
goBackToContextProviders();
+ inputRef.current?.focus();
}}
/>
{nestedContextProvider.display_title} -{" "}
@@ -888,18 +1213,23 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
</div>
{selectedContextItems.length === 0 &&
(downshiftProps.inputValue?.startsWith("/edit") ||
- (focused &&
+ (inputFocused &&
metaKeyPressed &&
downshiftProps.inputValue?.length > 0)) && (
- <div className="text-trueGray-400 pr-4 text-xs text-right">
+ <div
+ className="text-trueGray-400 pr-4 text-xs text-right"
+ style={{ backgroundColor: vscBackground }}
+ >
Inserting at cursor
</div>
)}
- <ContinueButton
- disabled={!(inputRef.current as any)?.value}
- onClick={() => props.onEnter(undefined)}
- />
- </>
+ {props.isMainInput && (
+ <ContinueButton
+ disabled={!(inputRef.current as any)?.value}
+ onClick={() => props.onEnter?.(undefined)}
+ />
+ )}
+ </div>
);
});
diff --git a/extension/react-app/src/components/ErrorStepContainer.tsx b/extension/react-app/src/components/ErrorStepContainer.tsx
index e8ab7950..666780c5 100644
--- a/extension/react-app/src/components/ErrorStepContainer.tsx
+++ b/extension/react-app/src/components/ErrorStepContainer.tsx
@@ -14,6 +14,7 @@ const Div = styled.div`
background-color: #ff000011;
border-radius: ${defaultBorderRadius};
border: 1px solid #cc0000;
+ margin: 8px;
`;
interface ErrorStepContainerProps {
@@ -28,8 +29,8 @@ function ErrorStepContainer(props: ErrorStepContainerProps) {
<div
style={{
position: "absolute",
- right: "4px",
- top: "4px",
+ right: "12px",
+ top: "12px",
display: "flex",
}}
>
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index 84e6118c..431d0455 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -11,6 +11,7 @@ interface HeaderButtonWithTextProps {
active?: boolean;
className?: string;
onKeyDown?: (e: any) => void;
+ tabIndex?: number;
}
const HeaderButtonWithText = React.forwardRef<
@@ -39,6 +40,7 @@ const HeaderButtonWithText = React.forwardRef<
onKeyDown={props.onKeyDown}
className={props.className}
ref={ref}
+ tabIndex={props.tabIndex}
>
{props.children}
</HeaderButton>
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index fb685a82..063572b5 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -1,23 +1,23 @@
-import { useContext, useEffect, useState } from "react";
+import { useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import {
StyledTooltip,
defaultBorderRadius,
lightGray,
secondaryDark,
- vscBackground,
vscForeground,
} from ".";
import {
TrashIcon,
PaintBrushIcon,
ExclamationTriangleIcon,
+ EyeIcon,
} from "@heroicons/react/24/outline";
import { GUIClientContext } from "../App";
import { useDispatch } from "react-redux";
-import { setBottomMessage } from "../redux/slices/uiStateSlice";
import { ContextItem } from "../../../schema/FullState";
import { getFontSize } from "../util";
+import HeaderButtonWithText from "./HeaderButtonWithText";
const Button = styled.button<{ fontSize?: number }>`
border: none;
@@ -80,7 +80,13 @@ interface PillButtonProps {
editingAny: boolean;
index: number;
areMultipleItems?: boolean;
- onDelete?: () => void;
+ onDelete?: (index?: number) => void;
+ onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
+ stepIndex?: number;
+ previewing?: boolean;
+ toggleViewContent?: () => void;
+ onBlur?: () => void;
+ focusing?: boolean;
}
interface StyledButtonProps {
@@ -88,6 +94,14 @@ interface StyledButtonProps {
editing?: boolean;
}
+const Container = styled.div<{ previewing?: boolean }>`
+ border-radius: ${defaultBorderRadius};
+ background-color: ${secondaryDark};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
const StyledButton = styled(Button)<StyledButtonProps>`
position: relative;
border-color: ${(props) => props.borderColor || "transparent"};
@@ -96,12 +110,34 @@ const StyledButton = styled(Button)<StyledButtonProps>`
&:focus {
outline: none;
- border-color: ${lightGray};
- border-width: 1px;
- border-style: solid;
+ /* border-color: ${lightGray}; */
+ text-decoration: underline;
+ }
+`;
+
+const HoverableInsidePillButton = styled(HeaderButtonWithText)<{
+ color: string;
+}>`
+ &:hover {
+ background-color: ${(props) => props.color};
}
`;
+const ClickableInsidePillButton = styled(HeaderButtonWithText)<{
+ color: string;
+ selected: boolean;
+}>`
+ ${(props) =>
+ props.selected &&
+ `
+ background-color: ${props.color};
+
+ &:hover {
+ background-color: ${props.color};
+ }
+ `}
+`;
+
const PillButton = (props: PillButtonProps) => {
const [isHovered, setIsHovered] = useState(false);
const client = useContext(GUIClientContext);
@@ -116,122 +152,125 @@ const PillButton = (props: PillButtonProps) => {
}
}, [props.editing, props.item]);
- const dispatch = useDispatch();
+ const pillContainerRef = useRef<HTMLDivElement>(null);
+ const buttonRef = useRef<HTMLButtonElement>(null);
return (
<div style={{ position: "relative" }}>
- <StyledButton
- fontSize={getFontSize()}
- borderColor={props.editing ? (warning ? "red" : undefined) : undefined}
- onMouseEnter={() => {
- setIsHovered(true);
- if (props.onHover) {
- props.onHover(true);
+ <Container previewing={props.previewing} ref={pillContainerRef}>
+ <StyledButton
+ fontSize={getFontSize()}
+ borderColor={
+ props.editing ? (warning ? "red" : undefined) : undefined
}
- }}
- 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"}
- onClick={() => {
- client?.setEditingAtIds([
- props.item.description.id.item_id,
- ]);
- }}
- >
- <PaintBrushIcon style={{ margin: "auto" }} width="1.6em" />
- </ButtonDiv>
- )}
-
- <StyledTooltip id={`pin-${props.index}`}>
- Edit this range
- </StyledTooltip>
- <ButtonDiv
- data-tooltip-id={`delete-${props.index}`}
- backgroundColor={"#cc000055"}
+ ref={buttonRef}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ if (props.onHover) {
+ props.onHover(true);
+ }
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ if (props.onHover) {
+ props.onHover(false);
+ }
+ }}
+ className={`pill-button-${props.stepIndex || "main"}`}
+ onKeyDown={(e) => {
+ if (e.key === "Backspace") {
+ props.onDelete?.(props.stepIndex);
+ } else if (e.key === "v") {
+ props.toggleViewContent?.();
+ } else if (e.key === "e") {
+ client?.setEditingAtIds([props.item.description.id.item_id]);
+ }
+ }}
+ onClick={(e) => {
+ props.onClick?.(e);
+ }}
+ onBlur={(e) => {
+ if (!pillContainerRef.current?.contains(e.relatedTarget as any)) {
+ props.onBlur?.();
+ } else {
+ e.preventDefault();
+ buttonRef.current?.focus();
+ }
+ }}
+ >
+ <span className={isHovered ? "underline" : ""}>
+ {props.item.description.name}
+ </span>
+ </StyledButton>
+ {((props.focusing && props.item.editable && props.editingAny) ||
+ props.editing) && (
+ <>
+ <ClickableInsidePillButton
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ text={
+ props.editing ? "Editing this range" : "Edit this range (e)"
+ }
onClick={() => {
- client?.deleteContextWithIds([props.item.description.id]);
- dispatch(setBottomMessage(undefined));
+ if (!props.editing) {
+ client?.setEditingAtIds([props.item.description.id.item_id]);
+ }
}}
+ tabIndex={-1}
+ color="#f0f4"
+ selected={props.editing}
>
- <TrashIcon style={{ margin: "auto" }} width="1.6em" />
- </ButtonDiv>
- </GridDiv>
+ <PaintBrushIcon width="1.4em" height="1.4em" />
+ </ClickableInsidePillButton>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
+ Editing this range
+ </StyledTooltip>
+ </>
+ )}
+ {(props.focusing || props.previewing) && (
+ <ClickableInsidePillButton
+ text="View (v)"
+ onClick={() => props.toggleViewContent?.()}
+ tabIndex={-1}
+ color="#ff04"
+ selected={props.previewing || false}
+ >
+ <EyeIcon width="1.4em" height="1.4em" />
+ </ClickableInsidePillButton>
+ )}
+ {props.focusing && (
+ <HoverableInsidePillButton
+ text="Delete (⌫)"
+ onClick={() => props.onDelete?.(props.stepIndex)}
+ tabIndex={-1}
+ color="#f004"
+ >
+ <TrashIcon width="1.4em" height="1.4em" />
+ </HoverableInsidePillButton>
)}
- {props.item.description.name}
- </StyledButton>
+ </Container>
<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}`}
- className="z-10"
- >
- <ExclamationTriangleIcon
- style={{ margin: "auto" }}
- width="1.0em"
- strokeWidth={2}
- />
- </CircleDiv>
- <StyledTooltip id={`circle-div-${props.item.description.name}`}>
- {warning}
- </StyledTooltip>
- </>
- ) : (
- <>
- <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>
- </>
- ))}
+ {props.editing && warning && (
+ <>
+ <CircleDiv
+ data-tooltip-id={`circle-div-${props.item.description.name}`}
+ className="z-10"
+ >
+ <ExclamationTriangleIcon
+ style={{ margin: "auto" }}
+ width="1.0em"
+ strokeWidth={2}
+ />
+ </CircleDiv>
+ <StyledTooltip id={`circle-div-${props.item.description.name}`}>
+ {warning}
+ </StyledTooltip>
+ </>
+ )}
</div>
);
};
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index e7264c5d..11e80fb2 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -35,10 +35,10 @@ const ButtonsDiv = styled.div`
background-color: ${vscBackground};
box-shadow: 1px 1px 10px ${vscBackground};
border-radius: ${defaultBorderRadius};
-
+ z-index: 100;
position: absolute;
- right: 0;
- top: 0;
+ right: 8px;
+ top: 16px;
height: 0;
`;
diff --git a/extension/react-app/src/components/StyledMarkdownPreview.tsx b/extension/react-app/src/components/StyledMarkdownPreview.tsx
index 78d4234c..f53e5289 100644
--- a/extension/react-app/src/components/StyledMarkdownPreview.tsx
+++ b/extension/react-app/src/components/StyledMarkdownPreview.tsx
@@ -12,12 +12,13 @@ import { getFontSize } from "../util";
const StyledMarkdownPreview = styled(MarkdownPreview)<{
light?: boolean;
fontSize?: number;
+ maxHeight?: number;
}>`
pre {
background-color: ${(props) =>
props.light ? vscBackground : secondaryDark};
border-radius: ${defaultBorderRadius};
- border: 0.5px solid ${lightGray};
+ /* border: 0.5px solid ${lightGray}; */
max-width: calc(100vw - 24px);
}
@@ -34,6 +35,15 @@ const StyledMarkdownPreview = styled(MarkdownPreview)<{
props.light ? vscBackground : secondaryDark};
color: ${vscForeground};
padding: 12px;
+
+ ${(props) => {
+ if (props.maxHeight) {
+ return `
+ max-height: ${props.maxHeight}px;
+ overflow-y: auto;
+ `;
+ }
+ }}
}
background-color: ${(props) => (props.light ? "transparent" : vscBackground)};
diff --git a/extension/react-app/src/components/Suggestions.tsx b/extension/react-app/src/components/Suggestions.tsx
index ed2eb558..bdda7579 100644
--- a/extension/react-app/src/components/Suggestions.tsx
+++ b/extension/react-app/src/components/Suggestions.tsx
@@ -150,6 +150,8 @@ const NUM_STAGES = suggestionsStages.length;
const TutorialDiv = styled.div`
margin: 4px;
+ margin-left: 8px;
+ margin-right: 8px;
position: relative;
background-color: #ff02;
border-radius: ${defaultBorderRadius};
diff --git a/extension/react-app/src/components/TimelineItem.tsx b/extension/react-app/src/components/TimelineItem.tsx
index f54788eb..b51dd307 100644
--- a/extension/react-app/src/components/TimelineItem.tsx
+++ b/extension/react-app/src/components/TimelineItem.tsx
@@ -11,7 +11,7 @@ const CollapseButton = styled.div`
align-items: center;
flex-shrink: 0;
flex-grow: 0;
- margin-left: 5px;
+ margin-left: 13px;
cursor: pointer;
`;
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 11671526..99b4bbc4 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -35,7 +35,6 @@ import { useSelector } from "react-redux";
interface UserInputContainerProps {
onDelete: () => void;
children: string;
- historyNode: HistoryNode;
index: number;
onToggle: (arg0: boolean) => void;
onToggleAll: (arg0: boolean) => void;
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 1c27527c..9d9b7c40 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -39,7 +39,7 @@ export const StyledTooltip = styled(Tooltip)`
padding: 6px;
padding-left: 12px;
padding-right: 12px;
- z-index: 100;
+ z-index: 1000;
max-width: 80vw;
`;
@@ -196,6 +196,11 @@ export const HeaderButton = styled.button<{ inverted: boolean | undefined }>`
border-radius: ${defaultBorderRadius};
cursor: ${({ disabled }) => (disabled ? "default" : "pointer")};
+ &:focus {
+ outline: none;
+ border: none;
+ }
+
&:hover {
background-color: ${({ inverted }) =>
typeof inverted === "undefined" || inverted
diff --git a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
index d71186d7..998d3a6d 100644
--- a/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/AbstractContinueGUIClientProtocol.ts
@@ -21,7 +21,7 @@ abstract class AbstractContinueGUIClientProtocol {
abstract deleteAtIndex(index: number): void;
- abstract deleteContextWithIds(ids: ContextItemId[]): void;
+ abstract deleteContextWithIds(ids: ContextItemId[], index?: number): void;
abstract setEditingAtIds(ids: string[]): void;
@@ -33,6 +33,12 @@ abstract class AbstractContinueGUIClientProtocol {
abstract selectContextItem(id: string, query: string): void;
+ abstract selectContextItemAtIndex(
+ id: string,
+ query: string,
+ index: number
+ ): void;
+
abstract loadSession(session_id?: string): void;
abstract onReconnectAtSession(session_id: string): void;
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 8205a629..863b1031 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -101,9 +101,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger?.send("delete_at_index", { index });
}
- deleteContextWithIds(ids: ContextItemId[]) {
+ deleteContextWithIds(ids: ContextItemId[], index?: number) {
this.messenger?.send("delete_context_with_ids", {
ids: ids.map((id) => `${id.provider_title}-${id.item_id}`),
+ index,
});
}
@@ -119,14 +120,22 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
this.messenger?.send("show_logs_at_index", { index });
}
- showContextVirtualFile(): void {
- this.messenger?.send("show_context_virtual_file", {});
+ showContextVirtualFile(index?: number): void {
+ this.messenger?.send("show_context_virtual_file", { index });
}
selectContextItem(id: string, query: string): void {
this.messenger?.send("select_context_item", { id, query });
}
+ selectContextItemAtIndex(id: string, query: string, index: number): void {
+ this.messenger?.send("select_context_item_at_index", {
+ id,
+ query,
+ index,
+ });
+ }
+
editStepAtIndex(userInput: string, index: number): void {
this.messenger?.send("edit_step_at_index", {
user_input: userInput,
diff --git a/extension/react-app/src/pages/gui.tsx b/extension/react-app/src/pages/gui.tsx
index a93ca9a0..12835121 100644
--- a/extension/react-app/src/pages/gui.tsx
+++ b/extension/react-app/src/pages/gui.tsx
@@ -17,7 +17,6 @@ import { usePostHog } from "posthog-js/react";
import { useDispatch, useSelector } from "react-redux";
import { RootStore } from "../redux/store";
import { postVscMessage } from "../vscode";
-import UserInputContainer from "../components/UserInputContainer";
import { isMetaEquivalentKeyPressed } from "../util";
import {
setBottomMessage,
@@ -30,6 +29,7 @@ import RingLoader from "../components/RingLoader";
import {
setServerState,
temporarilyClearSession,
+ temporarilyCreateNewUserInput,
temporarilyPushToUserInputQueue,
} from "../redux/slices/serverStateReducer";
import TimelineItem from "../components/TimelineItem";
@@ -76,11 +76,8 @@ const TitleTextInput = styled(TextInput)`
const StepsDiv = styled.div`
position: relative;
background-color: transparent;
- padding-left: 8px;
- padding-right: 8px;
& > * {
- z-index: 1;
position: relative;
}
@@ -331,7 +328,7 @@ function GUI(props: GUIProps) {
}
client.sendMainInput(input);
- dispatch(temporarilyPushToUserInputQueue(input));
+ dispatch(temporarilyCreateNewUserInput(input));
// Increment localstorage counter for popup
const counter = localStorage.getItem("mainTextEntryCounter");
@@ -645,10 +642,19 @@ function GUI(props: GUIProps) {
<>
{node.step.name === "User Input" ? (
node.step.hide || (
- <UserInputContainer
- active={getStepsInUserInputGroup(index).some((i) => {
- return history.timeline[i].active;
- })}
+ <ComboBox
+ isMainInput={false}
+ value={node.step.description as string}
+ active={
+ getStepsInUserInputGroup(index).some((i) => {
+ return history.timeline[i].active;
+ }) || history.timeline[index].active
+ }
+ onEnter={(e, value) => {
+ if (value) client?.editStepAtIndex(value, index);
+ e?.stopPropagation();
+ e?.preventDefault();
+ }}
groupIndices={getStepsInUserInputGroup(index)}
onToggle={(isOpen: boolean) => {
// Collapse all steps in the section
@@ -678,10 +684,7 @@ function GUI(props: GUIProps) {
client?.deleteAtIndex(i);
});
}}
- historyNode={node}
- >
- {node.step.description as string}
- </UserInputContainer>
+ />
)
) : (
<TimelineItem
@@ -761,8 +764,9 @@ function GUI(props: GUIProps) {
<div ref={aboveComboBoxDivRef} />
<ComboBox
+ isMainInput={true}
ref={mainTextInputRef}
- onEnter={(e) => {
+ onEnter={(e, _) => {
onMainTextInput(e);
e?.stopPropagation();
e?.preventDefault();
diff --git a/extension/react-app/src/redux/slices/serverStateReducer.ts b/extension/react-app/src/redux/slices/serverStateReducer.ts
index 9b3a780c..1f4836cb 100644
--- a/extension/react-app/src/redux/slices/serverStateReducer.ts
+++ b/extension/react-app/src/redux/slices/serverStateReducer.ts
@@ -98,6 +98,21 @@ export const serverStateSlice = createSlice({
temporarilyPushToUserInputQueue: (state, action) => {
state.user_input_queue = [...state.user_input_queue, action.payload];
},
+ temporarilyCreateNewUserInput: (state, action) => {
+ state.history.timeline = [
+ ...state.history.timeline,
+ {
+ step: {
+ description: action.payload,
+ name: "User Input",
+ hide: false,
+ },
+ depth: 0,
+ active: false,
+ context_used: state.selected_context_items,
+ },
+ ];
+ },
temporarilyClearSession: (state, action) => {
state.history.timeline = [];
state.selected_context_items = [];
@@ -114,5 +129,6 @@ export const {
setServerState,
temporarilyPushToUserInputQueue,
temporarilyClearSession,
+ temporarilyCreateNewUserInput,
} = serverStateSlice.actions;
export default serverStateSlice.reducer;
diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts
index fd74044d..5a95be41 100644
--- a/extension/react-app/src/util/index.ts
+++ b/extension/react-app/src/util/index.ts
@@ -46,3 +46,57 @@ export function getFontSize(): number {
const fontSize = localStorage.getItem("fontSize");
return fontSize ? parseInt(fontSize) : 13;
}
+
+export function getMarkdownLanguageTagForFile(filepath: string): string {
+ const ext = filepath.split(".").pop();
+ switch (ext) {
+ case "py":
+ return "python";
+ case "js":
+ return "javascript";
+ case "ts":
+ return "typescript";
+ case "java":
+ return "java";
+ case "go":
+ return "go";
+ case "rb":
+ return "ruby";
+ case "rs":
+ return "rust";
+ case "c":
+ return "c";
+ case "cpp":
+ return "cpp";
+ case "cs":
+ return "csharp";
+ case "php":
+ return "php";
+ case "scala":
+ return "scala";
+ case "swift":
+ return "swift";
+ case "kt":
+ return "kotlin";
+ case "md":
+ return "markdown";
+ case "json":
+ return "json";
+ case "html":
+ return "html";
+ case "css":
+ return "css";
+ case "sh":
+ return "shell";
+ case "yaml":
+ return "yaml";
+ case "toml":
+ return "toml";
+ case "tex":
+ return "latex";
+ case "sql":
+ return "sql";
+ default:
+ return "";
+ }
+}
diff --git a/extension/schema/ContinueConfig.d.ts b/extension/schema/ContinueConfig.d.ts
index 92f6e047..64aa5c02 100644
--- a/extension/schema/ContinueConfig.d.ts
+++ b/extension/schema/ContinueConfig.d.ts
@@ -72,10 +72,14 @@ export type VerifySsl = boolean;
*/
export type CaBundlePath = string;
/**
+ * Proxy URL to use when making the HTTP request
+ */
+export type Proxy = string;
+/**
* The API key for the LLM provider.
*/
export type ApiKey = string;
-export type Unused = LLM[];
+export type Saved = 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.
*/
@@ -205,12 +209,10 @@ export interface FunctionCall {
*/
export interface Models1 {
default: LLM;
- small?: LLM;
- medium?: LLM;
- large?: LLM;
+ summarize?: LLM;
edit?: LLM;
chat?: LLM;
- unused?: Unused;
+ saved?: Saved;
sdk?: ContinueSDK;
[k: string]: unknown;
}
@@ -224,6 +226,7 @@ export interface LLM {
timeout?: Timeout;
verify_ssl?: VerifySsl;
ca_bundle_path?: CaBundlePath;
+ proxy?: Proxy;
prompt_templates?: PromptTemplates;
api_key?: ApiKey;
[k: string]: unknown;
diff --git a/extension/schema/FullState.d.ts b/extension/schema/FullState.d.ts
index 5d5a5444..90b8506b 100644
--- a/extension/schema/FullState.d.ts
+++ b/extension/schema/FullState.d.ts
@@ -23,21 +23,22 @@ export type Depth = number;
export type Deleted = boolean;
export type Active = boolean;
export type Logs = string[];
-export type Timeline = HistoryNode[];
-export type CurrentIndex = number;
-export type Active1 = boolean;
-export type UserInputQueue = string[];
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 ContextUsed = ContextItem[];
+export type Timeline = HistoryNode[];
+export type CurrentIndex = number;
+export type Active1 = boolean;
+export type UserInputQueue = string[];
+export type Name4 = string;
+export type Description2 = string;
+export type SlashCommands = SlashCommandDescription[];
+export type AddingHighlightedCode = boolean;
export type SelectedContextItems = ContextItem[];
export type SessionId = string;
export type Title = string;
@@ -51,6 +52,7 @@ export type Description3 = string;
export type Dynamic = boolean;
export type RequiresQuery = boolean;
export type ContextProviders = ContextProviderDescription[];
+export type MeilisearchUrl = string;
/**
* A full state of the program, including the history
@@ -66,6 +68,7 @@ export interface FullState1 {
config: ContinueConfig;
saved_context_groups?: SavedContextGroups;
context_providers?: ContextProviders;
+ meilisearch_url?: MeilisearchUrl;
[k: string]: unknown;
}
/**
@@ -86,6 +89,7 @@ export interface HistoryNode {
deleted?: Deleted;
active?: Active;
logs?: Logs;
+ context_used?: ContextUsed;
[k: string]: unknown;
}
export interface Step {
@@ -114,11 +118,6 @@ export interface FunctionCall {
export interface Observation {
[k: string]: unknown;
}
-export interface SlashCommandDescription {
- name: Name3;
- description: Description1;
- [k: string]: unknown;
-}
/**
* A ContextItem is a single item that is stored in the ContextManager.
*/
@@ -135,8 +134,8 @@ export interface ContextItem {
* The id can be used to retrieve the ContextItem from the ContextManager.
*/
export interface ContextItemDescription {
- name: Name4;
- description: Description2;
+ name: Name3;
+ description: Description1;
id: ContextItemId;
[k: string]: unknown;
}
@@ -148,6 +147,11 @@ export interface ContextItemId {
item_id: ItemId;
[k: string]: unknown;
}
+export interface SlashCommandDescription {
+ name: Name4;
+ description: Description2;
+ [k: string]: unknown;
+}
export interface SessionInfo {
session_id: SessionId;
title: Title;
diff --git a/extension/schema/History.d.ts b/extension/schema/History.d.ts
index b00a1505..9b7db18a 100644
--- a/extension/schema/History.d.ts
+++ b/extension/schema/History.d.ts
@@ -23,6 +23,14 @@ export type Depth = number;
export type Deleted = boolean;
export type Active = boolean;
export type Logs = string[];
+export type Name3 = string;
+export type Description1 = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content1 = string;
+export type Editing = boolean;
+export type Editable = boolean;
+export type ContextUsed = ContextItem[];
export type Timeline = HistoryNode[];
export type CurrentIndex = number;
@@ -44,6 +52,7 @@ export interface HistoryNode {
deleted?: Deleted;
active?: Active;
logs?: Logs;
+ context_used?: ContextUsed;
[k: string]: unknown;
}
export interface Step {
@@ -72,3 +81,32 @@ export interface FunctionCall {
export interface Observation {
[k: string]: unknown;
}
+/**
+ * A ContextItem is a single item that is stored in the ContextManager.
+ */
+export interface ContextItem {
+ description: ContextItemDescription;
+ content: Content1;
+ 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: Name3;
+ description: Description1;
+ 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/HistoryNode.d.ts b/extension/schema/HistoryNode.d.ts
index 08424d75..ad4c1154 100644
--- a/extension/schema/HistoryNode.d.ts
+++ b/extension/schema/HistoryNode.d.ts
@@ -23,6 +23,14 @@ export type Depth = number;
export type Deleted = boolean;
export type Active = boolean;
export type Logs = string[];
+export type Name3 = string;
+export type Description1 = string;
+export type ProviderTitle = string;
+export type ItemId = string;
+export type Content1 = string;
+export type Editing = boolean;
+export type Editable = boolean;
+export type ContextUsed = ContextItem[];
/**
* A point in history, a list of which make up History
@@ -34,6 +42,7 @@ export interface HistoryNode1 {
deleted?: Deleted;
active?: Active;
logs?: Logs;
+ context_used?: ContextUsed;
[k: string]: unknown;
}
export interface Step {
@@ -62,3 +71,32 @@ export interface FunctionCall {
export interface Observation {
[k: string]: unknown;
}
+/**
+ * A ContextItem is a single item that is stored in the ContextManager.
+ */
+export interface ContextItem {
+ description: ContextItemDescription;
+ content: Content1;
+ 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: Name3;
+ description: Description1;
+ 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/LLM.d.ts b/extension/schema/LLM.d.ts
index 31d38456..2c1ced29 100644
--- a/extension/schema/LLM.d.ts
+++ b/extension/schema/LLM.d.ts
@@ -43,6 +43,10 @@ export type VerifySsl = boolean;
*/
export type CaBundlePath = string;
/**
+ * Proxy URL to use when making the HTTP request
+ */
+export type Proxy = string;
+/**
* The API key for the LLM provider.
*/
export type ApiKey = string;
@@ -57,6 +61,7 @@ export interface LLM1 {
timeout?: Timeout;
verify_ssl?: VerifySsl;
ca_bundle_path?: CaBundlePath;
+ proxy?: Proxy;
prompt_templates?: PromptTemplates;
api_key?: ApiKey;
[k: string]: unknown;
diff --git a/extension/schema/Models.d.ts b/extension/schema/Models.d.ts
index 9005c08c..67d73cfc 100644
--- a/extension/schema/Models.d.ts
+++ b/extension/schema/Models.d.ts
@@ -43,22 +43,24 @@ export type VerifySsl = boolean;
*/
export type CaBundlePath = string;
/**
+ * Proxy URL to use when making the HTTP request
+ */
+export type Proxy = string;
+/**
* The API key for the LLM provider.
*/
export type ApiKey = string;
-export type Unused = LLM[];
+export type Saved = LLM[];
/**
* Main class that holds the current model configuration
*/
export interface Models1 {
default: LLM;
- small?: LLM;
- medium?: LLM;
- large?: LLM;
+ summarize?: LLM;
edit?: LLM;
chat?: LLM;
- unused?: Unused;
+ saved?: Saved;
sdk?: ContinueSDK;
[k: string]: unknown;
}
@@ -72,6 +74,7 @@ export interface LLM {
timeout?: Timeout;
verify_ssl?: VerifySsl;
ca_bundle_path?: CaBundlePath;
+ proxy?: Proxy;
prompt_templates?: PromptTemplates;
api_key?: ApiKey;
[k: string]: unknown;
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index e2c86bdf..006ac156 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -70,10 +70,11 @@ class IdeProtocolClient {
});
messenger.onMessage((messageType, data, messenger) => {
this.handleMessage(messageType, data, messenger).catch((err) => {
+ console.log("Error handling message: ", err);
vscode.window
.showErrorMessage(
`Error handling message (${messageType}) from Continue server: ` +
- err.message,
+ err,
"View Logs"
)
.then((selection) => {