diff options
author | Nate Sesti <33237525+sestinj@users.noreply.github.com> | 2023-07-02 20:15:49 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-02 20:15:49 -0700 |
commit | cf3d08ceaabecd80b20aaac24c5c166cd8e6472f (patch) | |
tree | 7559fe0d9500d5156fa99ee5614ac0fe6d4e88f3 | |
parent | 0ffd2648d679916872c681036a68741a83d80c0e (diff) | |
parent | 0a812994703791023125177fe4820202374e45b0 (diff) | |
download | sncontinue-cf3d08ceaabecd80b20aaac24c5c166cd8e6472f.tar.gz sncontinue-cf3d08ceaabecd80b20aaac24c5c166cd8e6472f.tar.bz2 sncontinue-cf3d08ceaabecd80b20aaac24c5c166cd8e6472f.zip |
Merge pull request #170 from continuedev/explicit-context
Explicit context
20 files changed, 441 insertions, 423 deletions
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 3c7fbdef..1a77ca64 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -2,8 +2,10 @@ from functools import cached_property import traceback import time from typing import Any, Callable, Coroutine, Dict, List - +import os from aiohttp import ClientPayloadError + +from ..models.filesystem import RangeInFileWithContents from ..models.filesystem_edit import FileEditWithFullContents from ..libs.llm import LLM from .observation import Observation, InternalErrorObservation @@ -59,7 +61,13 @@ class Autopilot(ContinueBaseModel): keep_untouched = (cached_property,) def get_full_state(self) -> FullState: - return FullState(history=self.history, active=self._active, user_input_queue=self._main_user_input_queue, default_model=self.continue_sdk.config.default_model) + return FullState( + history=self.history, + active=self._active, + user_input_queue=self._main_user_input_queue, + default_model=self.continue_sdk.config.default_model, + highlighted_ranges=self._highlighted_ranges + ) async def get_available_slash_commands(self) -> List[Dict]: return list(map(lambda x: {"name": x.name, "description": x.description}, self.continue_sdk.config.slash_commands)) or [] @@ -124,6 +132,31 @@ class Autopilot(ContinueBaseModel): tb_step.step_name, {"output": output, **tb_step.params}) await self._run_singular_step(step) + _highlighted_ranges: List[RangeInFileWithContents] = [] + + async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]): + workspace_path = self.continue_sdk.ide.workspace_directory + for rif in range_in_files: + rif.filepath = os.path.relpath(rif.filepath, workspace_path) + + old_ranges = self._highlighted_ranges + range_in_files + new_ranges = [] + + while len(old_ranges) > 0: + old_range = old_ranges.pop(0) + found_overlap = False + for i in range(len(new_ranges)): + if old_range.filepath == new_ranges[i].filepath and old_range.range.overlaps_with(new_ranges[i].range): + new_ranges[i] = old_range.union(new_ranges[i]) + found_overlap = True + break + + if not found_overlap: + new_ranges.append(old_range) + + self._highlighted_ranges = new_ranges + await self.update_subscribers() + _step_depth: int = 0 async def retry_at_index(self, index: int): @@ -135,6 +168,10 @@ class Autopilot(ContinueBaseModel): self.history.timeline[index].deleted = True await self.update_subscribers() + async def delete_context_item_at_index(self, index: int): + self._highlighted_ranges.pop(index) + await self.update_subscribers() + async def _run_singular_step(self, step: "Step", is_future_step: bool = False) -> Coroutine[Observation, None, None]: # Allow config to set disallowed steps if step.__class__.__name__ in self.continue_sdk.config.disallowed_steps: diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py index 4c6f4dc2..2d84801c 100644 --- a/continuedev/src/continuedev/core/main.py +++ b/continuedev/src/continuedev/core/main.py @@ -2,6 +2,7 @@ import json from textwrap import dedent from typing import Callable, Coroutine, Dict, Generator, List, Literal, Tuple, Union +from ..models.filesystem import RangeInFileWithContents from ..models.main import ContinueBaseModel from pydantic import validator from .observation import Observation @@ -201,6 +202,7 @@ class FullState(ContinueBaseModel): active: bool user_input_queue: List[str] default_model: str + highlighted_ranges: List[RangeInFileWithContents] class ContinueSDK: diff --git a/continuedev/src/continuedev/core/sdk.py b/continuedev/src/continuedev/core/sdk.py index cfe2e436..50a14bed 100644 --- a/continuedev/src/continuedev/core/sdk.py +++ b/continuedev/src/continuedev/core/sdk.py @@ -179,7 +179,7 @@ class ContinueSDK(AbstractContinueSDK): async def get_chat_context(self) -> List[ChatMessage]: history_context = self.history.to_chat_history() - highlighted_code = await self.ide.getHighlightedCode() + highlighted_code = self.__autopilot._highlighted_ranges preface = "The following code is highlighted" @@ -190,11 +190,10 @@ class ContinueSDK(AbstractContinueSDK): if len(files) > 0: content = await self.ide.readFile(files[0]) highlighted_code = [ - RangeInFile.from_entire_file(files[0], content)] + RangeInFileWithContents.from_entire_file(files[0], content)] for rif in highlighted_code: - code = await self.ide.readRangeInFile(rif) - msg = ChatMessage(content=f"{preface} ({rif.filepath}):\n```\n{code}\n```", + msg = ChatMessage(content=f"{preface} ({rif.filepath}):\n```\n{rif.contents}\n```", role="system", summary=f"{preface}: {rif.filepath}") # Don't insert after latest user message or function call diff --git a/continuedev/src/continuedev/models/filesystem.py b/continuedev/src/continuedev/models/filesystem.py index b709dd21..df0b15d7 100644 --- a/continuedev/src/continuedev/models/filesystem.py +++ b/continuedev/src/continuedev/models/filesystem.py @@ -23,11 +23,34 @@ class RangeInFile(BaseModel): class RangeInFileWithContents(RangeInFile): + """A range in a file with the contents of the range.""" contents: str def __hash__(self): return hash((self.filepath, self.range, self.contents)) + def union(self, other: "RangeInFileWithContents") -> "RangeInFileWithContents": + assert self.filepath == other.filepath + # Use a placeholder variable for self and swap it with other if other comes before self + first = self + second = other + if other.range.start < self.range.start: + first = other + second = self + + assert first.filepath == second.filepath + + # Calculate union of contents + num_overlapping_lines = first.range.end.line - second.range.start.line + 1 + union_lines = first.contents.splitlines()[:-num_overlapping_lines] + \ + second.contents.splitlines() + + return RangeInFileWithContents( + filepath=first.filepath, + range=first.range.union(second.range), + contents="\n".join(union_lines) + ) + @staticmethod def from_entire_file(filepath: str, content: str) -> "RangeInFileWithContents": lines = content.splitlines() diff --git a/continuedev/src/continuedev/models/main.py b/continuedev/src/continuedev/models/main.py index d5f6e650..fa736772 100644 --- a/continuedev/src/continuedev/models/main.py +++ b/continuedev/src/continuedev/models/main.py @@ -43,12 +43,23 @@ class Position(BaseModel): def from_end_of_file(contents: str) -> "Position": return Position.from_index(contents, len(contents)) + def to_index(self, string: str) -> int: + """Convert line and character to index in string""" + lines = string.splitlines() + return sum(map(len, lines[:self.line])) + self.character + class Range(BaseModel): """A range in a file. 0-indexed.""" start: Position end: Position + def __lt__(self, other: "Range") -> bool: + return self.start < other.start or (self.start == other.start and self.end < other.end) + + def __eq__(self, other: "Range") -> bool: + return self.start == other.start and self.end == other.end + def __hash__(self): return hash((self.start, self.end)) diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py index c0178920..9a33fb6c 100644 --- a/continuedev/src/continuedev/server/gui.py +++ b/continuedev/src/continuedev/server/gui.py @@ -83,6 +83,8 @@ class GUIProtocolServer(AbstractGUIProtocolServer): self.on_clear_history() elif message_type == "delete_at_index": self.on_delete_at_index(data["index"]) + elif message_type == "delete_context_item_at_index": + self.on_delete_context_item_at_index(data["index"]) except Exception as e: print(e) @@ -127,6 +129,11 @@ class GUIProtocolServer(AbstractGUIProtocolServer): def on_delete_at_index(self, index: int): asyncio.create_task(self.session.autopilot.delete_at_index(index)) + def on_delete_context_item_at_index(self, index: int): + asyncio.create_task( + self.session.autopilot.delete_context_item_at_index(index) + ) + @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(websocket_session)): diff --git a/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py index 1f790991..e2685493 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -9,7 +9,7 @@ from uvicorn.main import Server from ..libs.util.telemetry import capture_event from ..libs.util.queue import AsyncSubscriptionQueue -from ..models.filesystem import FileSystem, RangeInFile, EditDiff, RealFileSystem +from ..models.filesystem import FileSystem, RangeInFile, EditDiff, RangeInFileWithContents, RealFileSystem from ..models.filesystem_edit import AddDirectory, AddFile, DeleteDirectory, DeleteFile, FileSystemEdit, FileEdit, FileEditWithFullContents, RenameDirectory, RenameFile, SequentialFileSystemEdit from pydantic import BaseModel from .gui import SessionManager, session_manager @@ -140,6 +140,9 @@ class IdeProtocolServer(AbstractIdeProtocolServer): fileEdits = list( map(lambda d: FileEditWithFullContents.parse_obj(d), data["fileEdits"])) self.onFileEdits(fileEdits) + elif message_type == "highlightedCodePush": + self.onHighlightedCodeUpdate( + [RangeInFileWithContents(**rif) for rif in data["highlightedCode"]]) elif message_type == "commandOutput": output = data["output"] self.onCommandOutput(output) @@ -234,6 +237,11 @@ class IdeProtocolServer(AbstractIdeProtocolServer): asyncio.create_task( session.autopilot.handle_command_output(output)) + def onHighlightedCodeUpdate(self, range_in_files: List[RangeInFileWithContents]): + for _, session in self.session_manager.sessions.items(): + asyncio.create_task( + session.autopilot.handle_highlighted_code(range_in_files)) + # Request information. Session doesn't matter. async def getOpenFiles(self) -> List[str]: resp = await self._send_and_receive_json({}, OpenFilesResponse, "openFiles") diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 7faf5563..de2eea27 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod, abstractproperty from ..models.main import Traceback from ..models.filesystem_edit import FileEdit, FileSystemEdit, EditDiff -from ..models.filesystem import RangeInFile +from ..models.filesystem import RangeInFile, RangeInFileWithContents class AbstractIdeProtocolServer(ABC): @@ -91,6 +91,10 @@ class AbstractIdeProtocolServer(ABC): async def runCommand(self, command: str) -> str: """Run a command""" + @abstractmethod + def onHighlightedCodeUpdate(self, range_in_files: List[RangeInFileWithContents]): + """Called when highlighted code is updated""" + @abstractproperty def workspace_directory(self) -> str: """Get the workspace directory""" diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py index 34a97a17..9d556655 100644 --- a/continuedev/src/continuedev/steps/chat.py +++ b/continuedev/src/continuedev/steps/chat.py @@ -126,7 +126,7 @@ class RunTerminalCommandStep(Step): class ViewDirectoryTreeStep(Step): name: str = "View Directory Tree" - description: str = "View the directory tree to learn which folder and files exist." + description: str = "View the directory tree to learn which folder and files exist. You should always do this before adding new files." async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]: return f"Viewed the directory tree." diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py index 8f59bc4d..4eb2445c 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -318,8 +318,7 @@ class DefaultModelEditCodeStep(Step): nonlocal current_block_start, current_line_in_file, original_lines, original_lines_below_previous_blocks, current_block_lines, indices_of_last_matched_lines, LINES_TO_MATCH_BEFORE_ENDING_BLOCK, offset_from_blocks # Highlight the line to show progress - # - len(current_block_lines) - line_to_highlight = current_line_in_file + line_to_highlight = current_line_in_file - len(current_block_lines) await sdk.ide.highlightCode(RangeInFile(filepath=rif.filepath, range=Range.from_shorthand( line_to_highlight, 0, line_to_highlight, 0)), "#FFFFFF22" if len(current_block_lines) == 0 else "#00FF0022") diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx index 1624b986..17f5626b 100644 --- a/extension/react-app/src/components/CodeBlock.tsx +++ b/extension/react-app/src/components/CodeBlock.tsx @@ -61,7 +61,7 @@ function CopyButton(props: { textToCopy: string; visible: boolean }) { ); } -function CodeBlock(props: { children: React.ReactNode }) { +function CodeBlock(props: { children: string; showCopy?: boolean }) { const [result, setResult] = useState<AutoHighlightResult | undefined>( undefined ); @@ -78,39 +78,36 @@ function CodeBlock(props: { children: React.ReactNode }) { setHighlightTimeout( setTimeout(() => { - const result = hljs.highlightAuto( - (props.children as any).props.children[0], - [ - "python", - "javascript", - "typescript", - "bash", - "html", - "css", - "json", - "yaml", - "markdown", - "sql", - "java", - "c", - "cpp", - "csharp", - "go", - "kotlin", - "php", - "ruby", - "rust", - "scala", - "swift", - "dart", - "haskell", - "perl", - "r", - "julia", - "objectivec", - "ocaml", - ] - ); + const result = hljs.highlightAuto(props.children, [ + "python", + "javascript", + "typescript", + "bash", + "html", + "css", + "json", + "yaml", + "markdown", + "sql", + "java", + "c", + "cpp", + "csharp", + "go", + "kotlin", + "php", + "ruby", + "rust", + "scala", + "swift", + "dart", + "haskell", + "perl", + "r", + "julia", + "objectivec", + "ocaml", + ]); setResult(result); setHighlightTimeout(undefined); }, 100) @@ -129,13 +126,11 @@ function CodeBlock(props: { children: React.ReactNode }) { > <CopyButtonDiv> <CopyButton - visible={hovered} - textToCopy={(props.children as any).props.children[0]} + visible={hovered && (props.showCopy || true)} + textToCopy={props.children} /> </CopyButtonDiv> - <StyledCode language={result?.language}> - {(props.children as any).props.children[0]} - </StyledCode> + <StyledCode language={result?.language}>{props.children}</StyledCode> </StyledPre> ); } diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 3816cee8..f299c3a2 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import { useCombobox } from "downshift"; import styled from "styled-components"; import { @@ -7,8 +7,27 @@ import { secondaryDark, vscBackground, } from "."; +import CodeBlock from "./CodeBlock"; +import { RangeInFile } from "../../../src/client"; +import PillButton from "./PillButton"; const mainInputFontSize = 16; + +const ContextDropdown = styled.div` + position: absolute; + padding: 4px; + width: calc(100% - 16px - 8px); + background-color: ${secondaryDark}; + color: white; + border-bottom-right-radius: ${defaultBorderRadius}; + border-bottom-left-radius: ${defaultBorderRadius}; + /* border: 1px solid white; */ + border-top: none; + margin: 8px; + outline: 1px solid orange; + z-index: 5; +`; + const MainTextInput = styled.textarea` resize: none; @@ -20,6 +39,7 @@ const MainTextInput = styled.textarea` width: 100%; background-color: ${vscBackground}; color: white; + z-index: 1; &:focus { border: 1px solid transparent; @@ -49,6 +69,7 @@ const Ul = styled.ul<{ border-radius: ${defaultBorderRadius}; overflow: hidden; border: 0.5px solid gray; + z-index: 2; `; const Li = styled.li<{ @@ -71,6 +92,8 @@ interface ComboBoxProps { onInputValueChange: (inputValue: string) => void; disabled?: boolean; onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void; + highlightedCodeSections?: (RangeInFile & { contents: string })[]; + deleteContextItem?: (idx: number) => void; } const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { @@ -78,6 +101,26 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { // The position of the current command you are typing now, so the one that will be appended to history once you press enter const [positionInHistory, setPositionInHistory] = React.useState<number>(0); const [items, setItems] = React.useState(props.items); + const [hoveringButton, setHoveringButton] = React.useState(false); + const [hoveringContextDropdown, setHoveringContextDropdown] = + React.useState(false); + const [highlightedCodeSections, setHighlightedCodeSections] = React.useState( + props.highlightedCodeSections || [ + { + filepath: "test.ts", + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + contents: "import * as a from 'a';", + }, + ] + ); + + useEffect(() => { + setHighlightedCodeSections(props.highlightedCodeSections || []); + }, [props.highlightedCodeSections]); + const { isOpen, getToggleButtonProps, @@ -111,90 +154,144 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { }; return ( - <div className="flex px-2" ref={divRef} hidden={!isOpen}> - <MainTextInput - disabled={props.disabled} - placeholder="Type '/' to see available slash commands." - {...getInputProps({ - 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`; - }, - onKeyDown: (event) => { - if (event.key === "Enter" && event.shiftKey) { - // Prevent Downshift's default 'Enter' behavior. - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if ( - event.key === "Enter" && - (!isOpen || items.length === 0) - ) { - // Prevent Downshift's default 'Enter' behavior. - (event.nativeEvent as any).preventDownshiftDefault = true; - if (props.onEnter) props.onEnter(event); - setInputValue(""); - const value = event.currentTarget.value; - if (value !== "") { - setPositionInHistory(history.length + 1); - setHistory([...history, value]); + <> + <div className="flex px-2" ref={divRef} hidden={!isOpen}> + <MainTextInput + disabled={props.disabled} + placeholder="Type '/' to see available slash commands." + {...getInputProps({ + 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("@")); + }, + onKeyDown: (event) => { + if (event.key === "Enter" && event.shiftKey) { + // Prevent Downshift's default 'Enter' behavior. + (event.nativeEvent as any).preventDownshiftDefault = true; + } else if ( + event.key === "Enter" && + (!isOpen || items.length === 0) + ) { + // Prevent Downshift's default 'Enter' behavior. + (event.nativeEvent as any).preventDownshiftDefault = true; + if (props.onEnter) props.onEnter(event); + setInputValue(""); + const value = event.currentTarget.value; + if (value !== "") { + setPositionInHistory(history.length + 1); + setHistory([...history, value]); + } + } else if (event.key === "Tab" && items.length > 0) { + setInputValue(items[0].name); + event.preventDefault(); + } else if ( + event.key === "ArrowUp" || + (event.key === "ArrowDown" && + event.currentTarget.value.split("\n").length > 1) + ) { + (event.nativeEvent as any).preventDownshiftDefault = true; + } else if ( + event.key === "ArrowUp" && + event.currentTarget.value.split("\n").length > 1 + ) { + if (positionInHistory == 0) return; + setInputValue(history[positionInHistory - 1]); + setPositionInHistory((prev) => prev - 1); + } else if ( + event.key === "ArrowDown" && + event.currentTarget.value.split("\n").length > 1 + ) { + if (positionInHistory < history.length - 1) { + setInputValue(history[positionInHistory + 1]); + } + setPositionInHistory((prev) => + Math.min(prev + 1, history.length) + ); + } + }, + ref: ref as any, + })} + /> + <Ul + {...getMenuProps({ + ref: ulRef, + })} + showAbove={showAbove()} + ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + > + {isOpen && + items.map((item, index) => ( + <Li + key={`${item.name}${index}`} + {...getItemProps({ item, index })} + highlighted={highlightedIndex === index} + selected={selectedItem === item} + > + <span> + {item.name}: {item.description} + </span> + </Li> + ))} + </Ul> + </div> + <div className="px-2 flex gap-2 items-center flex-wrap"> + {highlightedCodeSections.map((section, idx) => ( + <PillButton + title={section.filepath} + onDelete={() => { + if (props.deleteContextItem) { + props.deleteContextItem(idx); } - } else if (event.key === "Tab" && items.length > 0) { - setInputValue(items[0].name); - event.preventDefault(); - } else if ( - event.key === "ArrowUp" || - (event.key === "ArrowDown" && - event.currentTarget.value.split("\n").length > 1) - ) { - (event.nativeEvent as any).preventDownshiftDefault = true; - } else if ( - event.key === "ArrowUp" && - event.currentTarget.value.split("\n").length > 1 - ) { - if (positionInHistory == 0) return; - setInputValue(history[positionInHistory - 1]); - setPositionInHistory((prev) => prev - 1); - } else if ( - event.key === "ArrowDown" && - event.currentTarget.value.split("\n").length > 1 - ) { - if (positionInHistory < history.length - 1) { - setInputValue(history[positionInHistory + 1]); + setHighlightedCodeSections((prev) => { + const newSections = [...prev]; + newSections.splice(idx, 1); + return newSections; + }); + }} + onHover={(val: boolean) => { + if (val) { + setHoveringButton(val); + } else { + setTimeout(() => { + setHoveringButton(val); + }, 100); } - setPositionInHistory((prev) => - Math.min(prev + 1, history.length) - ); - } - }, - ref: ref as any, - })} - /> - <Ul - {...getMenuProps({ - ref: ulRef, - })} - showAbove={showAbove()} - ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0} + }} + /> + ))} + + <span className="text-trueGray-400 ml-auto mr-4 text-xs"> + Highlight code to include as context.{" "} + {highlightedCodeSections.length > 0 && + "Otherwise using entire currently open file."} + </span> + </div> + <ContextDropdown + onMouseEnter={() => { + setHoveringContextDropdown(true); + }} + onMouseLeave={() => { + setHoveringContextDropdown(false); + }} + hidden={!hoveringContextDropdown && !hoveringButton} > - {isOpen && - items.map((item, index) => ( - <Li - key={`${item.name}${index}`} - {...getItemProps({ item, index })} - highlighted={highlightedIndex === index} - selected={selectedItem === item} - > - <span> - {item.name}: {item.description} - </span> - </Li> - ))} - </Ul> - </div> + {highlightedCodeSections.map((section, idx) => ( + <> + <p>{section.filepath}</p> + <CodeBlock showCopy={false} key={idx}> + {section.contents} + </CodeBlock> + </> + ))} + </ContextDropdown> + </> ); }); diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx index ef6719b7..5295799a 100644 --- a/extension/react-app/src/components/ContinueButton.tsx +++ b/extension/react-app/src/components/ContinueButton.tsx @@ -6,6 +6,7 @@ import { RootStore } from "../redux/store"; let StyledButton = styled(Button)` margin: auto; + margin-top: 8px; display: grid; grid-template-columns: 30px 1fr; align-items: center; diff --git a/extension/react-app/src/components/IterationContainer.tsx b/extension/react-app/src/components/IterationContainer.tsx deleted file mode 100644 index a0053519..00000000 --- a/extension/react-app/src/components/IterationContainer.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useState } from "react"; -import styled from "styled-components"; -import { - defaultBorderRadius, - MainContainerWithBorder, - secondaryDark, - vscBackground, -} from "."; -import { RangeInFile, FileEdit } from "../../../src/client"; -import CodeBlock from "./CodeBlock"; -import SubContainer from "./SubContainer"; - -import { ChevronDown, ChevronRight } from "@styled-icons/heroicons-outline"; - -export interface IterationContext { - codeSelections: RangeInFile[]; - instruction: string; - suggestedChanges: FileEdit[]; - status: "waiting" | "accepted" | "rejected"; - summary?: string; - action: string; - error?: string; -} - -interface IterationContainerProps { - iterationContext: IterationContext; -} - -const IterationContainerDiv = styled.div<{ open: boolean }>` - background-color: ${(props) => (props.open ? vscBackground : secondaryDark)}; - border-radius: ${defaultBorderRadius}; - padding: ${(props) => (props.open ? "2px" : "8px")}; -`; - -function IterationContainer(props: IterationContainerProps) { - const [open, setOpen] = useState(false); - - return ( - <MainContainerWithBorder className="m-2 overflow-hidden"> - <IterationContainerDiv open={open}> - <p - className="m-2 cursor-pointer" - onClick={() => setOpen((prev) => !prev)} - > - {open ? <ChevronDown size="1.4em" /> : <ChevronRight size="1.4em" />} - {props.iterationContext.summary || - props.iterationContext.codeSelections - .map((cs) => cs.filepath) - .join("\n")} - </p> - - {open && ( - <> - <SubContainer title="Action"> - {props.iterationContext.action} - </SubContainer> - {props.iterationContext.error && ( - <SubContainer title="Error"> - <CodeBlock>{props.iterationContext.error}</CodeBlock> - </SubContainer> - )} - {props.iterationContext.suggestedChanges.map((sc) => { - return ( - <SubContainer title="Suggested Change"> - {sc.filepath} - <CodeBlock>{sc.replacement}</CodeBlock> - </SubContainer> - ); - })} - </> - )} - </IterationContainerDiv> - </MainContainerWithBorder> - ); -} - -export default IterationContainer; diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx new file mode 100644 index 00000000..33451db5 --- /dev/null +++ b/extension/react-app/src/components/PillButton.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { defaultBorderRadius } from "."; + +const Button = styled.button` + border: none; + color: white; + background-color: transparent; + border: 1px solid white; + border-radius: ${defaultBorderRadius}; + padding: 3px 6px; + + &:hover { + background-color: white; + color: black; + } +`; + +interface PillButtonProps { + onHover?: (arg0: boolean) => void; + onDelete?: () => void; + title: string; +} + +const PillButton = (props: PillButtonProps) => { + const [isHovered, setIsHovered] = useState(false); + return ( + <Button + onMouseEnter={() => { + setIsHovered(true); + if (props.onHover) { + props.onHover(true); + } + }} + onMouseLeave={() => { + setIsHovered(false); + if (props.onHover) { + props.onHover(false); + } + }} + > + <div + style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "4px" }} + > + <span>{props.title}</span> + <span + style={{ + cursor: "pointer", + color: "red", + borderLeft: "1px solid black", + paddingLeft: "4px", + }} + hidden={!isHovered} + onClick={() => { + props.onDelete?.(); + props.onHover?.(false); + }} + > + X + </span> + </div> + </Button> + ); +}; + +export default PillButton; diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index 02c04548..cb83f20a 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -246,10 +246,14 @@ function StepContainer(props: StepContainerProps) { className="overflow-x-scroll" components={{ pre: ({ node, ...props }) => { - return <CodeBlock children={props.children[0]} />; + return ( + <CodeBlock + children={(props.children[0] as any).props.children[0]} + /> + ); }, code: ({ node, ...props }) => { - return <StyledCode children={props.children[0]} />; + return <StyledCode children={props.children[0] as any} />; }, }} > diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index 3d8e0a38..228e9a53 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -20,6 +20,8 @@ abstract class AbstractContinueGUIClientProtocol { abstract retryAtIndex(index: number): void; abstract deleteAtIndex(index: number): void; + + abstract deleteContextItemAtIndex(index: number): void; } export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts index f43a66ff..a0c38c0f 100644 --- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts +++ b/extension/react-app/src/hooks/useContinueGUIProtocol.ts @@ -70,6 +70,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { deleteAtIndex(index: number) { this.messenger.send("delete_at_index", { index }); } + + deleteContextItemAtIndex(index: number) { + this.messenger.send("delete_context_item_at_index", { index }); + } } export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index bc0e5365..e0b32a24 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -69,6 +69,7 @@ function GUI(props: GUIProps) { const [usingFastModel, setUsingFastModel] = useState(false); const [waitingForSteps, setWaitingForSteps] = useState(false); const [userInputQueue, setUserInputQueue] = useState<string[]>([]); + const [highlightedRanges, setHighlightedRanges] = useState([]); const [availableSlashCommands, setAvailableSlashCommands] = useState< { name: string; description: string }[] >([]); @@ -83,60 +84,6 @@ function GUI(props: GUIProps) { timeline: [ { step: { - name: "SequentialStep", - hide: true, - description: "Running step: SequentialStep", - system_message: null, - chat_context: [], - manage_own_chat_context: false, - steps: [ - { - name: "Welcome to Continue", - hide: false, - description: `Welcome to Continue`, - system_message: null, - chat_context: [], - manage_own_chat_context: false, - message: `# Welcome to Continue - - _If it's your first time using Continue, it can take up to a minute for the server to install._ - - Continue is not perfect, but a great tool to add to your toolbox. These are the tasks that Continue is currently best at: - - - Highlight a section of code and instruct Continue to refactor it, e.g. \`"/edit make this use more descriptive variable names"\` - - Ask questions of the open file, e.g. \`"/explain what is the purpose of each of these if statements?"\` - - Ask Continue to build the scaffolding of a new file from scratch, e.g. \`"add a React component for syntax highlighted code"\` - - You can use "slash commands" to directly instruct Continue what to do, or just enter a request and it will automatically decide next steps. To see the list of available slash commands, type '/'. - - If you highlight code, edits and explanations will be localized to the highlighted range. Otherwise, the currently open file is used. In both cases, the code is combined with the previous steps to construct the context. - `, - }, - { - name: "Welcome to Continue!", - hide: true, - description: "Welcome to Continue!", - system_message: null, - chat_context: [], - manage_own_chat_context: false, - }, - { - name: "StepsOnStartupStep", - hide: true, - description: "Running steps on startup", - system_message: null, - chat_context: [], - manage_own_chat_context: false, - }, - ], - }, - observation: null, - depth: 0, - deleted: false, - active: false, - }, - { - step: { name: "Welcome to Continue", hide: false, description: @@ -147,167 +94,13 @@ function GUI(props: GUIProps) { message: "Type '/' to see the list of available slash commands. If you highlight code, edits and explanations will be localized to the highlighted range. Otherwise, the currently open file is used. In both cases, the code is combined with the previous steps to construct the context.", }, - observation: { - text: "Type '/' to see the list of available slash commands. If you highlight code, edits and explanations will be localized to the highlighted range. Otherwise, the currently open file is used. In both cases, the code is combined with the previous steps to construct the context.", - }, - depth: 1, - deleted: false, - active: false, - }, - { - step: { - name: "Welcome to Continue!", - hide: true, - description: "Welcome to Continue!", - system_message: null, - chat_context: [], - manage_own_chat_context: false, - }, - observation: null, - depth: 1, - deleted: false, - active: false, - }, - { - step: { - name: "StepsOnStartupStep", - hide: true, - description: "Running steps on startup", - system_message: null, - chat_context: [], - manage_own_chat_context: false, - }, - observation: null, - depth: 1, + depth: 0, deleted: false, active: false, }, ], current_index: 3, } as any); - // { - // timeline: [ - // { - // step: { - // name: "Waiting for user input", - // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // description: - // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py` and ```\nprint(sum(first, second))\n```\n- Testing\n- Testing 2\n- Testing 3", - // }, - // observation: { - // title: "ERROR FOUND", - // error: - // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'", - // }, - // output: [ - // { - // traceback: { - // frames: [ - // { - // filepath: - // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // lineno: 7, - // function: "<module>", - // code: "print(sum(first, second))", - // }, - // ], - // message: "unsupported operand type(s) for +: 'int' and 'str'", - // error_type: - // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError', - // full_traceback: - // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'", - // }, - // }, - // null, - // ], - // }, - // { - // step: { - // name: "EditCodeStep", - // range_in_files: [ - // { - // filepath: - // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // range: { - // start: { - // line: 0, - // character: 0, - // }, - // end: { - // line: 6, - // character: 25, - // }, - // }, - // }, - // ], - // prompt: - // "I ran into this problem with my Python code:\n\n Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'\n\n Below are the files that might need to be fixed:\n\n {code}\n\n This is what the code should be in order to avoid the problem:\n", - // description: - // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py` and\n```python\nprint(sum(first, second))\n```\n- Testing\n- Testing 2\n- Testing 3", - // }, - // output: [ - // null, - // { - // reversible: true, - // actions: [ - // { - // reversible: true, - // filesystem: {}, - // filepath: - // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // range: { - // start: { - // line: 0, - // character: 0, - // }, - // end: { - // line: 6, - // character: 25, - // }, - // }, - // replacement: - // "\nfrom sum import sum\n\nfirst = 1\nsecond = 2\n\nprint(sum(first, second))", - // }, - // ], - // }, - // ], - // }, - // { - // active: false, - // step: { - // name: "SolveTracebackStep", - // traceback: { - // frames: [ - // { - // filepath: - // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // lineno: 7, - // function: "<module>", - // code: "print(sum(first, second))", - // }, - // ], - // message: "unsupported operand type(s) for +: 'int' and 'str'", - // error_type: - // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError', - // full_traceback: - // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'", - // }, - // description: "Running step: SolveTracebackStep", - // }, - // output: [null, null], - // }, - // { - // step: { - // name: "RunCodeStep", - // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py", - // description: - // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`", - // }, - // output: [null, null], - // }, - // ], - // current_index: 3, - // } as any); const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); const [feedbackDialogMessage, setFeedbackDialogMessage] = useState(""); @@ -357,6 +150,7 @@ function GUI(props: GUIProps) { topGuiDivRef.current?.offsetHeight - window.scrollY < 100; setWaitingForSteps(state.active); setHistory(state.history); + setHighlightedRanges(state.highlighted_ranges); setUserInputQueue(state.user_input_queue); setStepsOpen((prev) => { const nextStepsOpen = [...prev]; @@ -393,6 +187,13 @@ function GUI(props: GUIProps) { const mainTextInputRef = useRef<HTMLInputElement>(null); + const deleteContextItem = useCallback( + (idx: number) => { + client?.deleteContextItemAtIndex(idx); + }, + [client] + ); + useEffect(() => { if (mainTextInputRef.current) { mainTextInputRef.current.focus(); @@ -535,6 +336,8 @@ function GUI(props: GUIProps) { }} onInputValueChange={() => {}} items={availableSlashCommands} + highlightedCodeSections={highlightedRanges} + deleteContextItem={deleteContextItem} /> <ContinueButton onClick={onMainTextInput} /> </TopGUIDiv> diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index b39e6565..999bca88 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -12,18 +12,17 @@ import { acceptSuggestionCommand, rejectSuggestionCommand, } from "./suggestions"; -import { debugPanelWebview, setupDebugPanel } from "./debugPanel"; import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; import fs = require("fs"); import { WebsocketMessenger } from "./util/messenger"; -import { decorationManager } from "./decorations"; - class IdeProtocolClient { private messenger: WebsocketMessenger | null = null; private readonly context: vscode.ExtensionContext; private _makingEdit = 0; + private _highlightDebounce: NodeJS.Timeout | null = null; + constructor(serverUrl: string, context: vscode.ExtensionContext) { this.context = context; @@ -65,6 +64,36 @@ class IdeProtocolClient { // this._makingEdit--; // } // }); + + // Setup listeners for any file changes in open editors + vscode.window.onDidChangeTextEditorSelection((event) => { + if (this._highlightDebounce) { + clearTimeout(this._highlightDebounce); + } + this._highlightDebounce = setTimeout(() => { + const highlightedCode = event.textEditor.selections + .filter((s) => !s.isEmpty) + .map((selection) => { + const range = new vscode.Range(selection.start, selection.end); + const contents = event.textEditor.document.getText(range); + return { + filepath: event.textEditor.document.uri.fsPath, + contents, + range: { + start: { + line: selection.start.line, + character: selection.start.character, + }, + end: { + line: selection.end.line, + character: selection.end.character, + }, + }, + }; + }); + this.sendHighlightedCode(highlightedCode); + }, 100); + }); } async handleMessage( @@ -376,6 +405,10 @@ class IdeProtocolClient { this.messenger?.send("commandOutput", { output }); } + sendHighlightedCode(highlightedCode: (RangeInFile & { contents: string })[]) { + this.messenger?.send("highlightedCodePush", { highlightedCode }); + } + sendAcceptRejectSuggestion(accepted: boolean) { this.messenger?.send("acceptRejectSuggestion", { accepted }); } |