summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--continuedev/src/continuedev/core/autopilot.py15
-rw-r--r--continuedev/src/continuedev/core/config.py17
-rw-r--r--continuedev/src/continuedev/core/main.py1
-rw-r--r--continuedev/src/continuedev/core/policy.py66
-rw-r--r--continuedev/src/continuedev/server/gui.py7
-rw-r--r--continuedev/src/continuedev/steps/chat.py6
-rw-r--r--continuedev/src/continuedev/steps/core/core.py102
-rw-r--r--extension/package-lock.json31
-rw-r--r--extension/package.json18
-rw-r--r--extension/react-app/package-lock.json27
-rw-r--r--extension/react-app/package.json1
-rw-r--r--extension/react-app/src/components/ComboBox.tsx36
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx4
-rw-r--r--extension/react-app/src/components/PillButton.tsx27
-rw-r--r--extension/react-app/src/components/StepContainer.tsx12
-rw-r--r--extension/react-app/src/components/UserInputContainer.tsx6
-rw-r--r--extension/react-app/src/hooks/ContinueGUIClientProtocol.ts2
-rw-r--r--extension/react-app/src/hooks/useContinueGUIProtocol.ts4
-rw-r--r--extension/react-app/src/main.tsx4
-rw-r--r--extension/react-app/src/tabs/gui.tsx24
-rw-r--r--extension/src/commands.ts4
-rw-r--r--extension/src/continueIdeClient.ts38
-rw-r--r--extension/src/diffs.ts140
-rw-r--r--extension/src/lang-server/codeLens.ts56
24 files changed, 486 insertions, 162 deletions
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index 313ceded..b1c4f471 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -69,7 +69,8 @@ class Autopilot(ContinueBaseModel):
user_input_queue=self._main_user_input_queue,
default_model=self.continue_sdk.config.default_model,
highlighted_ranges=self._highlighted_ranges,
- slash_commands=self.get_available_slash_commands()
+ slash_commands=self.get_available_slash_commands(),
+ adding_highlighted_code=self._adding_highlighted_code,
)
def get_available_slash_commands(self) -> List[Dict]:
@@ -140,8 +141,16 @@ class Autopilot(ContinueBaseModel):
await self._run_singular_step(step)
_highlighted_ranges: List[RangeInFileWithContents] = []
+ _adding_highlighted_code: bool = False
async def handle_highlighted_code(self, range_in_files: List[RangeInFileWithContents]):
+ if not self._adding_highlighted_code:
+ return
+
+ # Filter out rifs from ~/.continue/diffs folder
+ range_in_files = [
+ rif for rif in range_in_files if not os.path.dirname(rif.filepath) == os.path.expanduser("~/.continue/diffs")]
+
workspace_path = self.continue_sdk.ide.workspace_directory
for rif in range_in_files:
rif.filepath = os.path.basename(rif.filepath)
@@ -186,6 +195,10 @@ class Autopilot(ContinueBaseModel):
self._highlighted_ranges = kept_ranges
await self.update_subscribers()
+ async def toggle_adding_highlighted_code(self):
+ self._adding_highlighted_code = not self._adding_highlighted_code
+ 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/config.py b/continuedev/src/continuedev/core/config.py
index 9e8541dc..ff7b8cb0 100644
--- a/continuedev/src/continuedev/core/config.py
+++ b/continuedev/src/continuedev/core/config.py
@@ -33,11 +33,11 @@ DEFAULT_SLASH_COMMANDS = [
description="Edit code in the current file or the highlighted code",
step_name="EditHighlightedCodeStep",
),
- SlashCommand(
- name="explain",
- description="Reply to instructions or a question with previous steps and the highlighted code or current file as context",
- step_name="SimpleChatStep",
- ),
+ # SlashCommand(
+ # name="explain",
+ # description="Reply to instructions or a question with previous steps and the highlighted code or current file as context",
+ # step_name="SimpleChatStep",
+ # ),
SlashCommand(
name="config",
description="Open the config file to create new and edit existing slash commands",
@@ -71,7 +71,10 @@ class ContinueConfig(BaseModel):
allow_anonymous_telemetry: Optional[bool] = True
default_model: Literal["gpt-3.5-turbo", "gpt-3.5-turbo-16k",
"gpt-4"] = 'gpt-4'
- custom_commands: Optional[List[CustomCommand]] = []
+ custom_commands: Optional[List[CustomCommand]] = [CustomCommand(
+ name="test",
+ prompt="Write a comprehensive set of unit tests for the selected code. It should setup, run tests that check for correctness including important edge cases, and teardown. Ensure that the tests are complete and sophisticated. Give the tests just as chat output, don't edit any file.",
+ )]
slash_commands: Optional[List[SlashCommand]] = DEFAULT_SLASH_COMMANDS
on_traceback: Optional[List[OnTracebackSteps]] = [
OnTracebackSteps(step_name="DefaultOnTracebackStep")]
@@ -126,7 +129,7 @@ def load_global_config() -> ContinueConfig:
config_path = os.path.join(global_dir, 'config.json')
if not os.path.exists(config_path):
with open(config_path, 'w') as f:
- json.dump(dict(ContinueConfig()), f)
+ json.dump(ContinueConfig().dict(), f)
with open(config_path, 'r') as f:
try:
config_dict = json.load(f)
diff --git a/continuedev/src/continuedev/core/main.py b/continuedev/src/continuedev/core/main.py
index 8bad09d1..28fd964e 100644
--- a/continuedev/src/continuedev/core/main.py
+++ b/continuedev/src/continuedev/core/main.py
@@ -207,6 +207,7 @@ class FullState(ContinueBaseModel):
default_model: str
highlighted_ranges: List[RangeInFileWithContents]
slash_commands: List[SlashCommandDescription]
+ adding_highlighted_code: bool
class ContinueSDK:
diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py
index b0853380..fc9266ab 100644
--- a/continuedev/src/continuedev/core/policy.py
+++ b/continuedev/src/continuedev/core/policy.py
@@ -1,5 +1,5 @@
from textwrap import dedent
-from typing import List, Tuple, Type
+from typing import List, Tuple, Type, Union
from ..steps.welcome import WelcomeStep
from .config import ContinueConfig
@@ -22,6 +22,34 @@ from ..libs.util.step_name_to_steps import get_step_from_name
from ..steps.custom_command import CustomCommandStep
+def parse_slash_command(inp: str, config: ContinueConfig) -> Union[None, Step]:
+ """
+ Parses a slash command, returning the command name and the rest of the input.
+ """
+ if inp.startswith("/"):
+ command_name = inp.split(" ")[0]
+ after_command = " ".join(inp.split(" ")[1:])
+
+ for slash_command in config.slash_commands:
+ if slash_command.name == command_name[1:]:
+ params = slash_command.params
+ params["user_input"] = after_command
+ return get_step_from_name(slash_command.step_name, params)
+ return None
+
+
+def parse_custom_command(inp: str, config: ContinueConfig) -> Union[None, Step]:
+ command_name = inp.split(" ")[0]
+ after_command = " ".join(inp.split(" ")[1:])
+ for custom_cmd in config.custom_commands:
+ if custom_cmd.name == command_name[1:]:
+ slash_command = parse_slash_command(custom_cmd.prompt, config)
+ if slash_command is not None:
+ return slash_command
+ return CustomCommandStep(name=custom_cmd.name, prompt=custom_cmd.prompt, user_input=after_command)
+ return None
+
+
class DemoPolicy(Policy):
ran_code_last: bool = False
@@ -46,34 +74,14 @@ class DemoPolicy(Policy):
# This could be defined with ObservationTypePolicy. Ergonomics not right though.
user_input = observation.user_input
- if user_input.startswith("/"):
- command_name = user_input.split(" ")[0]
- after_command = " ".join(user_input.split(" ")[1:])
- for slash_command in config.slash_commands:
- if slash_command.name == command_name[1:]:
- params = slash_command.params
- params["user_input"] = after_command
- return get_step_from_name(slash_command.step_name, params)
-
- for custom_cmd in config.custom_commands:
- if custom_cmd.name == command_name[1:]:
- return CustomCommandStep(name=custom_cmd.name, prompt=custom_cmd.prompt, user_input=after_command)
+ slash_command = parse_slash_command(user_input, config)
+ if slash_command is not None:
+ return slash_command
- # return EditHighlightedCodeStep(user_input=user_input)
- return ChatWithFunctions(user_input=user_input)
- return NLDecisionStep(user_input=user_input, steps=[
- (EditHighlightedCodeStep(user_input=user_input),
- "Edit the highlighted code"),
- # AnswerQuestionChroma(question=user_input),
- # EditFileChroma(request=user_input),
- (SimpleChatStep(user_input=user_input),
- "Respond to the user with a chat message. Can answer questions about code or anything else."),
- ], default_step=EditHighlightedCodeStep(user_input=user_input))
+ custom_command = parse_custom_command(user_input, config)
+ if custom_command is not None:
+ return custom_command
- state = history.get_current()
+ return SimpleChatStep(user_input=user_input)
- if observation is not None and isinstance(observation, TracebackObservation):
- self.ran_code_last = False
- return SolveTracebackStep(traceback=observation.traceback)
- else:
- return None
+ return None
diff --git a/continuedev/src/continuedev/server/gui.py b/continuedev/src/continuedev/server/gui.py
index 4e960f7c..fa573b37 100644
--- a/continuedev/src/continuedev/server/gui.py
+++ b/continuedev/src/continuedev/server/gui.py
@@ -85,6 +85,8 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
self.on_delete_at_index(data["index"])
elif message_type == "delete_context_at_indices":
self.on_delete_context_at_indices(data["indices"])
+ elif message_type == "toggle_adding_highlighted_code":
+ self.on_toggle_adding_highlighted_code()
except Exception as e:
print(e)
@@ -128,6 +130,11 @@ class GUIProtocolServer(AbstractGUIProtocolServer):
self.session.autopilot.delete_context_at_indices(indices)
)
+ def on_toggle_adding_highlighted_code(self):
+ asyncio.create_task(
+ self.session.autopilot.toggle_adding_highlighted_code()
+ )
+
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, session: Session = Depends(websocket_session)):
diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py
index db3f9d7f..8f88244c 100644
--- a/continuedev/src/continuedev/steps/chat.py
+++ b/continuedev/src/continuedev/steps/chat.py
@@ -107,12 +107,8 @@ class RunTerminalCommandStep(Step):
description: str = "Run a terminal command."
command: str
- async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]:
- return f"Ran the terminal command `{self.command}`."
-
async def run(self, sdk: ContinueSDK):
- await sdk.wait_for_user_confirmation(f"Run the following terminal command?\n\n```bash\n{self.command}\n```")
- await sdk.run(self.command)
+ self.description = f"Copy this command and run in your terminal:\n\n```bash\n{self.command}\n```"
class ViewDirectoryTreeStep(Step):
diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py
index b9f0da35..3a7c8876 100644
--- a/continuedev/src/continuedev/steps/core/core.py
+++ b/continuedev/src/continuedev/steps/core/core.py
@@ -286,6 +286,7 @@ class DefaultModelEditCodeStep(Step):
return "```" in line or "<modified_code_to_edit>" in line or "<file_prefix>" in line or "</file_prefix>" in line or "<file_suffix>" in line or "</file_suffix>" in line or "<user_request>" in line or "</user_request>" in line or "<code_to_edit>" in line
async def stream_rif(self, rif: RangeInFileWithContents, sdk: ContinueSDK):
+ await sdk.ide.saveFile(rif.filepath)
full_file_contents = await sdk.ide.readFile(rif.filepath)
file_prefix, contents, file_suffix, model_to_use = await self.get_prompt_parts(
@@ -295,6 +296,17 @@ class DefaultModelEditCodeStep(Step):
prompt = self.compile_prompt(file_prefix, contents, file_suffix, sdk)
full_file_contents_lines = full_file_contents.split("\n")
+ async def sendDiffUpdate(lines: List[str], sdk: ContinueSDK):
+ nonlocal full_file_contents_lines, rif
+
+ completion = "\n".join(lines)
+
+ full_prefix_lines = full_file_contents_lines[:rif.range.start.line]
+ full_suffix_lines = full_file_contents_lines[rif.range.end.line + 1:]
+ new_file_contents = "\n".join(
+ full_prefix_lines) + "\n" + completion + "\n" + "\n".join(full_suffix_lines)
+ await sdk.ide.showDiff(rif.filepath, new_file_contents)
+
# Important state variables
# -------------------------
original_lines = [] if rif.contents == "" else rif.contents.split("\n")
@@ -320,8 +332,9 @@ class DefaultModelEditCodeStep(Step):
# Highlight the line to show progress
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")
+ if False:
+ 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")
if len(current_block_lines) == 0:
# Set this as the start of the next block
@@ -370,12 +383,14 @@ class DefaultModelEditCodeStep(Step):
replacement = "\n".join(current_block_lines)
start_line = current_block_start
end_line = current_block_start + index_of_last_line_in_block
- await sdk.ide.showSuggestion(FileEdit(
- filepath=rif.filepath,
- range=Range.from_shorthand(
- start_line, 0, end_line, 0),
- replacement=replacement
- ))
+
+ if False:
+ await sdk.ide.showSuggestion(FileEdit(
+ filepath=rif.filepath,
+ range=Range.from_shorthand(
+ start_line, 0, end_line, 0),
+ replacement=replacement
+ ))
# Reset current block / update variables
current_line_in_file += 1
@@ -435,16 +450,16 @@ class DefaultModelEditCodeStep(Step):
chunk_lines.pop() # because this will be an empty string
else:
unfinished_line = chunk_lines.pop()
- lines.extend(chunk_lines)
+ lines.extend(map(lambda l: common_whitespace + l, chunk_lines))
+
+ if True:
+ await sendDiffUpdate(lines, sdk)
# Deal with newly accumulated lines
for line in chunk_lines:
# Trailing whitespace doesn't matter
line = line.rstrip()
- # Add the common whitespace that was removed before prompting
- line = common_whitespace + line
-
# Lines that should signify the end of generation
if self.is_end_line(line):
break
@@ -463,7 +478,9 @@ class DefaultModelEditCodeStep(Step):
break
# If none of the above, insert the line!
- await handle_generated_line(line)
+ if False:
+ await handle_generated_line(line)
+
completion_lines_covered += 1
current_line_in_file += 1
@@ -475,34 +492,37 @@ class DefaultModelEditCodeStep(Step):
completion_lines_covered += 1
current_line_in_file += 1
- # If the current block isn't empty, add that suggestion
- if len(current_block_lines) > 0:
- # We have a chance to back-track here for blank lines that are repeats of the end of the original
- # Don't want to have the same ending in both the original and the generated, can just leave it there
- num_to_remove = 0
- for i in range(-1, -len(current_block_lines) - 1, -1):
- if len(original_lines_below_previous_blocks) == 0:
- break
- if current_block_lines[i] == original_lines_below_previous_blocks[-1]:
- num_to_remove += 1
- original_lines_below_previous_blocks.pop()
- else:
- break
- current_block_lines = current_block_lines[:-
- num_to_remove] if num_to_remove > 0 else current_block_lines
-
- # It's also possible that some lines match at the beginning of the block
- # while len(current_block_lines) > 0 and len(original_lines_below_previous_blocks) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[0]:
- # current_block_lines.pop(0)
- # original_lines_below_previous_blocks.pop(0)
- # current_block_start += 1
-
- await sdk.ide.showSuggestion(FileEdit(
- filepath=rif.filepath,
- range=Range.from_shorthand(
- current_block_start, 0, current_block_start + len(original_lines_below_previous_blocks), 0),
- replacement="\n".join(current_block_lines)
- ))
+ await sendDiffUpdate(lines, sdk)
+
+ if False:
+ # If the current block isn't empty, add that suggestion
+ if len(current_block_lines) > 0:
+ # We have a chance to back-track here for blank lines that are repeats of the end of the original
+ # Don't want to have the same ending in both the original and the generated, can just leave it there
+ num_to_remove = 0
+ for i in range(-1, -len(current_block_lines) - 1, -1):
+ if len(original_lines_below_previous_blocks) == 0:
+ break
+ if current_block_lines[i] == original_lines_below_previous_blocks[-1]:
+ num_to_remove += 1
+ original_lines_below_previous_blocks.pop()
+ else:
+ break
+ current_block_lines = current_block_lines[:-
+ num_to_remove] if num_to_remove > 0 else current_block_lines
+
+ # It's also possible that some lines match at the beginning of the block
+ # while len(current_block_lines) > 0 and len(original_lines_below_previous_blocks) > 0 and current_block_lines[0] == original_lines_below_previous_blocks[0]:
+ # current_block_lines.pop(0)
+ # original_lines_below_previous_blocks.pop(0)
+ # current_block_start += 1
+
+ await sdk.ide.showSuggestion(FileEdit(
+ filepath=rif.filepath,
+ range=Range.from_shorthand(
+ current_block_start, 0, current_block_start + len(original_lines_below_previous_blocks), 0),
+ replacement="\n".join(current_block_lines)
+ ))
# Record the completion
completion = "\n".join(lines)
diff --git a/extension/package-lock.json b/extension/package-lock.json
index c4a930de..b322acb7 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "continue",
- "version": "0.0.108",
+ "version": "0.0.113",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "continue",
- "version": "0.0.108",
+ "version": "0.0.113",
"license": "Apache-2.0",
"dependencies": {
"@electron/rebuild": "^3.2.10",
@@ -15,6 +15,7 @@
"@segment/analytics-node": "^0.0.1-beta.16",
"@sentry/node": "^7.57.0",
"@styled-icons/heroicons-outline": "^10.47.0",
+ "@styled-icons/heroicons-solid": "^10.47.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"axios": "^1.2.5",
"downshift": "^7.6.0",
@@ -2238,6 +2239,23 @@
"styled-components": "*"
}
},
+ "node_modules/@styled-icons/heroicons-solid": {
+ "version": "10.47.0",
+ "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz",
+ "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.20.7",
+ "@styled-icons/styled-icon": "^10.7.0"
+ },
+ "funding": {
+ "type": "GitHub",
+ "url": "https://github.com/sponsors/jacobwgillespie"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "styled-components": "*"
+ }
+ },
"node_modules/@styled-icons/styled-icon": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz",
@@ -13120,6 +13138,15 @@
"@styled-icons/styled-icon": "^10.7.0"
}
},
+ "@styled-icons/heroicons-solid": {
+ "version": "10.47.0",
+ "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz",
+ "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==",
+ "requires": {
+ "@babel/runtime": "^7.20.7",
+ "@styled-icons/styled-icon": "^10.7.0"
+ }
+ },
"@styled-icons/styled-icon": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz",
diff --git a/extension/package.json b/extension/package.json
index 87dd7ba6..09703da4 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -14,7 +14,7 @@
"displayName": "Continue",
"pricing": "Free",
"description": "The open-source coding autopilot",
- "version": "0.0.108",
+ "version": "0.0.113",
"publisher": "Continue",
"engines": {
"vscode": "^1.67.0"
@@ -39,6 +39,7 @@
"onView:continueGUIView"
],
"main": "./out/extension.js",
+ "browser": "./out/extension.js",
"contributes": {
"configuration": {
"title": "Continue",
@@ -87,6 +88,16 @@
"title": "Reject Suggestion"
},
{
+ "command": "continue.acceptDiff",
+ "category": "Continue",
+ "title": "Accept Diff"
+ },
+ {
+ "command": "continue.rejectDiff",
+ "category": "Continue",
+ "title": "Reject Diff"
+ },
+ {
"command": "continue.acceptAllSuggestions",
"category": "Continue",
"title": "Accept All Suggestions"
@@ -119,12 +130,12 @@
"key": "shift+ctrl+enter"
},
{
- "command": "continue.acceptAllSuggestions",
+ "command": "continue.acceptDiff",
"mac": "shift+cmd+enter",
"key": "shift+ctrl+enter"
},
{
- "command": "continue.rejectAllSuggestions",
+ "command": "continue.rejectDiff",
"mac": "shift+cmd+backspace",
"key": "shift+ctrl+backspace"
}
@@ -242,6 +253,7 @@
"@segment/analytics-node": "^0.0.1-beta.16",
"@sentry/node": "^7.57.0",
"@styled-icons/heroicons-outline": "^10.47.0",
+ "@styled-icons/heroicons-solid": "^10.47.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"axios": "^1.2.5",
"downshift": "^7.6.0",
diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json
index 85b8633b..fb13dffd 100644
--- a/extension/react-app/package-lock.json
+++ b/extension/react-app/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@styled-icons/heroicons-outline": "^10.47.0",
+ "@styled-icons/heroicons-solid": "^10.47.0",
"@types/vscode-webview": "^1.57.1",
"downshift": "^7.6.0",
"posthog-js": "^1.58.0",
@@ -691,6 +692,23 @@
"styled-components": "*"
}
},
+ "node_modules/@styled-icons/heroicons-solid": {
+ "version": "10.47.0",
+ "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz",
+ "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.20.7",
+ "@styled-icons/styled-icon": "^10.7.0"
+ },
+ "funding": {
+ "type": "GitHub",
+ "url": "https://github.com/sponsors/jacobwgillespie"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "styled-components": "*"
+ }
+ },
"node_modules/@styled-icons/styled-icon": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz",
@@ -3937,6 +3955,15 @@
"@styled-icons/styled-icon": "^10.7.0"
}
},
+ "@styled-icons/heroicons-solid": {
+ "version": "10.47.0",
+ "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz",
+ "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==",
+ "requires": {
+ "@babel/runtime": "^7.20.7",
+ "@styled-icons/styled-icon": "^10.7.0"
+ }
+ },
"@styled-icons/styled-icon": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz",
diff --git a/extension/react-app/package.json b/extension/react-app/package.json
index e46fdc8c..12701906 100644
--- a/extension/react-app/package.json
+++ b/extension/react-app/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@styled-icons/heroicons-outline": "^10.47.0",
+ "@styled-icons/heroicons-solid": "^10.47.0",
"@types/vscode-webview": "^1.57.1",
"downshift": "^7.6.0",
"posthog-js": "^1.58.0",
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 3e1f3e16..81b148b9 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -11,7 +11,12 @@ import CodeBlock from "./CodeBlock";
import { RangeInFile } from "../../../src/client";
import PillButton from "./PillButton";
import HeaderButtonWithText from "./HeaderButtonWithText";
-import { Trash, LockClosed, LockOpen } from "@styled-icons/heroicons-outline";
+import {
+ Trash,
+ LockClosed,
+ LockOpen,
+ Plus,
+} from "@styled-icons/heroicons-outline";
// #region styled components
const mainInputFontSize = 16;
@@ -50,7 +55,7 @@ const MainTextInput = styled.textarea`
}
`;
-const UlMaxHeight = 200;
+const UlMaxHeight = 400;
const Ul = styled.ul<{
hidden: boolean;
showAbove: boolean;
@@ -100,6 +105,8 @@ interface ComboBoxProps {
highlightedCodeSections: (RangeInFile & { contents: string })[];
deleteContextItems: (indices: number[]) => void;
onTogglePin: () => void;
+ onToggleAddContext: () => void;
+ addingHighlightedCode: boolean;
}
const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
@@ -188,6 +195,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
) {
// Prevent Downshift's default 'Enter' behavior.
(event.nativeEvent as any).preventDownshiftDefault = true;
+
+ // cmd+enter to /edit
+ if (event.metaKey) {
+ event.currentTarget.value = `/edit ${event.currentTarget.value}`;
+ }
if (props.onEnter) props.onEnter(event);
setInputValue("");
const value = event.currentTarget.value;
@@ -249,6 +261,19 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
</Ul>
</div>
<div className="px-2 flex gap-2 items-center flex-wrap">
+ {highlightedCodeSections.length === 0 && (
+ <HeaderButtonWithText
+ text={
+ props.addingHighlightedCode ? "Adding Context" : "Add Context"
+ }
+ onClick={() => {
+ props.onToggleAddContext();
+ }}
+ inverted={props.addingHighlightedCode}
+ >
+ <Plus size="1.6em" />
+ </HeaderButtonWithText>
+ )}
{highlightedCodeSections.length > 0 && (
<>
<HeaderButtonWithText
@@ -304,10 +329,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
/>
))}
- <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 className="text-trueGray-400 ml-auto mr-4 text-xs text-right">
+ Highlight code to include as context. Currently open file included by
+ default. {highlightedCodeSections.length === 0 && ""}
</span>
</div>
<ContextDropdown
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index 3ddac93c..72a653c5 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -8,15 +8,17 @@ interface HeaderButtonWithTextProps {
children: React.ReactNode;
disabled?: boolean;
inverted?: boolean;
+ active?: boolean;
}
const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
const [hover, setHover] = useState(false);
+ const paddingLeft = (props.disabled ? (props.active ? "3px" : "1px"): (hover ? "4px" : "1px"));
return (
<HeaderButton
inverted={props.inverted}
disabled={props.disabled}
- style={{ padding: "1px", paddingLeft: hover ? "4px" : "1px" }}
+ style={{ padding: (props.active ? "3px" : "1px"), paddingLeft, borderRadius: (props.active ? "50%" : undefined) }}
onMouseEnter={() => {
if (!props.disabled) {
setHover(true);
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
index 2352c3ad..5a02c6b2 100644
--- a/extension/react-app/src/components/PillButton.tsx
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -15,6 +15,8 @@ const Button = styled.button`
background-color: white;
color: black;
}
+
+ cursor: pointer;
`;
interface PillButtonProps {
@@ -39,26 +41,13 @@ const PillButton = (props: PillButtonProps) => {
props.onHover(false);
}
}}
+ onClick={() => {
+ if (props.onDelete) {
+ props.onDelete();
+ }
+ }}
>
- <div
- style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "4px" }}
- >
- <span
- style={{
- cursor: "pointer",
- color: "red",
- borderRight: "1px solid black",
- paddingRight: "4px",
- }}
- onClick={() => {
- props.onDelete?.();
- props.onHover?.(false);
- }}
- >
- <XMark style={{ padding: "0px" }} size="1.2em" strokeWidth="2px" />
- </span>
- <span>{props.title}</span>
- </div>
+ {props.title}
</Button>
);
};
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 35d34976..2aed2e72 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -10,9 +10,10 @@ import {
import {
ChevronDown,
ChevronRight,
- XMark,
ArrowPath,
+ XMark,
} from "@styled-icons/heroicons-outline";
+import { Stop } from "@styled-icons/heroicons-solid";
import { HistoryNode } from "../../../schema/HistoryNode";
import ReactMarkdown from "react-markdown";
import HeaderButtonWithText from "./HeaderButtonWithText";
@@ -207,9 +208,14 @@ function StepContainer(props: StepContainerProps) {
e.stopPropagation();
props.onDelete();
}}
- text="Delete"
+ text={props.historyNode.active ? "Stop" : "Delete"}
+ active={props.historyNode.active}
>
- <XMark size="1.6em" onClick={props.onDelete} />
+ {props.historyNode.active ? (
+ <Stop size="1.2em" onClick={props.onDelete} />
+ ) : (
+ <XMark size="1.6em" onClick={props.onDelete} />
+ )}
</HeaderButtonWithText>
{props.historyNode.observation?.error ? (
<HeaderButtonWithText
diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx
index 44fdba38..28437d35 100644
--- a/extension/react-app/src/components/UserInputContainer.tsx
+++ b/extension/react-app/src/components/UserInputContainer.tsx
@@ -15,7 +15,7 @@ interface UserInputContainerProps {
}
const StyledDiv = styled.div`
- background-color: rgb(50 50 50);
+ background-color: rgb(45 45 45);
padding: 8px;
padding-left: 16px;
padding-right: 16px;
@@ -28,8 +28,8 @@ const StyledDiv = styled.div`
const UserInputContainer = (props: UserInputContainerProps) => {
return (
- <StyledDiv hidden={props.historyNode.step.hide as any}>
- {props.children}
+ <StyledDiv>
+ <b>{props.children}</b>
<div style={{ marginLeft: "auto" }}>
<HeaderButtonWithText
onClick={(e) => {
diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
index 96ea7ab3..f123bb2b 100644
--- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
+++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts
@@ -22,6 +22,8 @@ abstract class AbstractContinueGUIClientProtocol {
abstract deleteAtIndex(index: number): void;
abstract deleteContextAtIndices(indices: number[]): void;
+
+ abstract toggleAddingHighlightedCode(): void;
}
export default AbstractContinueGUIClientProtocol;
diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
index e950387c..49f200ae 100644
--- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts
+++ b/extension/react-app/src/hooks/useContinueGUIProtocol.ts
@@ -74,6 +74,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol {
deleteContextAtIndices(indices: number[]) {
this.messenger.send("delete_context_at_indices", { indices });
}
+
+ toggleAddingHighlightedCode(): void {
+ this.messenger.send("toggle_adding_highlighted_code", {});
+ }
}
export default ContinueGUIClientProtocol;
diff --git a/extension/react-app/src/main.tsx b/extension/react-app/src/main.tsx
index 1b94dc82..0b02575c 100644
--- a/extension/react-app/src/main.tsx
+++ b/extension/react-app/src/main.tsx
@@ -8,6 +8,10 @@ import { PostHogProvider } from "posthog-js/react";
posthog.init("phc_JS6XFROuNbhJtVCEdTSYk6gl5ArRrTNMpCcguAXlSPs", {
api_host: "https://app.posthog.com",
+ session_recording: {
+ // WARNING: Only enable this if you understand the security implications
+ recordCrossOriginIframes: true,
+ } as any,
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index bbf0b126..e5320c6a 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -71,6 +71,7 @@ function GUI(props: GUIProps) {
const [waitingForSteps, setWaitingForSteps] = useState(false);
const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
const [highlightedRanges, setHighlightedRanges] = useState([]);
+ const [addingHighlightedCode, setAddingHighlightedCode] = useState(false);
const [availableSlashCommands, setAvailableSlashCommands] = useState<
{ name: string; description: string }[]
>([]);
@@ -157,6 +158,7 @@ function GUI(props: GUIProps) {
setHistory(state.history);
setHighlightedRanges(state.highlighted_ranges);
setUserInputQueue(state.user_input_queue);
+ setAddingHighlightedCode(state.adding_highlighted_code);
setAvailableSlashCommands(
state.slash_commands.map((c: any) => {
return {
@@ -293,14 +295,16 @@ function GUI(props: GUIProps) {
)}
{history?.timeline.map((node: HistoryNode, index: number) => {
return node.step.name === "User Input" ? (
- <UserInputContainer
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- historyNode={node}
- >
- {node.step.description as string}
- </UserInputContainer>
+ node.step.hide || (
+ <UserInputContainer
+ onDelete={() => {
+ client?.deleteAtIndex(index);
+ }}
+ historyNode={node}
+ >
+ {node.step.description as string}
+ </UserInputContainer>
+ )
) : (
<StepContainer
isLast={index === history.timeline.length - 1}
@@ -361,6 +365,10 @@ function GUI(props: GUIProps) {
onTogglePin={() => {
setPinned((prev: boolean) => !prev);
}}
+ onToggleAddContext={() => {
+ client?.toggleAddingHighlightedCode();
+ }}
+ addingHighlightedCode={addingHighlightedCode}
/>
<ContinueButton onClick={onMainTextInput} />
</TopGUIDiv>
diff --git a/extension/src/commands.ts b/extension/src/commands.ts
index 8072353b..4414a171 100644
--- a/extension/src/commands.ts
+++ b/extension/src/commands.ts
@@ -12,6 +12,8 @@ import {
acceptAllSuggestionsCommand,
rejectAllSuggestionsCommand,
} from "./suggestions";
+
+import { acceptDiffCommand, rejectDiffCommand } from "./diffs";
import * as bridge from "./bridge";
import { debugPanelWebview } from "./debugPanel";
import { sendTelemetryEvent, TelemetryEvent } from "./telemetry";
@@ -51,6 +53,8 @@ const commandsMap: { [command: string]: (...args: any) => any } = {
"continue.suggestionUp": suggestionUpCommand,
"continue.acceptSuggestion": acceptSuggestionCommand,
"continue.rejectSuggestion": rejectSuggestionCommand,
+ "continue.acceptDiff": acceptDiffCommand,
+ "continue.rejectDiff": rejectDiffCommand,
"continue.acceptAllSuggestions": acceptAllSuggestionsCommand,
"continue.rejectAllSuggestions": rejectAllSuggestionsCommand,
"continue.focusContinueInput": async () => {
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index b9969858..90547edc 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -15,6 +15,10 @@ import {
import { FileEditWithFullContents } from "../schema/FileEditWithFullContents";
import fs = require("fs");
import { WebsocketMessenger } from "./util/messenger";
+import * as path from "path";
+import * as os from "os";
+import { diffManager } from "./diffs";
+
class IdeProtocolClient {
private messenger: WebsocketMessenger | null = null;
private readonly context: vscode.ExtensionContext;
@@ -239,40 +243,8 @@ class IdeProtocolClient {
);
}
- contentProvider: vscode.Disposable | null = null;
-
showDiff(filepath: string, replacement: string) {
- const myProvider = new (class
- implements vscode.TextDocumentContentProvider
- {
- onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
- onDidChange = this.onDidChangeEmitter.event;
- provideTextDocumentContent = (uri: vscode.Uri) => {
- return replacement;
- };
- })();
- this.contentProvider = vscode.workspace.registerTextDocumentContentProvider(
- "continueDiff",
- myProvider
- );
-
- // Call the event fire
- const diffFilename = `continueDiff://${filepath}`;
- myProvider.onDidChangeEmitter.fire(vscode.Uri.parse(diffFilename));
-
- const leftUri = vscode.Uri.file(filepath);
- const rightUri = vscode.Uri.parse(diffFilename);
- const title = "Continue Diff";
- vscode.commands
- .executeCommand("vscode.diff", leftUri, rightUri, title)
- .then(
- () => {
- console.log("Diff view opened successfully");
- },
- (error) => {
- console.error("Error opening diff view:", error);
- }
- );
+ diffManager.writeDiff(filepath, replacement);
}
openFile(filepath: string) {
diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts
new file mode 100644
index 00000000..1b8888e8
--- /dev/null
+++ b/extension/src/diffs.ts
@@ -0,0 +1,140 @@
+import * as os from "os";
+import * as path from "path";
+import * as fs from "fs";
+import * as vscode from "vscode";
+
+interface DiffInfo {
+ originalFilepath: string;
+ newFilepath: string;
+ editor?: vscode.TextEditor;
+}
+
+export const DIFF_DIRECTORY = path.join(os.homedir(), ".continue", "diffs");
+
+class DiffManager {
+ // Create a temporary file in the global .continue directory which displays the updated version
+ // Doing this because virtual files are read-only
+ private diffs: Map<string, DiffInfo> = new Map();
+
+ private setupDirectory() {
+ // Make sure the diff directory exists
+ if (!fs.existsSync(DIFF_DIRECTORY)) {
+ fs.mkdirSync(DIFF_DIRECTORY, {
+ recursive: true,
+ });
+ }
+ }
+
+ constructor() {
+ this.setupDirectory();
+ }
+
+ private escapeFilepath(filepath: string): string {
+ return filepath.replace(/\\/g, "_").replace(/\//g, "_");
+ }
+
+ private openDiffEditor(
+ originalFilepath: string,
+ newFilepath: string,
+ newContent: string
+ ): vscode.TextEditor {
+ const rightUri = vscode.Uri.parse(newFilepath);
+ const leftUri = vscode.Uri.file(originalFilepath);
+ const title = "Continue Diff";
+ vscode.commands.executeCommand("vscode.diff", leftUri, rightUri, title);
+
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ throw new Error("No active text editor found for Continue Diff");
+ }
+
+ // Change the vscode setting to allow codeLens in diff editor
+ vscode.workspace
+ .getConfiguration("diffEditor", editor.document.uri)
+ .update("codeLens", true, vscode.ConfigurationTarget.Global);
+
+ return editor;
+ }
+
+ writeDiff(originalFilepath: string, newContent: string): string {
+ this.setupDirectory();
+
+ // Create or update existing diff
+ const newFilepath = path.join(
+ DIFF_DIRECTORY,
+ this.escapeFilepath(originalFilepath)
+ );
+ fs.writeFileSync(newFilepath, newContent);
+
+ // Open the diff editor if this is a new diff
+ if (!this.diffs.has(newFilepath)) {
+ const diffInfo: DiffInfo = {
+ originalFilepath,
+ newFilepath,
+ };
+ diffInfo.editor = this.openDiffEditor(
+ originalFilepath,
+ newFilepath,
+ newContent
+ );
+ this.diffs.set(newFilepath, diffInfo);
+ }
+ return newFilepath;
+ }
+
+ cleanUpDiff(diffInfo: DiffInfo) {
+ // Close the editor, remove the record, delete the file
+ if (diffInfo.editor) {
+ vscode.window.showTextDocument(diffInfo.editor.document);
+ vscode.commands.executeCommand("workbench.action.closeActiveEditor");
+ }
+ this.diffs.delete(diffInfo.newFilepath);
+ fs.unlinkSync(diffInfo.newFilepath);
+ }
+
+ acceptDiff(newFilepath?: string) {
+ // If no newFilepath is provided and there is only one in the dictionary, use that
+ if (!newFilepath && this.diffs.size === 1) {
+ newFilepath = Array.from(this.diffs.keys())[0];
+ }
+ if (!newFilepath) {
+ return;
+ }
+ // Get the diff info, copy new file to original, then delete from record and close the corresponding editor
+ const diffInfo = this.diffs.get(newFilepath);
+ if (!diffInfo) {
+ return;
+ }
+ fs.writeFileSync(
+ diffInfo.originalFilepath,
+ fs.readFileSync(diffInfo.newFilepath)
+ );
+ this.cleanUpDiff(diffInfo);
+ }
+
+ rejectDiff(newFilepath?: string) {
+ // If no newFilepath is provided and there is only one in the dictionary, use that
+ if (!newFilepath && this.diffs.size === 1) {
+ newFilepath = Array.from(this.diffs.keys())[0];
+ }
+ if (!newFilepath) {
+ return;
+ }
+ const diffInfo = this.diffs.get(newFilepath);
+ if (!diffInfo) {
+ return;
+ }
+
+ this.cleanUpDiff(diffInfo);
+ }
+}
+
+export const diffManager = new DiffManager();
+
+export async function acceptDiffCommand(newFilepath?: string) {
+ diffManager.acceptDiff(newFilepath);
+}
+
+export async function rejectDiffCommand(newFilepath?: string) {
+ diffManager.rejectDiff(newFilepath);
+}
diff --git a/extension/src/lang-server/codeLens.ts b/extension/src/lang-server/codeLens.ts
index 3bd4f153..381a0084 100644
--- a/extension/src/lang-server/codeLens.ts
+++ b/extension/src/lang-server/codeLens.ts
@@ -1,6 +1,8 @@
import * as vscode from "vscode";
import { editorToSuggestions, editorSuggestionsLocked } from "../suggestions";
-
+import * as path from "path";
+import * as os from "os";
+import { DIFF_DIRECTORY } from "../diffs";
class SuggestionsCodeLensProvider implements vscode.CodeLensProvider {
public provideCodeLenses(
document: vscode.TextDocument,
@@ -60,15 +62,67 @@ class SuggestionsCodeLensProvider implements vscode.CodeLensProvider {
}
}
+class DiffViewerCodeLensProvider implements vscode.CodeLensProvider {
+ public provideCodeLenses(
+ document: vscode.TextDocument,
+ token: vscode.CancellationToken
+ ): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
+ if (path.dirname(document.uri.fsPath) === DIFF_DIRECTORY) {
+ const codeLenses: vscode.CodeLens[] = [];
+ const range = new vscode.Range(0, 0, 1, 0);
+ codeLenses.push(
+ new vscode.CodeLens(range, {
+ title: "Accept ✅",
+ command: "continue.acceptDiff",
+ arguments: [document.uri.fsPath],
+ }),
+ new vscode.CodeLens(range, {
+ title: "Reject ❌",
+ command: "continue.rejectDiff",
+ arguments: [document.uri.fsPath],
+ })
+ );
+ return codeLenses;
+ } else {
+ return [];
+ }
+ }
+
+ onDidChangeCodeLenses?: vscode.Event<void> | undefined;
+
+ constructor(emitter?: vscode.EventEmitter<void>) {
+ if (emitter) {
+ this.onDidChangeCodeLenses = emitter.event;
+ this.onDidChangeCodeLenses(() => {
+ if (vscode.window.activeTextEditor) {
+ this.provideCodeLenses(
+ vscode.window.activeTextEditor.document,
+ new vscode.CancellationTokenSource().token
+ );
+ }
+ });
+ }
+ }
+}
+
+let diffsCodeLensDisposable: vscode.Disposable | undefined = undefined;
let suggestionsCodeLensDisposable: vscode.Disposable | undefined = undefined;
export function registerAllCodeLensProviders(context: vscode.ExtensionContext) {
if (suggestionsCodeLensDisposable) {
suggestionsCodeLensDisposable.dispose();
}
+ if (diffsCodeLensDisposable) {
+ diffsCodeLensDisposable.dispose();
+ }
suggestionsCodeLensDisposable = vscode.languages.registerCodeLensProvider(
"*",
new SuggestionsCodeLensProvider()
);
+ diffsCodeLensDisposable = vscode.languages.registerCodeLensProvider(
+ "*",
+ new DiffViewerCodeLensProvider()
+ );
context.subscriptions.push(suggestionsCodeLensDisposable);
+ context.subscriptions.push(diffsCodeLensDisposable);
}