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) => {  | 
