diff options
author | Nate Sesti <sestinj@gmail.com> | 2023-07-01 12:36:57 -0700 |
---|---|---|
committer | Nate Sesti <sestinj@gmail.com> | 2023-07-01 12:36:57 -0700 |
commit | 36577b8e94809da47a540499132774a0fe2c085d (patch) | |
tree | e912172745fedf947b0f393ceaa2d36aaa703626 | |
parent | 95ce61f2655dcbeb4fed019b6a9d8a632bad7adc (diff) | |
download | sncontinue-36577b8e94809da47a540499132774a0fe2c085d.tar.gz sncontinue-36577b8e94809da47a540499132774a0fe2c085d.tar.bz2 sncontinue-36577b8e94809da47a540499132774a0fe2c085d.zip |
explicit context pill buttons
-rw-r--r-- | continuedev/src/continuedev/core/autopilot.py | 40 | ||||
-rw-r--r-- | continuedev/src/continuedev/core/main.py | 2 | ||||
-rw-r--r-- | continuedev/src/continuedev/core/sdk.py | 7 | ||||
-rw-r--r-- | continuedev/src/continuedev/models/filesystem.py | 29 | ||||
-rw-r--r-- | continuedev/src/continuedev/models/main.py | 6 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/gui.py | 7 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/ide.py | 10 | ||||
-rw-r--r-- | continuedev/src/continuedev/server/ide_protocol.py | 6 | ||||
-rw-r--r-- | continuedev/src/continuedev/steps/core/core.py | 3 | ||||
-rw-r--r-- | extension/react-app/src/components/CodeBlock.tsx | 73 | ||||
-rw-r--r-- | extension/react-app/src/components/ComboBox.tsx | 258 | ||||
-rw-r--r-- | extension/react-app/src/components/IterationContainer.tsx | 77 | ||||
-rw-r--r-- | extension/react-app/src/components/StepContainer.tsx | 8 | ||||
-rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 2 | ||||
-rw-r--r-- | extension/react-app/src/hooks/useContinueGUIProtocol.ts | 4 | ||||
-rw-r--r-- | extension/react-app/src/tabs/gui.tsx | 221 | ||||
-rw-r--r-- | extension/src/continueIdeClient.ts | 39 |
17 files changed, 369 insertions, 423 deletions
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 3c7fbdef..b9e61c63 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,30 @@ 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) + + new_ranges = [] + for rif in range_in_files: + found_overlap = False + for i in range(len(self._highlighted_ranges)): + hr = self._highlighted_ranges[i] + if hr.filepath == rif.filepath and hr.range.overlaps_with(rif.range): + new_ranges.append(rif.union(hr)) + found_overlap = True + self._highlighted_ranges.pop(i) + break + + if not found_overlap: + new_ranges.append(rif) + + self._highlighted_ranges += new_ranges + await self.update_subscribers() + _step_depth: int = 0 async def retry_at_index(self, index: int): @@ -135,6 +167,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 d95a233f..632f8683 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..fc1c3f13 100644 --- a/continuedev/src/continuedev/models/filesystem.py +++ b/continuedev/src/continuedev/models/filesystem.py @@ -23,11 +23,40 @@ 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 the start and end positions of the overlap + overlap_start = max(first.range.start, + second.range.start) - first.range.start + overlap_end = min(first.range.end, second.range.end) - \ + first.range.start + + # Calculate the new contents by removing the overlap + union_contents = first.contents[:overlap_start] + \ + second.contents[overlap_start:overlap_end] + \ + first.contents[overlap_end:] + + return RangeInFileWithContents( + filepath=first.filepath, + range=first.range.union(second.range), + contents=union_contents + ) + @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..101be4ae 100644 --- a/continuedev/src/continuedev/models/main.py +++ b/continuedev/src/continuedev/models/main.py @@ -49,6 +49,12 @@ class Range(BaseModel): 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 e1f19447..f3deecdb 100644 --- a/continuedev/src/continuedev/server/ide.py +++ b/continuedev/src/continuedev/server/ide.py @@ -8,7 +8,7 @@ from fastapi import WebSocket, Body, APIRouter from uvicorn.main import Server 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 @@ -139,6 +139,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) @@ -229,6 +232,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 d2dafa9a..17a09c3d 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/core/core.py b/continuedev/src/continuedev/steps/core/core.py index a84263cc..729f5e66 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -311,8 +311,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..34027a42 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,44 @@ import { secondaryDark, vscBackground, } from "."; +import CodeBlock from "./CodeBlock"; +import { RangeInFile } from "../../../src/client"; 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-left: 8px; + margin-right: 8px; + margin-top: -12px; + outline: 1px solid orange; +`; + +const PillButton = styled.button` + display: flex; + justify-content: space-between; + align-items: center; + border: none; + color: white; + background-color: gray; + border-radius: 50px; + padding: 5px 10px; + margin: 5px 0; + cursor: pointer; + + &:hover { + background-color: ${buttonColor}; + } +`; + const MainTextInput = styled.textarea` resize: none; @@ -20,6 +56,7 @@ const MainTextInput = styled.textarea` width: 100%; background-color: ${vscBackground}; color: white; + z-index: 1; &:focus { border: 1px solid transparent; @@ -49,6 +86,7 @@ const Ul = styled.ul<{ border-radius: ${defaultBorderRadius}; overflow: hidden; border: 0.5px solid gray; + z-index: 2; `; const Li = styled.li<{ @@ -71,6 +109,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 +118,24 @@ 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 [showContextDropdown, setShowContextDropdown] = 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 +169,124 @@ 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) + ); } - } 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]); + }, + 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> + <ContextDropdown hidden={!showContextDropdown}> + <p>Highlight code to include as context:</p> + {highlightedCodeSections.map((section, idx) => ( + <> + <p>{section.filepath}</p> + <CodeBlock showCopy={false} key={idx}> + {section.contents} + </CodeBlock> + </> + ))} + </ContextDropdown> + <div className="px-2"> + {highlightedCodeSections.map((section, idx) => ( + <PillButton + onClick={() => { + console.log("delete context item", idx); + if (props.deleteContextItem) { + props.deleteContextItem(idx); } - 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> + setHighlightedCodeSections((prev) => { + const newSections = [...prev]; + newSections.splice(idx, 1); + return newSections; + }); + }} + > + {section.filepath} + </PillButton> + ))} + </div> + </> ); }); 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/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 39925fc5..658aa503 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); @@ -356,6 +149,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]; @@ -392,6 +186,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(); @@ -533,6 +334,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 21104abe..8f45b849 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( @@ -375,6 +404,10 @@ class IdeProtocolClient { sendCommandOutput(output: string) { this.messenger?.send("commandOutput", { output }); } + + sendHighlightedCode(highlightedCode: (RangeInFile & { contents: string })[]) { + this.messenger?.send("highlightedCodePush", { highlightedCode }); + } } export default IdeProtocolClient; |