diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-09-28 01:02:52 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-28 01:02:52 -0700 |
commit | 95363a5b52f3bf73531ac76b00178fa79ca97661 (patch) | |
tree | 9b9c1614556f1f0d21f363e6a9fe950069affb5d /extension | |
parent | d4acf4bb11dbd7d3d6210e2949d21143d721e81e (diff) | |
download | sncontinue-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')
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) => { |