summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--continuedev/src/continuedev/core/abstract_sdk.py2
-rw-r--r--continuedev/src/continuedev/core/autopilot.py19
-rw-r--r--continuedev/src/continuedev/core/config.py1
-rw-r--r--continuedev/src/continuedev/core/policy.py2
-rw-r--r--continuedev/src/continuedev/core/sdk.py2
-rw-r--r--continuedev/src/continuedev/libs/llm/proxy_server.py7
-rw-r--r--continuedev/src/continuedev/libs/util/traceback_parsers.py37
-rw-r--r--continuedev/src/continuedev/recipes/CreatePipelineRecipe/main.py4
-rw-r--r--continuedev/src/continuedev/recipes/CreatePipelineRecipe/steps.py4
-rw-r--r--continuedev/src/continuedev/server/ide.py2
-rw-r--r--continuedev/src/continuedev/steps/chat.py2
-rw-r--r--continuedev/src/continuedev/steps/core/core.py16
-rw-r--r--continuedev/src/continuedev/steps/main.py23
-rw-r--r--continuedev/src/continuedev/steps/on_traceback.py11
-rw-r--r--extension/package-lock.json4
-rw-r--r--extension/package.json2
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx6
-rw-r--r--extension/react-app/src/components/index.ts2
-rw-r--r--extension/react-app/src/index.css1
-rw-r--r--extension/react-app/src/tabs/gui.tsx34
-rw-r--r--extension/scripts/continuedev-0.1.1-py3-none-any.whlbin83961 -> 84291 bytes
-rw-r--r--extension/src/activation/activate.ts6
-rw-r--r--extension/src/continueIdeClient.ts2
-rw-r--r--extension/src/terminal/terminalEmulator.ts104
-rw-r--r--extension/src/util/lcs.ts30
25 files changed, 212 insertions, 111 deletions
diff --git a/continuedev/src/continuedev/core/abstract_sdk.py b/continuedev/src/continuedev/core/abstract_sdk.py
index 0658f1b8..017e75ef 100644
--- a/continuedev/src/continuedev/core/abstract_sdk.py
+++ b/continuedev/src/continuedev/core/abstract_sdk.py
@@ -85,7 +85,7 @@ class AbstractContinueSDK(ABC):
pass
@abstractmethod
- def add_chat_context(self, content: str, role: ChatMessageRole = "assistent"):
+ def add_chat_context(self, content: str, role: ChatMessageRole = "assistant"):
pass
@abstractmethod
diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index 3ccce89a..73f46a37 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
@@ -110,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/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/core/sdk.py b/continuedev/src/continuedev/core/sdk.py
index 8aea6b7f..7639d010 100644
--- a/continuedev/src/continuedev/core/sdk.py
+++ b/continuedev/src/continuedev/core/sdk.py
@@ -165,7 +165,7 @@ class ContinueSDK(AbstractContinueSDK):
def raise_exception(self, message: str, title: str, with_step: Union[Step, None] = None):
raise ContinueCustomException(message, title, with_step)
- def add_chat_context(self, content: str, summary: Union[str, None] = None, role: ChatMessageRole = "assistent"):
+ def add_chat_context(self, content: str, summary: Union[str, None] = None, role: ChatMessageRole = "assistant"):
self.history.timeline[self.history.current_index].step.chat_context.append(
ChatMessage(content=content, role=role, summary=summary))
diff --git a/continuedev/src/continuedev/libs/llm/proxy_server.py b/continuedev/src/continuedev/libs/llm/proxy_server.py
index 4227042f..93f2d48a 100644
--- a/continuedev/src/continuedev/libs/llm/proxy_server.py
+++ b/continuedev/src/continuedev/libs/llm/proxy_server.py
@@ -17,7 +17,7 @@ CHAT_MODELS = {
"gpt-3.5-turbo", "gpt-4"
}
-# SERVER_URL = "http://127.0.0.1:8002"
+# SERVER_URL = "http://127.0.0.1:8080"
SERVER_URL = "https://proxy-server-l6vsfbzhba-uc.a.run.app"
@@ -87,4 +87,7 @@ class ProxyServer(LLM):
}) as resp:
async for line in resp.content:
if line:
- yield line.decode("utf-8")
+ try:
+ yield line.decode("utf-8")
+ except json.JSONDecodeError:
+ raise Exception(str(line))
diff --git a/continuedev/src/continuedev/libs/util/traceback_parsers.py b/continuedev/src/continuedev/libs/util/traceback_parsers.py
index c31929c1..a2e94c26 100644
--- a/continuedev/src/continuedev/libs/util/traceback_parsers.py
+++ b/continuedev/src/continuedev/libs/util/traceback_parsers.py
@@ -1,24 +1,25 @@
-from typing import Union
-from ...models.main import Traceback
-from boltons import tbutils
+PYTHON_TRACEBACK_PREFIX = "Traceback (most recent call last):"
-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."""
+def get_python_traceback(output: str) -> str:
+ if PYTHON_TRACEBACK_PREFIX in output:
+ return PYTHON_TRACEBACK_PREFIX + output.split(PYTHON_TRACEBACK_PREFIX)[-1]
+ elif "SyntaxError" in output:
+ return "SyntaxError" + output.split("SyntaxError")[-1]
+ else:
+ return None
- # Sometimes paths are not quoted, but they need to be
- if "File \"" not in stdout:
- stdout = stdout.replace("File ", "File \"").replace(
- ", line ", "\", line ")
- try:
- tbutil_parsed_exc = tbutils.ParsedException.from_string(stdout)
- return Traceback.from_tbutil_parsed_exc(tbutil_parsed_exc)
+def get_javascript_traceback(output: str) -> str:
+ lines = output.splitlines()
+ 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
- except Exception:
+ 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/continuedev/src/continuedev/server/ide.py b/continuedev/src/continuedev/server/ide.py
index c66cc142..c83fbc8a 100644
--- a/continuedev/src/continuedev/server/ide.py
+++ b/continuedev/src/continuedev/server/ide.py
@@ -230,7 +230,7 @@ class IdeProtocolServer(AbstractIdeProtocolServer):
resp = await self._send_and_receive_json({}, UniqueIdResponse, "uniqueId")
return resp.uniqueId
- @cached_property
+ @property
def workspace_directory(self) -> str:
return asyncio.run(self.getWorkspaceDirectory())
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..053b4ef4 100644
--- a/continuedev/src/continuedev/steps/on_traceback.py
+++ b/continuedev/src/continuedev/steps/on_traceback.py
@@ -1,3 +1,4 @@
+import os
from ..core.main import Step
from ..core.sdk import ContinueSDK
from .chat import SimpleChatStep
@@ -9,6 +10,14 @@ class DefaultOnTracebackStep(Step):
hide: bool = True
async def run(self, sdk: ContinueSDK):
- sdk.run_step(SimpleChatStep(
+ # Add context for any files in the traceback that are in the workspace
+ for line in self.output.split("\n"):
+ segs = line.split(" ")
+ for seg in segs:
+ if seg.startswith(os.path.sep) and os.path.exists(seg) and os.path.commonprefix([seg, sdk.ide.workspace_directory]) == sdk.ide.workspace_directory:
+ file_contents = await sdk.ide.readFile(seg)
+ await sdk.add_chat_context(f"The contents of {seg}:\n```\n{file_contents}\n```", "", "user")
+
+ 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/package-lock.json b/extension/package-lock.json
index 7e8da126..86c816e0 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "continue",
- "version": "0.0.44",
+ "version": "0.0.47",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "continue",
- "version": "0.0.44",
+ "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 ec348aa5..56a522ac 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.44",
+ "version": "0.0.47",
"publisher": "Continue",
"engines": {
"vscode": "^1.74.0"
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
index d70a3d70..c4f22211 100644
--- a/extension/react-app/src/components/HeaderButtonWithText.tsx
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -14,7 +14,11 @@ const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
<HeaderButton
style={{ padding: "3px" }}
onMouseEnter={() => setHover(true)}
- onMouseLeave={() => setHover(false)}
+ onMouseLeave={() => {
+ setTimeout(() => {
+ setHover(false);
+ }, 100);
+ }}
onClick={props.onClick}
>
<span hidden={!hover}>{props.text}</span>
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
index 525989af..d99b4d96 100644
--- a/extension/react-app/src/components/index.ts
+++ b/extension/react-app/src/components/index.ts
@@ -48,7 +48,7 @@ export const Pre = styled.pre`
max-height: 150px;
overflow-y: scroll;
margin: 0;
- background-color: ${secondaryDark};
+ background-color: ${vscBackground};
border: none;
/* text wrapping */
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) {
</div>
<ComboBox
- disabled={
- history?.timeline.length
- ? history.timeline[history.current_index].step.name ===
- "Waiting for user confirmation"
- : false
- }
+ // disabled={
+ // history?.timeline.length
+ // ? history.timeline[history.current_index].step.name ===
+ // "Waiting for user confirmation"
+ // : false
+ // }
ref={mainTextInputRef}
onEnter={(e) => {
onMainTextInput();
@@ -365,6 +367,14 @@ function GUI(props: GUIProps) {
<ContinueButton onClick={onMainTextInput} />
</TopGUIDiv>
<Footer>
+ <HeaderButtonWithText
+ onClick={() => {
+ client?.sendClear();
+ }}
+ text="Clear History"
+ >
+ <Trash size="1.6em" />
+ </HeaderButtonWithText>
<a href="https://continue.dev/docs" className="no-underline">
<HeaderButtonWithText text="Continue Docs">
<BookOpen size="1.6em" />
@@ -379,14 +389,6 @@ function GUI(props: GUIProps) {
>
<ChatBubbleOvalLeftEllipsis size="1.6em" />
</HeaderButtonWithText>
- <HeaderButtonWithText
- onClick={() => {
- client?.sendClear();
- }}
- text="Clear History"
- >
- <Trash size="1.6em" />
- </HeaderButtonWithText>
</Footer>
</>
);
diff --git a/extension/scripts/continuedev-0.1.1-py3-none-any.whl b/extension/scripts/continuedev-0.1.1-py3-none-any.whl
index 614190c7..b0b84230 100644
--- a/extension/scripts/continuedev-0.1.1-py3-none-any.whl
+++ b/extension/scripts/continuedev-0.1.1-py3-none-any.whl
Binary files differ
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 35f02ac0..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<T>(id: string): T | null {
try {
@@ -70,12 +71,21 @@ export class CapturedTerminal {
private hasRunCommand: boolean = false;
private dataEndsInPrompt(strippedData: string): boolean {
- const lines = this.dataBuffer.split("\n");
+ const lines = strippedData.split("\n");
+ const lastLine = lines[lines.length - 1];
+
return (
lines.length > 0 &&
- (lines[lines.length - 1].includes("bash-") ||
- lines[lines.length - 1].includes(") $ ")) &&
- lines[lines.length - 1].includes("$")
+ (((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)))
);
}
@@ -94,12 +104,6 @@ export class CapturedTerminal {
}
async runCommand(command: string): Promise<string> {
- 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]);
@@ -107,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 {
@@ -127,15 +135,33 @@ 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 = "";
}
}
+ private runningClearToGetPrompt: boolean = false;
+ private seenClear: boolean = false;
+ private commandPromptString: string | undefined = undefined;
+ private resolveMeWhenCommandPromptStringFound:
+ | ((_: unknown) => void)
+ | undefined = undefined;
+
+ private async refreshCommandPromptString(): Promise<string | undefined> {
+ // 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<vscode.ExtensionTerminalOptions>,
onCommandOutput?: (output: string) => void
@@ -153,7 +179,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,
@@ -163,9 +189,57 @@ export class CapturedTerminal {
this.writeEmitter = new vscode.EventEmitter<string>();
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);
for (let listener of this.onDataListeners) {
listener(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;
+}