summaryrefslogtreecommitdiff
path: root/extension
diff options
context:
space:
mode:
authorTy Dunn <ty@tydunn.com>2023-06-16 15:56:01 -0700
committerGitHub <noreply@github.com>2023-06-16 15:56:01 -0700
commitd9e576d0f81a22a0c6e7f0659e67f3fa38a0d1aa (patch)
tree7d798f83ae62f4d28dfb5c2256b01d52e9a5c2d3 /extension
parentc980e01d2f9328d5c37df14bea02f84a4890bc6a (diff)
parent3aa4f014608c09b8da2f4ab95137a959487af245 (diff)
downloadsncontinue-d9e576d0f81a22a0c6e7f0659e67f3fa38a0d1aa.tar.gz
sncontinue-d9e576d0f81a22a0c6e7f0659e67f3fa38a0d1aa.tar.bz2
sncontinue-d9e576d0f81a22a0c6e7f0659e67f3fa38a0d1aa.zip
Merge branch 'main' into too-large
Diffstat (limited to 'extension')
-rw-r--r--extension/package-lock.json4
-rw-r--r--extension/package.json4
-rw-r--r--extension/react-app/src/components/ComboBox.tsx3
-rw-r--r--extension/react-app/src/components/DebugPanel.tsx85
-rw-r--r--extension/react-app/src/components/HeaderButtonWithText.tsx30
-rw-r--r--extension/react-app/src/components/StepContainer.tsx16
-rw-r--r--extension/react-app/src/components/index.ts2
-rw-r--r--extension/react-app/src/index.css4
-rw-r--r--extension/react-app/src/tabs/gui.tsx206
-rw-r--r--extension/scripts/continuedev-0.1.1-py3-none-any.whlbin81620 -> 84291 bytes
-rw-r--r--extension/src/activation/activate.ts38
-rw-r--r--extension/src/continueIdeClient.ts12
-rw-r--r--extension/src/terminal/terminalEmulator.ts140
-rw-r--r--extension/src/util/lcs.ts30
14 files changed, 377 insertions, 197 deletions
diff --git a/extension/package-lock.json b/extension/package-lock.json
index e41cd2c2..86c816e0 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "continue",
- "version": "0.0.40",
+ "version": "0.0.47",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "continue",
- "version": "0.0.40",
+ "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 4b199420..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.40",
+ "version": "0.0.47",
"publisher": "Continue",
"engines": {
"vscode": "^1.74.0"
@@ -96,7 +96,7 @@
{
"type": "webview",
"id": "continue.continueGUIView",
- "name": ")",
+ "name": "GUI",
"visibility": "visible"
}
]
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index ace0605e..2b140567 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -113,6 +113,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
(event.nativeEvent as any).preventDownshiftDefault = true;
if (props.onEnter) props.onEnter(event);
setInputValue("");
+ } else if (event.key === "Tab" && items.length > 0) {
+ setInputValue(items[0].name);
+ event.preventDefault();
}
},
ref: ref as any,
diff --git a/extension/react-app/src/components/DebugPanel.tsx b/extension/react-app/src/components/DebugPanel.tsx
index 11ec2fe2..30f38779 100644
--- a/extension/react-app/src/components/DebugPanel.tsx
+++ b/extension/react-app/src/components/DebugPanel.tsx
@@ -17,39 +17,15 @@ interface DebugPanelProps {
}[];
}
-const GradientContainer = styled.div`
- // Uncomment to get gradient border
- /* background: linear-gradient(
- 101.79deg,
- #12887a 0%,
- #87245c 37.64%,
- #e12637 65.98%,
- #ffb215 110.45%
- ); */
- /* padding: 10px; */
- background-color: ${secondaryDark};
- margin: 0;
- height: 100%;
- /* border: 1px solid white; */
- border-radius: ${defaultBorderRadius};
-`;
-
-const MainDiv = styled.div`
- height: 100%;
- border-radius: ${defaultBorderRadius};
- scrollbar-base-color: transparent;
- background-color: ${vscBackground};
-`;
-
const TabBar = styled.div<{ numTabs: number }>`
display: grid;
grid-template-columns: repeat(${(props) => props.numTabs}, 1fr);
`;
const TabsAndBodyDiv = styled.div`
- display: grid;
- grid-template-rows: auto 1fr;
height: 100%;
+ border-radius: ${defaultBorderRadius};
+ scrollbar-base-color: transparent;
`;
function DebugPanel(props: DebugPanelProps) {
@@ -76,42 +52,43 @@ function DebugPanel(props: DebugPanelProps) {
const [currentTab, setCurrentTab] = useState(0);
return (
- <GradientContainer>
- <MainDiv>
- <TabsAndBodyDiv>
- {props.tabs.length > 1 && (
- <TabBar numTabs={props.tabs.length}>
- {props.tabs.map((tab, index) => {
- return (
- <div
- key={index}
- className={`p-2 cursor-pointer text-center ${
- index === currentTab
- ? "bg-secondary-dark"
- : "bg-vsc-background"
- }`}
- onClick={() => setCurrentTab(index)}
- >
- {tab.title}
- </div>
- );
- })}
- </TabBar>
- )}
+ <TabsAndBodyDiv>
+ {props.tabs.length > 1 && (
+ <TabBar numTabs={props.tabs.length}>
{props.tabs.map((tab, index) => {
return (
<div
key={index}
- hidden={index !== currentTab}
- style={{ scrollbarGutter: "stable both-edges" }}
+ className={`p-2 cursor-pointer text-center ${
+ index === currentTab
+ ? "bg-secondary-dark"
+ : "bg-vsc-background"
+ }`}
+ onClick={() => setCurrentTab(index)}
>
- {tab.element}
+ {tab.title}
</div>
);
})}
- </TabsAndBodyDiv>
- </MainDiv>
- </GradientContainer>
+ </TabBar>
+ )}
+ {props.tabs.map((tab, index) => {
+ return (
+ <div
+ key={index}
+ hidden={index !== currentTab}
+ style={{
+ scrollbarGutter: "stable both-edges",
+ minHeight: "100%",
+ display: "grid",
+ gridTemplateRows: "1fr auto",
+ }}
+ >
+ {tab.element}
+ </div>
+ );
+ })}
+ </TabsAndBodyDiv>
);
}
diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx
new file mode 100644
index 00000000..c4f22211
--- /dev/null
+++ b/extension/react-app/src/components/HeaderButtonWithText.tsx
@@ -0,0 +1,30 @@
+import React, { useState } from "react";
+
+import { HeaderButton } from ".";
+
+interface HeaderButtonWithTextProps {
+ text: string;
+ onClick?: (e: any) => void;
+ children: React.ReactNode;
+}
+
+const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => {
+ const [hover, setHover] = useState(false);
+ return (
+ <HeaderButton
+ style={{ padding: "3px" }}
+ onMouseEnter={() => setHover(true)}
+ onMouseLeave={() => {
+ setTimeout(() => {
+ setHover(false);
+ }, 100);
+ }}
+ onClick={props.onClick}
+ >
+ <span hidden={!hover}>{props.text}</span>
+ {props.children}
+ </HeaderButton>
+ );
+};
+
+export default HeaderButtonWithText;
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
index 48f970d7..480f517f 100644
--- a/extension/react-app/src/components/StepContainer.tsx
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -21,9 +21,7 @@ import {
} from "@styled-icons/heroicons-outline";
import { HistoryNode } from "../../../schema/HistoryNode";
import ReactMarkdown from "react-markdown";
-import ContinueButton from "./ContinueButton";
-import InputAndButton from "./InputAndButton";
-import ToggleErrorDiv from "./ToggleErrorDiv";
+import HeaderButtonWithText from "./HeaderButtonWithText";
interface StepContainerProps {
historyNode: HistoryNode;
@@ -152,23 +150,25 @@ function StepContainer(props: StepContainerProps) {
</HeaderButton> */}
<>
- <HeaderButton
+ <HeaderButtonWithText
onClick={(e) => {
e.stopPropagation();
props.onDelete();
}}
+ text="Delete"
>
<XMark size="1.6em" onClick={props.onDelete} />
- </HeaderButton>
+ </HeaderButtonWithText>
{props.historyNode.observation?.error ? (
- <HeaderButton
+ <HeaderButtonWithText
+ text="Retry"
onClick={(e) => {
e.stopPropagation();
props.onRetry();
}}
>
<ArrowPath size="1.6em" onClick={props.onRetry} />
- </HeaderButton>
+ </HeaderButtonWithText>
) : (
<></>
)}
@@ -193,7 +193,7 @@ function StepContainer(props: StepContainerProps) {
) : (
<ReactMarkdown
key={1}
- className="overflow-scroll"
+ className="overflow-x-scroll"
components={
{
// pre: ({ node, ...props }) => {
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 20599d30..db8afab9 100644
--- a/extension/react-app/src/index.css
+++ b/extension/react-app/src/index.css
@@ -21,7 +21,8 @@
html,
body,
#root {
- height: calc(100%);
+ height: 100%;
+ background-color: var(--vsc-background);
}
body {
@@ -31,4 +32,5 @@ body {
font-family: "Mona Sans", "Arial", sans-serif;
padding: 0px;
margin: 0px;
+ height: 100%;
}
diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx
index 5316f42b..994cb896 100644
--- a/extension/react-app/src/tabs/gui.tsx
+++ b/extension/react-app/src/tabs/gui.tsx
@@ -14,25 +14,18 @@ import StepContainer from "../components/StepContainer";
import useContinueGUIProtocol from "../hooks/useWebsocket";
import {
BookOpen,
- ChatBubbleOvalLeft,
ChatBubbleOvalLeftEllipsis,
Trash,
} from "@styled-icons/heroicons-outline";
import ComboBox from "../components/ComboBox";
import TextDialog from "../components/TextDialog";
+import HeaderButtonWithText from "../components/HeaderButtonWithText";
-const MainDiv = styled.div`
- display: grid;
- grid-template-rows: 1fr auto;
+const TopGUIDiv = styled.div`
+ overflow: hidden;
`;
-let TopGUIDiv = styled.div`
- display: grid;
- grid-template-columns: 1fr;
- background-color: ${vscBackground};
-`;
-
-let UserInputQueueItem = styled.div`
+const UserInputQueueItem = styled.div`
border-radius: ${defaultBorderRadius};
color: gray;
padding: 8px;
@@ -40,7 +33,7 @@ let UserInputQueueItem = styled.div`
text-align: center;
`;
-const TopBar = styled.div`
+const Footer = styled.footer`
display: flex;
flex-direction: row;
gap: 8px;
@@ -201,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);
@@ -213,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);
@@ -303,103 +298,98 @@ function GUI(props: GUIProps) {
setShowFeedbackDialog(false);
}}
></TextDialog>
- <MainDiv>
- <TopGUIDiv
- ref={topGuiDivRef}
- onKeyDown={(e) => {
- if (e.key === "Enter" && e.ctrlKey) {
- onMainTextInput();
- }
- }}
- >
- {typeof client === "undefined" && (
- <>
- <Loader></Loader>
- <p style={{ textAlign: "center" }}>
- Trying to reconnect with server...
- </p>
- </>
- )}
- {history?.timeline.map((node: HistoryNode, index: number) => {
- return (
- <StepContainer
- key={index}
- onUserInput={(input: string) => {
- onStepUserInput(input, index);
- }}
- inFuture={index > history?.current_index}
- historyNode={node}
- onRefinement={(input: string) => {
- client?.sendRefinementInput(input, index);
- }}
- onReverse={() => {
- client?.reverseToIndex(index);
- }}
- onRetry={() => {
- client?.retryAtIndex(index);
- setWaitingForSteps(true);
- }}
- onDelete={() => {
- client?.deleteAtIndex(index);
- }}
- />
- );
- })}
- {waitingForSteps && <Loader></Loader>}
- <div>
- {userInputQueue.map((input) => {
- return <UserInputQueueItem>{input}</UserInputQueueItem>;
- })}
- </div>
-
- <ComboBox
- disabled={
- history?.timeline.length
- ? history.timeline[history.current_index].step.name ===
- "Waiting for user confirmation"
- : false
- }
- ref={mainTextInputRef}
- onEnter={(e) => {
- onMainTextInput();
- e.stopPropagation();
- e.preventDefault();
- }}
- onInputValueChange={() => {}}
- items={availableSlashCommands}
- />
- <ContinueButton onClick={onMainTextInput} />
-
- <TopBar>
- <a href="https://continue.dev/docs" className="no-underline">
- <HeaderButton style={{ padding: "3px" }}>
- Continue Docs
- <BookOpen size="1.6em" />
- </HeaderButton>
- </a>
- <HeaderButton
- style={{ padding: "3px" }}
- onClick={() => {
- // Set dialog open
- setShowFeedbackDialog(true);
+ <TopGUIDiv
+ ref={topGuiDivRef}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && e.ctrlKey) {
+ onMainTextInput();
+ }
+ }}
+ >
+ {typeof client === "undefined" && (
+ <>
+ <Loader></Loader>
+ <p style={{ textAlign: "center" }}>
+ Trying to reconnect with server...
+ </p>
+ </>
+ )}
+ {history?.timeline.map((node: HistoryNode, index: number) => {
+ return (
+ <StepContainer
+ key={index}
+ onUserInput={(input: string) => {
+ onStepUserInput(input, index);
+ }}
+ inFuture={index > history?.current_index}
+ historyNode={node}
+ onRefinement={(input: string) => {
+ client?.sendRefinementInput(input, index);
+ }}
+ onReverse={() => {
+ client?.reverseToIndex(index);
}}
- >
- Feedback
- <ChatBubbleOvalLeftEllipsis size="1.6em" />
- </HeaderButton>
- <HeaderButton
- onClick={() => {
- client?.sendClear();
+ onRetry={() => {
+ client?.retryAtIndex(index);
+ setWaitingForSteps(true);
}}
- style={{ padding: "3px" }}
- >
- Clear History
- <Trash size="1.6em" />
- </HeaderButton>
- </TopBar>
- </TopGUIDiv>
- </MainDiv>
+ onDelete={() => {
+ client?.deleteAtIndex(index);
+ }}
+ />
+ );
+ })}
+ {waitingForSteps && <Loader></Loader>}
+
+ <div>
+ {userInputQueue.map((input) => {
+ return <UserInputQueueItem>{input}</UserInputQueueItem>;
+ })}
+ </div>
+
+ <ComboBox
+ // disabled={
+ // history?.timeline.length
+ // ? history.timeline[history.current_index].step.name ===
+ // "Waiting for user confirmation"
+ // : false
+ // }
+ ref={mainTextInputRef}
+ onEnter={(e) => {
+ onMainTextInput();
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onInputValueChange={() => {}}
+ items={availableSlashCommands}
+ />
+ <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" />
+ </HeaderButtonWithText>
+ </a>
+ <HeaderButtonWithText
+ onClick={() => {
+ // Set dialog open
+ setShowFeedbackDialog(true);
+ }}
+ text="Feedback"
+ >
+ <ChatBubbleOvalLeftEllipsis 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 2f8f1550..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 135a8ec7..32726c86 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,42 @@ 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 (!ideProtocolClient.continueTerminal) {
+ ideProtocolClient.continueTerminal = capturedTerminal;
+ }
+ });
+
+ // 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();
+ if (!ideProtocolClient.continueTerminal) {
+ ideProtocolClient.continueTerminal = capturedTerminal;
+ }
+ });
+
extensionContext = context;
}
diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts
index ef9a91c8..9a93a4ef 100644
--- a/extension/src/continueIdeClient.ts
+++ b/extension/src/continueIdeClient.ts
@@ -323,16 +323,22 @@ class IdeProtocolClient {
return rangeInFiles;
}
- private continueTerminal: CapturedTerminal | undefined;
+ public 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..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 {
@@ -62,21 +63,38 @@ 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 = strippedData.split("\n");
+ const lastLine = lines[lines.length - 1];
+
+ return (
+ 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)))
+ );
+ }
+
private async waitForCommandToFinish() {
return new Promise<string>((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 = [];
@@ -86,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]);
@@ -99,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 {
@@ -112,8 +128,48 @@ export class CapturedTerminal {
private readonly writeEmitter: vscode.EventEmitter<string>;
- 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 += data;
+ if (this.dataEndsInPrompt(strippedData)) {
+ if (this.onCommandOutput) {
+ this.onCommandOutput(stripAnsi(this.splitByCommandsBuffer));
+ }
+ 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
+ ) {
+ this.onCommandOutput = onCommandOutput;
+
+ // this.shellCmd = "bash"; // getDefaultShell();
+ this.shellCmd = getDefaultShell();
const env = { ...(process.env as any) };
if (os.platform() !== "win32") {
@@ -123,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,
@@ -133,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);
}
@@ -154,7 +258,7 @@ export class CapturedTerminal {
// Create and clear the terminal
this.terminal = vscode.window.createTerminal({
- name: terminalName,
+ ...options,
pty: newPty,
});
this.terminal.show();
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;
+}