From c188ba78c5efb82b8701d285b11e9d198eda8f86 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Thu, 15 Jun 2023 23:42:25 -0700 Subject: handle and explain tracebacks --- extension/src/activation/activate.ts | 32 +++++++++++++++++++ extension/src/continueIdeClient.ts | 10 ++++-- extension/src/terminal/terminalEmulator.ts | 50 ++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 12 deletions(-) (limited to 'extension/src') diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index 135a8ec7..77010241 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -8,6 +8,7 @@ import * as path from "path"; import IdeProtocolClient from "../continueIdeClient"; import { getContinueServerUrl } from "../bridge"; import { setupDebugPanel, ContinueGUIWebviewViewProvider } from "../debugPanel"; +import { CapturedTerminal } from "../terminal/terminalEmulator"; export let extensionContext: vscode.ExtensionContext | undefined = undefined; @@ -47,5 +48,36 @@ export function activateExtension( ); })(); + // All opened terminals should be replaced by our own terminal + vscode.window.onDidOpenTerminal((terminal) => { + if (terminal.name === "Continue") { + return; + } + const options = terminal.creationOptions; + const capturedTerminal = new CapturedTerminal({ + ...options, + name: "Continue", + }); + terminal.dispose(); + }); + + // If any terminals are open to start, replace them + vscode.window.terminals.forEach((terminal) => { + if (terminal.name === "Continue") { + return; + } + const options = terminal.creationOptions; + const capturedTerminal = new CapturedTerminal( + { + ...options, + name: "Continue", + }, + (commandOutput: string) => { + ideProtocolClient.sendCommandOutput(commandOutput); + } + ); + terminal.dispose(); + }); + extensionContext = context; } diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index ef9a91c8..a889d3dc 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -326,13 +326,19 @@ class IdeProtocolClient { private continueTerminal: CapturedTerminal | undefined; async runCommand(command: string) { - if (!this.continueTerminal) { - this.continueTerminal = new CapturedTerminal("Continue"); + if (!this.continueTerminal || this.continueTerminal.isClosed()) { + this.continueTerminal = new CapturedTerminal({ + name: "Continue", + }); } this.continueTerminal.show(); return await this.continueTerminal.runCommand(command); } + + sendCommandOutput(output: string) { + this.messenger?.send("commandOutput", { output }); + } } export default IdeProtocolClient; diff --git a/extension/src/terminal/terminalEmulator.ts b/extension/src/terminal/terminalEmulator.ts index b3031baf..35f02ac0 100644 --- a/extension/src/terminal/terminalEmulator.ts +++ b/extension/src/terminal/terminalEmulator.ts @@ -62,21 +62,29 @@ export class CapturedTerminal { this.terminal.show(); } + isClosed(): boolean { + return this.terminal.exitStatus !== undefined; + } + private commandQueue: [string, (output: string) => void][] = []; private hasRunCommand: boolean = false; + private dataEndsInPrompt(strippedData: string): boolean { + const lines = this.dataBuffer.split("\n"); + return ( + lines.length > 0 && + (lines[lines.length - 1].includes("bash-") || + lines[lines.length - 1].includes(") $ ")) && + lines[lines.length - 1].includes("$") + ); + } + private async waitForCommandToFinish() { return new Promise((resolve, reject) => { this.onDataListeners.push((data: any) => { const strippedData = stripAnsi(data); this.dataBuffer += strippedData; - const lines = this.dataBuffer.split("\n"); - if ( - lines.length > 0 && - (lines[lines.length - 1].includes("bash-") || - lines[lines.length - 1].includes(") $ ")) && - lines[lines.length - 1].includes("$") - ) { + if (this.dataEndsInPrompt(strippedData)) { resolve(this.dataBuffer); this.dataBuffer = ""; this.onDataListeners = []; @@ -112,8 +120,30 @@ export class CapturedTerminal { private readonly writeEmitter: vscode.EventEmitter; - constructor(terminalName: string) { - this.shellCmd = "bash"; // getDefaultShell(); + private splitByCommandsBuffer: string = ""; + private readonly onCommandOutput: ((output: string) => void) | undefined; + + splitByCommandsListener(data: string) { + // Split the output by commands so it can be sent to Continue Server + + const strippedData = stripAnsi(data); + this.splitByCommandsBuffer += strippedData; + if (this.dataEndsInPrompt(strippedData)) { + if (this.onCommandOutput) { + this.onCommandOutput(this.splitByCommandsBuffer); + } + this.dataBuffer = ""; + } + } + + constructor( + options: { name: string } & Partial, + onCommandOutput?: (output: string) => void + ) { + this.onCommandOutput = onCommandOutput; + + // this.shellCmd = "bash"; // getDefaultShell(); + this.shellCmd = getDefaultShell(); const env = { ...(process.env as any) }; if (os.platform() !== "win32") { @@ -154,7 +184,7 @@ export class CapturedTerminal { // Create and clear the terminal this.terminal = vscode.window.createTerminal({ - name: terminalName, + ...options, pty: newPty, }); this.terminal.show(); -- cgit v1.2.3-70-g09d2 From 5162e4e2fde74a05f9b359c90711f221305ba620 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 16 Jun 2023 00:56:28 -0700 Subject: catching and explaining tracebacks --- continuedev/src/continuedev/core/autopilot.py | 15 ++++++---- continuedev/src/continuedev/core/policy.py | 2 +- .../src/continuedev/libs/util/traceback_parsers.py | 33 ++++++++-------------- continuedev/src/continuedev/server/ide_protocol.py | 4 --- continuedev/src/continuedev/steps/chat.py | 2 +- continuedev/src/continuedev/steps/core/core.py | 16 ----------- continuedev/src/continuedev/steps/main.py | 23 --------------- continuedev/src/continuedev/steps/on_traceback.py | 2 +- extension/src/terminal/terminalEmulator.ts | 18 ++++++------ 9 files changed, 34 insertions(+), 81 deletions(-) (limited to 'extension/src') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 3ccce89a..782d6270 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -14,6 +14,7 @@ from ..libs.util.telemetry import capture_event from .sdk import ContinueSDK import asyncio from ..libs.util.step_name_to_steps import get_step_from_name +from ..libs.util.traceback_parsers import get_python_traceback, get_javascript_traceback class Autopilot(ContinueBaseModel): @@ -92,12 +93,14 @@ class Autopilot(ContinueBaseModel): # Note that this is being overriden to do nothing in DemoAgent async def handle_command_output(self, output: str): - is_traceback = False - if is_traceback: - for tb_step in self.continue_sdk.config.on_traceback: - step = get_step_from_name(tb_step.step_name)( - output=output, **tb_step.params) - await self._run_singular_step(step) + get_traceback_funcs = [get_python_traceback, get_javascript_traceback] + for get_tb_func in get_traceback_funcs: + traceback = get_tb_func(output) + if traceback is not None: + for tb_step in self.continue_sdk.config.on_traceback: + step = get_step_from_name( + tb_step.step_name, {"output": output, **tb_step.params}) + await self._run_singular_step(step) _step_depth: int = 0 diff --git a/continuedev/src/continuedev/core/policy.py b/continuedev/src/continuedev/core/policy.py index 255f598d..1b53834b 100644 --- a/continuedev/src/continuedev/core/policy.py +++ b/continuedev/src/continuedev/core/policy.py @@ -8,7 +8,7 @@ from ..recipes.DeployPipelineAirflowRecipe.main import DeployPipelineAirflowReci from ..recipes.AddTransformRecipe.main import AddTransformRecipe from .main import Step, Validator, History, Policy from .observation import Observation, TracebackObservation, UserInputObservation -from ..steps.main import EditHighlightedCodeStep, SolveTracebackStep, RunCodeStep, FasterEditHighlightedCodeStep, StarCoderEditHighlightedCodeStep, EmptyStep, SetupContinueWorkspaceStep +from ..steps.main import EditHighlightedCodeStep, SolveTracebackStep from ..recipes.WritePytestsRecipe.main import WritePytestsRecipe from ..recipes.ContinueRecipeRecipe.main import ContinueStepStep from ..steps.comment_code import CommentCodeStep diff --git a/continuedev/src/continuedev/libs/util/traceback_parsers.py b/continuedev/src/continuedev/libs/util/traceback_parsers.py index c31929c1..4e7a24e0 100644 --- a/continuedev/src/continuedev/libs/util/traceback_parsers.py +++ b/continuedev/src/continuedev/libs/util/traceback_parsers.py @@ -1,24 +1,15 @@ -from typing import Union -from ...models.main import Traceback -from boltons import tbutils - - -def sort_func(items): - """Sort a list of items.""" - return sorted(items) - - -def parse_python_traceback(stdout: str) -> Union[Traceback, None]: - """Parse a python traceback from stdout.""" - - # Sometimes paths are not quoted, but they need to be - if "File \"" not in stdout: - stdout = stdout.replace("File ", "File \"").replace( - ", line ", "\", line ") +def get_python_traceback(output: str) -> str: + if "Traceback (most recent call last):" in output or "SyntaxError" in output: + return output + else: + return None - try: - tbutil_parsed_exc = tbutils.ParsedException.from_string(stdout) - return Traceback.from_tbutil_parsed_exc(tbutil_parsed_exc) - except Exception: +def get_javascript_traceback(output: str) -> str: + lines = output.splitlines() + if len(lines) > 0: + first_line = lines[0].split(": ") + if len(lines) > 1 and len(first_line) > 0 and len(first_line[0]) > 0 and "at" in lines[1].lstrip(): + return output + else: return None diff --git a/continuedev/src/continuedev/server/ide_protocol.py b/continuedev/src/continuedev/server/ide_protocol.py index 1d98f4a1..2dcedc30 100644 --- a/continuedev/src/continuedev/server/ide_protocol.py +++ b/continuedev/src/continuedev/server/ide_protocol.py @@ -35,10 +35,6 @@ class AbstractIdeProtocolServer(ABC): def onAcceptRejectSuggestion(self, suggestionId: str, accepted: bool): """Called when the user accepts or rejects a suggestion""" - @abstractmethod - def onTraceback(self, traceback: Traceback): - """Called when a traceback is received""" - @abstractmethod def onFileSystemUpdate(self, update: FileSystemEdit): """Called when a file system update is received""" diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py index 499d127f..90514ad6 100644 --- a/continuedev/src/continuedev/steps/chat.py +++ b/continuedev/src/continuedev/steps/chat.py @@ -10,7 +10,7 @@ class SimpleChatStep(Step): name: str = "Chat" async def run(self, sdk: ContinueSDK): - self.description = f"## {self.user_input}\n\n" + self.description = f"```{self.user_input}```\n\n" async for chunk in sdk.models.default.stream_chat(self.user_input, with_history=await sdk.get_chat_context()): self.description += chunk await sdk.update_ui() diff --git a/continuedev/src/continuedev/steps/core/core.py b/continuedev/src/continuedev/steps/core/core.py index 7f3a93ba..59af5f38 100644 --- a/continuedev/src/continuedev/steps/core/core.py +++ b/continuedev/src/continuedev/steps/core/core.py @@ -332,19 +332,3 @@ class WaitForUserConfirmationStep(Step): self.description = self.prompt resp = await sdk.wait_for_user_input() return TextObservation(text=resp) - - -def get_python_traceback(output: str) -> str: - if "Traceback" in output: - return output - else: - return None - -def get_javascript_traceback(output: str) -> str: - lines = output.splitlines("\n") - if len(lines) > 0: - first_line = lines[0].split(": ") - if len(lines) > 1 and len(first_line) > 0 and len(first_line[0]) > 0 and "at" in lines[1].lstrip(): - return output - else: - return None \ No newline at end of file diff --git a/continuedev/src/continuedev/steps/main.py b/continuedev/src/continuedev/steps/main.py index b61aa3fe..5ba86c53 100644 --- a/continuedev/src/continuedev/steps/main.py +++ b/continuedev/src/continuedev/steps/main.py @@ -3,7 +3,6 @@ from typing import Coroutine, List, Union from pydantic import BaseModel -from ..libs.util.traceback_parsers import parse_python_traceback from ..libs.llm import LLM from ..models.main import Traceback, Range from ..models.filesystem_edit import EditDiff, FileEdit @@ -33,28 +32,6 @@ class SetupContinueWorkspaceStep(Step): }""")) -class RunCodeStep(Step): - cmd: str - - async def describe(self, models: Models) -> Coroutine[str, None, None]: - return f"Ran command: `{self.cmd}`" - - async def run(self, sdk: ContinueSDK) -> Coroutine[Observation, None, None]: - result = subprocess.run( - self.cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout = result.stdout.decode("utf-8") - stderr = result.stderr.decode("utf-8") - print(stdout, stderr) - - # If it fails, return the error - tb = parse_python_traceback(stdout) or parse_python_traceback(stderr) - if tb: - return TracebackObservation(traceback=tb) - else: - self.hide = True - return None - - class Policy(BaseModel): pass diff --git a/continuedev/src/continuedev/steps/on_traceback.py b/continuedev/src/continuedev/steps/on_traceback.py index de668775..a0c4d07b 100644 --- a/continuedev/src/continuedev/steps/on_traceback.py +++ b/continuedev/src/continuedev/steps/on_traceback.py @@ -9,6 +9,6 @@ class DefaultOnTracebackStep(Step): hide: bool = True async def run(self, sdk: ContinueSDK): - sdk.run_step(SimpleChatStep( + await sdk.run_step(SimpleChatStep( name="Help With Traceback", user_input=f"""I got the following error, can you please help explain how to fix it?\n\n{self.output}""")) diff --git a/extension/src/terminal/terminalEmulator.ts b/extension/src/terminal/terminalEmulator.ts index 35f02ac0..9d90b331 100644 --- a/extension/src/terminal/terminalEmulator.ts +++ b/extension/src/terminal/terminalEmulator.ts @@ -70,12 +70,13 @@ export class CapturedTerminal { private hasRunCommand: boolean = false; private dataEndsInPrompt(strippedData: string): boolean { - const lines = this.dataBuffer.split("\n"); + const lines = strippedData.split("\n"); + const last_line = lines[lines.length - 1]; return ( - lines.length > 0 && - (lines[lines.length - 1].includes("bash-") || - lines[lines.length - 1].includes(") $ ")) && - lines[lines.length - 1].includes("$") + (lines.length > 0 && + (last_line.includes("bash-") || last_line.includes(") $ ")) && + last_line.includes("$")) || + (last_line.includes("]> ") && last_line.includes(") [")) ); } @@ -127,12 +128,12 @@ export class CapturedTerminal { // Split the output by commands so it can be sent to Continue Server const strippedData = stripAnsi(data); - this.splitByCommandsBuffer += strippedData; + this.splitByCommandsBuffer += data; if (this.dataEndsInPrompt(strippedData)) { if (this.onCommandOutput) { - this.onCommandOutput(this.splitByCommandsBuffer); + this.onCommandOutput(stripAnsi(this.splitByCommandsBuffer)); } - this.dataBuffer = ""; + this.splitByCommandsBuffer = ""; } } @@ -166,6 +167,7 @@ export class CapturedTerminal { // Pass data through to terminal this.writeEmitter.fire(data); + this.splitByCommandsListener(data); for (let listener of this.onDataListeners) { listener(data); } -- cgit v1.2.3-70-g09d2 From 149db209603c5de16d4135f1fdc4f19104974861 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 16 Jun 2023 11:40:36 -0700 Subject: dlt demo work --- continuedev/src/continuedev/core/autopilot.py | 4 +++ continuedev/src/continuedev/core/config.py | 1 + .../src/continuedev/libs/util/traceback_parsers.py | 13 ++++++--- .../recipes/CreatePipelineRecipe/main.py | 4 ++- .../recipes/CreatePipelineRecipe/steps.py | 4 +-- extension/react-app/src/index.css | 1 + extension/react-app/src/tabs/gui.tsx | 34 ++++++++++++---------- extension/src/activation/activate.ts | 6 ++++ extension/src/continueIdeClient.ts | 2 +- extension/src/terminal/terminalEmulator.ts | 7 +++-- 10 files changed, 49 insertions(+), 27 deletions(-) (limited to 'extension/src') diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py index 782d6270..73f46a37 100644 --- a/continuedev/src/continuedev/core/autopilot.py +++ b/continuedev/src/continuedev/core/autopilot.py @@ -113,6 +113,10 @@ class Autopilot(ContinueBaseModel): 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: + return None + # If a parent step is deleted/cancelled, don't run this step last_depth = self._step_depth i = self.history.current_index diff --git a/continuedev/src/continuedev/core/config.py b/continuedev/src/continuedev/core/config.py index d8b29f5b..8f703758 100644 --- a/continuedev/src/continuedev/core/config.py +++ b/continuedev/src/continuedev/core/config.py @@ -22,6 +22,7 @@ class ContinueConfig(BaseModel): A pydantic class for the continue config file. """ steps_on_startup: Optional[Dict[str, Dict]] = {} + disallowed_steps: Optional[List[str]] = [] server_url: Optional[str] = None allow_anonymous_telemetry: Optional[bool] = True default_model: Literal["gpt-3.5-turbo", diff --git a/continuedev/src/continuedev/libs/util/traceback_parsers.py b/continuedev/src/continuedev/libs/util/traceback_parsers.py index 1885cc79..a2e94c26 100644 --- a/continuedev/src/continuedev/libs/util/traceback_parsers.py +++ b/continuedev/src/continuedev/libs/util/traceback_parsers.py @@ -12,9 +12,14 @@ def get_python_traceback(output: str) -> str: def get_javascript_traceback(output: str) -> str: lines = output.splitlines() - if len(lines) > 0: - first_line = lines[0].split(": ") - if len(lines) > 1 and len(first_line) > 0 and len(first_line[0]) > 0 and "at" in lines[1].lstrip(): - return output + first_line = None + for i in range(len(lines) - 1): + segs = lines[i].split(":") + if len(segs) > 1 and segs[0] != "" and segs[1].startswith(" ") and lines[i + 1].strip().startswith("at"): + first_line = lines[i] + break + + if first_line is not None: + return "\n".join(lines[lines.index(first_line):]) else: return None diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py index 92bddc98..55ef107b 100644 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py +++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py @@ -27,5 +27,7 @@ class CreatePipelineRecipe(Step): await sdk.run_step( SetupPipelineStep(api_description=text_observation.text) >> ValidatePipelineStep() >> - RunQueryStep() + RunQueryStep() >> + MessageStep( + name="Congrats!", message="You've successfully created your first dlt pipeline! 🎉") ) diff --git a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py index 1a756a76..91515dc2 100644 --- a/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py +++ b/continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py @@ -64,7 +64,7 @@ class SetupPipelineStep(Step): # wait for user to put API key in secrets.toml await sdk.ide.setFileOpen(await sdk.ide.getWorkspaceDirectory() + "/.dlt/secrets.toml") - await sdk.wait_for_user_confirmation("If this service requires an API key, please add it to the `secrets.toml` file and then press `Continue`. Otherwise, type '/edit this API does not require an API key'") + await sdk.wait_for_user_confirmation("If this service requires an API key, please add it to the `secrets.toml` file and then press `Continue`.") sdk.context.set("source_name", source_name) @@ -172,5 +172,5 @@ class RunQueryStep(Step): This is a brief summary of the error followed by a suggestion on how it can be fixed:""")) sdk.raise_exception( - title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion) + title="Error while running query", message=output, with_step=MessageStep(name=f"Suggestion to solve error {AI_ASSISTED_STRING}", message=suggestion + "\n\nIt is also very likely that no duckdb table was created, which can happen if the resource function did not yield any data. Please make sure that it is yielding data and then rerun this step.") ) diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css index 32a92d0e..db8afab9 100644 --- a/extension/react-app/src/index.css +++ b/extension/react-app/src/index.css @@ -22,6 +22,7 @@ html, body, #root { height: 100%; + background-color: var(--vsc-background); } body { diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index 279d052b..994cb896 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -194,7 +194,7 @@ function GUI(props: GUIProps) { if (topGuiDivRef.current) { const timeout = setTimeout(() => { window.scrollTo({ - top: window.outerHeight, + top: topGuiDivRef.current!.offsetHeight, behavior: "smooth", }); }, 200); @@ -206,7 +206,9 @@ function GUI(props: GUIProps) { console.log("CLIENT ON STATE UPDATE: ", client, client?.onStateUpdate); client?.onStateUpdate((state) => { // Scroll only if user is at very bottom of the window. - const shouldScrollToBottom = window.outerHeight - window.scrollY < 200; + const shouldScrollToBottom = + topGuiDivRef.current && + topGuiDivRef.current?.offsetHeight - window.scrollY < 100; setWaitingForSteps(state.active); setHistory(state.history); setUserInputQueue(state.user_input_queue); @@ -347,12 +349,12 @@ function GUI(props: GUIProps) { { onMainTextInput(); @@ -365,6 +367,14 @@ function GUI(props: GUIProps) { ); diff --git a/extension/src/activation/activate.ts b/extension/src/activation/activate.ts index 77010241..32726c86 100644 --- a/extension/src/activation/activate.ts +++ b/extension/src/activation/activate.ts @@ -59,6 +59,9 @@ export function activateExtension( name: "Continue", }); terminal.dispose(); + if (!ideProtocolClient.continueTerminal) { + ideProtocolClient.continueTerminal = capturedTerminal; + } }); // If any terminals are open to start, replace them @@ -77,6 +80,9 @@ export function activateExtension( } ); terminal.dispose(); + if (!ideProtocolClient.continueTerminal) { + ideProtocolClient.continueTerminal = capturedTerminal; + } }); extensionContext = context; diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index a889d3dc..9a93a4ef 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -323,7 +323,7 @@ class IdeProtocolClient { return rangeInFiles; } - private continueTerminal: CapturedTerminal | undefined; + public continueTerminal: CapturedTerminal | undefined; async runCommand(command: string) { if (!this.continueTerminal || this.continueTerminal.isClosed()) { diff --git a/extension/src/terminal/terminalEmulator.ts b/extension/src/terminal/terminalEmulator.ts index 9d90b331..ff2060f8 100644 --- a/extension/src/terminal/terminalEmulator.ts +++ b/extension/src/terminal/terminalEmulator.ts @@ -76,7 +76,8 @@ export class CapturedTerminal { (lines.length > 0 && (last_line.includes("bash-") || last_line.includes(") $ ")) && last_line.includes("$")) || - (last_line.includes("]> ") && last_line.includes(") [")) + (last_line.includes("]> ") && last_line.includes(") [")) || + (last_line.includes(" (") && last_line.includes(")>")) ); } @@ -98,7 +99,7 @@ export class CapturedTerminal { if (!this.hasRunCommand) { this.hasRunCommand = true; // Let the first bash- prompt appear and let python env be opened - await this.waitForCommandToFinish(); + // await this.waitForCommandToFinish(); } if (this.commandQueue.length === 0) { @@ -154,7 +155,7 @@ export class CapturedTerminal { // Create the pseudo terminal this.ptyProcess = pty.spawn(this.shellCmd, [], { name: "xterm-256color", - cols: 160, // TODO: Get size of vscode terminal, and change with resize + cols: 250, // No way to get the size of VS Code terminal, or listen to resize, so make it just bigger than most conceivable VS Code widths rows: 26, cwd: getRootDir(), env, -- cgit v1.2.3-70-g09d2 From b740c5bf216b63dbb2ffa88f8385e9795aa57403 Mon Sep 17 00:00:00 2001 From: Nate Sesti Date: Fri, 16 Jun 2023 14:34:30 -0700 Subject: Capturing command line prompt --- extension/package-lock.json | 4 +- extension/package.json | 2 +- extension/src/terminal/terminalEmulator.ts | 97 ++++++++++++++++++++++++++---- extension/src/util/lcs.ts | 30 +++++++++ 4 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 extension/src/util/lcs.ts (limited to 'extension/src') diff --git a/extension/package-lock.json b/extension/package-lock.json index eac8edf2..86c816e0 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.45", + "version": "0.0.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.45", + "version": "0.0.47", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", diff --git a/extension/package.json b/extension/package.json index 8f7da94d..3a3927f3 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@ "displayName": "Continue", "pricing": "Free", "description": "Refine code 10x faster", - "version": "0.0.45", + "version": "0.0.47", "publisher": "Continue", "engines": { "vscode": "^1.74.0" diff --git a/extension/src/terminal/terminalEmulator.ts b/extension/src/terminal/terminalEmulator.ts index ff2060f8..8e49737e 100644 --- a/extension/src/terminal/terminalEmulator.ts +++ b/extension/src/terminal/terminalEmulator.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode"; import os = require("os"); import stripAnsi from "strip-ansi"; +import { longestCommonSubsequence } from "../util/lcs"; function loadNativeModule(id: string): T | null { try { @@ -71,13 +72,20 @@ export class CapturedTerminal { private dataEndsInPrompt(strippedData: string): boolean { const lines = strippedData.split("\n"); - const last_line = lines[lines.length - 1]; + const lastLine = lines[lines.length - 1]; + return ( - (lines.length > 0 && - (last_line.includes("bash-") || last_line.includes(") $ ")) && - last_line.includes("$")) || - (last_line.includes("]> ") && last_line.includes(") [")) || - (last_line.includes(" (") && last_line.includes(")>")) + lines.length > 0 && + (((lastLine.includes("bash-") || lastLine.includes(") $ ")) && + lastLine.includes("$")) || + (lastLine.includes("]> ") && lastLine.includes(") [")) || + (lastLine.includes(" (") && lastLine.includes(")>")) || + (typeof this.commandPromptString !== "undefined" && + (lastLine.includes(this.commandPromptString) || + this.commandPromptString.length - + longestCommonSubsequence(lastLine, this.commandPromptString) + .length < + 3))) ); } @@ -96,12 +104,6 @@ export class CapturedTerminal { } async runCommand(command: string): Promise { - if (!this.hasRunCommand) { - this.hasRunCommand = true; - // Let the first bash- prompt appear and let python env be opened - // await this.waitForCommandToFinish(); - } - if (this.commandQueue.length === 0) { return new Promise(async (resolve, reject) => { this.commandQueue.push([command, resolve]); @@ -109,8 +111,12 @@ export class CapturedTerminal { while (this.commandQueue.length > 0) { const [command, resolve] = this.commandQueue.shift()!; + // Refresh the command prompt string every time in case it changes + await this.refreshCommandPromptString(); + this.terminal.sendText(command); - resolve(await this.waitForCommandToFinish()); + const output = await this.waitForCommandToFinish(); + resolve(output); } }); } else { @@ -138,6 +144,24 @@ export class CapturedTerminal { } } + private runningClearToGetPrompt: boolean = false; + private seenClear: boolean = false; + private commandPromptString: string | undefined = undefined; + private resolveMeWhenCommandPromptStringFound: + | ((_: unknown) => void) + | undefined = undefined; + + private async refreshCommandPromptString(): Promise { + // Sends a message that will be received by the terminal to get the command prompt string, see the onData method below in constructor. + this.runningClearToGetPrompt = true; + this.terminal.sendText("echo"); + const promise = new Promise((resolve, reject) => { + this.resolveMeWhenCommandPromptStringFound = resolve; + }); + await promise; + return this.commandPromptString; + } + constructor( options: { name: string } & Partial, onCommandOutput?: (output: string) => void @@ -165,7 +189,54 @@ export class CapturedTerminal { this.writeEmitter = new vscode.EventEmitter(); this.ptyProcess.onData((data: any) => { + if (this.runningClearToGetPrompt) { + if ( + stripAnsi(data) + .split("\n") + .flatMap((line) => line.split("\r")) + .find((line) => line.trim() === "echo") !== undefined + ) { + this.seenClear = true; + return; + } else if (this.seenClear) { + const strippedLines = stripAnsi(data) + .split("\r") + .filter( + (line) => + line.trim().length > 0 && + line.trim() !== "%" && + line.trim() !== "⏎" + ); + const lastLine = strippedLines[strippedLines.length - 1] || ""; + const lines = lastLine + .split("\n") + .filter( + (line) => + line.trim().length > 0 && + line.trim() !== "%" && + line.trim() !== "⏎" + ); + const commandPromptString = (lines[lines.length - 1] || "").trim(); + if ( + commandPromptString.length > 0 && + !commandPromptString.includes("echo") + ) { + this.runningClearToGetPrompt = false; + this.seenClear = false; + this.commandPromptString = commandPromptString; + console.log( + "Found command prompt string: " + this.commandPromptString + ); + if (this.resolveMeWhenCommandPromptStringFound) { + this.resolveMeWhenCommandPromptStringFound(undefined); + } + } + return; + } + } + // Pass data through to terminal + data = data.replace("⏎", ""); this.writeEmitter.fire(data); this.splitByCommandsListener(data); diff --git a/extension/src/util/lcs.ts b/extension/src/util/lcs.ts new file mode 100644 index 00000000..17ea63f9 --- /dev/null +++ b/extension/src/util/lcs.ts @@ -0,0 +1,30 @@ +export function longestCommonSubsequence(a: string, b: string) { + const lengths: number[][] = []; + for (let i = 0; i <= a.length; i++) { + lengths[i] = []; + for (let j = 0; j <= b.length; j++) { + if (i === 0 || j === 0) { + lengths[i][j] = 0; + } else if (a[i - 1] === b[j - 1]) { + lengths[i][j] = lengths[i - 1][j - 1] + 1; + } else { + lengths[i][j] = Math.max(lengths[i - 1][j], lengths[i][j - 1]); + } + } + } + let result = ""; + let x = a.length; + let y = b.length; + while (x !== 0 && y !== 0) { + if (lengths[x][y] === lengths[x - 1][y]) { + x--; + } else if (lengths[x][y] === lengths[x][y - 1]) { + y--; + } else { + result = a[x - 1] + result; + x--; + y--; + } + } + return result; +} -- cgit v1.2.3-70-g09d2