summaryrefslogtreecommitdiff
path: root/extension/react-app/src/tabs
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src/tabs')
-rw-r--r--extension/react-app/src/tabs/additionalContext.tsx18
-rw-r--r--extension/react-app/src/tabs/chat/MessageDiv.tsx73
-rw-r--r--extension/react-app/src/tabs/chat/index.tsx267
-rw-r--r--extension/react-app/src/tabs/main.tsx189
-rw-r--r--extension/react-app/src/tabs/notebook.tsx285
-rw-r--r--extension/react-app/src/tabs/welcome.tsx22
6 files changed, 854 insertions, 0 deletions
diff --git a/extension/react-app/src/tabs/additionalContext.tsx b/extension/react-app/src/tabs/additionalContext.tsx
new file mode 100644
index 00000000..98fce9f1
--- /dev/null
+++ b/extension/react-app/src/tabs/additionalContext.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { H3, TextArea } from "../components";
+
+function AdditionalContextTab() {
+ return (
+ <div className="mx-5">
+ <H3>Additional Context</H3>
+ <TextArea
+ rows={8}
+ placeholder="Copy and paste information related to the bug from GitHub Issues, Slack threads, or other notes here."
+ className="additionalContextTextarea"
+ ></TextArea>
+ <br></br>
+ </div>
+ );
+}
+
+export default AdditionalContextTab;
diff --git a/extension/react-app/src/tabs/chat/MessageDiv.tsx b/extension/react-app/src/tabs/chat/MessageDiv.tsx
new file mode 100644
index 00000000..ab632220
--- /dev/null
+++ b/extension/react-app/src/tabs/chat/MessageDiv.tsx
@@ -0,0 +1,73 @@
+import React, { useEffect } from "react";
+import { ChatMessage } from "../../redux/store";
+import styled from "styled-components";
+import {
+ buttonColor,
+ defaultBorderRadius,
+ secondaryDark,
+} from "../../components";
+import VSCodeFileLink from "../../components/VSCodeFileLink";
+import ReactMarkdown from "react-markdown";
+import "../../highlight/dark.min.css";
+import hljs from "highlight.js";
+import { useSelector } from "react-redux";
+import { selectIsStreaming } from "../../redux/selectors/chatSelectors";
+
+const Container = styled.div`
+ padding-left: 8px;
+ padding-right: 8px;
+ border-radius: 8px;
+ margin: 3px;
+ width: fit-content;
+ max-width: 75%;
+ overflow: scroll;
+ word-wrap: break-word;
+ -ms-word-wrap: break-word;
+ height: fit-content;
+ overflow: hidden;
+ background-color: ${(props) => {
+ if (props.role === "user") {
+ return buttonColor;
+ } else {
+ return secondaryDark;
+ }
+ }};
+ float: ${(props) => {
+ if (props.role === "user") {
+ return "right";
+ } else {
+ return "left";
+ }
+ }};
+ display: block;
+
+ & pre {
+ border: 1px solid gray;
+ border-radius: ${defaultBorderRadius};
+ }
+`;
+
+function MessageDiv(props: ChatMessage) {
+ const [richContent, setRichContent] = React.useState<JSX.Element[]>([]);
+ const isStreaming = useSelector(selectIsStreaming);
+
+ useEffect(() => {
+ if (!isStreaming) {
+ hljs.highlightAll();
+ }
+ }, [richContent, isStreaming]);
+
+ useEffect(() => {
+ setRichContent([<ReactMarkdown key={1}>{props.content}</ReactMarkdown>]);
+ }, [props.content]);
+
+ return (
+ <>
+ <div className="overflow-auto">
+ <Container role={props.role}>{richContent}</Container>
+ </div>
+ </>
+ );
+}
+
+export default MessageDiv;
diff --git a/extension/react-app/src/tabs/chat/index.tsx b/extension/react-app/src/tabs/chat/index.tsx
new file mode 100644
index 00000000..a93ad4f9
--- /dev/null
+++ b/extension/react-app/src/tabs/chat/index.tsx
@@ -0,0 +1,267 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { selectChatMessages } from "../../redux/selectors/chatSelectors";
+import MessageDiv from "./MessageDiv";
+import styled from "styled-components";
+import { addMessage, setIsStreaming } from "../../redux/slices/chatSlice";
+import { AnyAction, Dispatch } from "@reduxjs/toolkit";
+import { closeStream, streamUpdate } from "../../redux/slices/chatSlice";
+import { ChatMessage, RootStore } from "../../redux/store";
+import { postVscMessage, vscRequest } from "../../vscode";
+import { defaultBorderRadius, Loader } from "../../components";
+import { selectHighlightedCode } from "../../redux/selectors/miscSelectors";
+import { readRangeInVirtualFileSystem } from "../../util";
+import { selectDebugContext } from "../../redux/selectors/debugContextSelectors";
+
+let textEntryBarHeight = "30px";
+
+const ChatContainer = styled.div`
+ display: grid;
+ grid-template-rows: 1fr auto;
+ height: 100%;
+`;
+
+const BottomDiv = styled.div`
+ display: grid;
+ grid-template-rows: auto auto;
+`;
+
+const BottomButton = styled.button(
+ (props: { active: boolean }) => `
+ font-size: 10px;
+ border: none;
+ color: white;
+ margin-right: 4px;
+ cursor: pointer;
+ background-color: ${props.active ? "black" : "gray"};
+ border-radius: ${defaultBorderRadius};
+ padding: 8px;
+`
+);
+
+const TextEntryBar = styled.input`
+ height: ${textEntryBarHeight};
+ border-bottom-left-radius: ${defaultBorderRadius};
+ border-bottom-right-radius: ${defaultBorderRadius};
+ padding: 8px;
+ border: 1px solid white;
+ background-color: black;
+ color: white;
+ outline: none;
+`;
+
+function ChatTab() {
+ const dispatch = useDispatch();
+ const chatMessages = useSelector(selectChatMessages);
+ const isStreaming = useSelector((state: RootStore) => state.chat.isStreaming);
+ const baseUrl = useSelector((state: RootStore) => state.config.apiUrl);
+ const debugContext = useSelector(selectDebugContext);
+
+ const [includeHighlightedCode, setIncludeHighlightedCode] = useState(true);
+ const [writeToEditor, setWriteToEditor] = useState(false);
+ const [waitingForResponse, setWaitingForResponse] = useState(false);
+
+ const highlightedCode = useSelector(selectHighlightedCode);
+
+ const streamToStateThunk = useCallback(
+ (dispatch: Dispatch<AnyAction>, getResponse: () => Promise<Response>) => {
+ let streamToCursor = writeToEditor;
+ getResponse().then((resp) => {
+ setWaitingForResponse(false);
+ if (resp.body) {
+ resp.body.pipeTo(
+ new WritableStream({
+ write(chunk) {
+ let update = new TextDecoder("utf-8").decode(chunk);
+ dispatch(streamUpdate(update));
+ if (streamToCursor) {
+ postVscMessage("streamUpdate", { update });
+ }
+ },
+ close() {
+ dispatch(closeStream());
+ if (streamToCursor) {
+ postVscMessage("closeStream", null);
+ }
+ },
+ })
+ );
+ }
+ });
+ },
+ [writeToEditor]
+ );
+
+ const compileHiddenChatMessages = useCallback(async () => {
+ let messages: ChatMessage[] = [];
+ if (
+ includeHighlightedCode &&
+ highlightedCode?.filepath !== undefined &&
+ highlightedCode?.range !== undefined &&
+ debugContext.filesystem[highlightedCode.filepath] !== undefined
+ ) {
+ let fileContents = readRangeInVirtualFileSystem(
+ highlightedCode,
+ debugContext.filesystem
+ );
+ if (fileContents) {
+ messages.push({
+ role: "user",
+ content: fileContents,
+ });
+ }
+ } else {
+ // Similarity search over workspace
+ let data = await vscRequest("queryEmbeddings", {
+ query: chatMessages[chatMessages.length - 1].content,
+ });
+ let codeContextMessages = data.results.map(
+ (result: { id: string; document: string }) => {
+ let msg: ChatMessage = {
+ role: "user",
+ content: `File: ${result.id} \n ${result.document}`,
+ };
+ return msg;
+ }
+ );
+ codeContextMessages.push({
+ role: "user",
+ content:
+ "Use the above code to help you answer the question below. Answer in asterisk bullet points, and give the full path whenever you reference files.",
+ });
+ messages.push(...codeContextMessages);
+ }
+
+ let systemMsgContent = writeToEditor
+ ? "Respond only with the exact code requested, no additional text."
+ : "Use the above code to help you answer the question below. Respond in markdown if using bullets or other special formatting, being sure to specify language for code blocks.";
+
+ messages.push({
+ role: "system",
+ content: systemMsgContent,
+ });
+ return messages;
+ }, [highlightedCode, chatMessages, includeHighlightedCode, writeToEditor]);
+
+ useEffect(() => {
+ if (
+ chatMessages.length > 0 &&
+ chatMessages[chatMessages.length - 1].role === "user" &&
+ !isStreaming
+ ) {
+ dispatch(setIsStreaming(true));
+ streamToStateThunk(dispatch, async () => {
+ if (chatMessages.length === 0) {
+ return new Promise((resolve, _) => resolve(new Response()));
+ }
+ let hiddenChatMessages = await compileHiddenChatMessages();
+ let augmentedMessages = [
+ ...chatMessages.slice(0, -1),
+ ...hiddenChatMessages,
+ chatMessages[chatMessages.length - 1],
+ ];
+ console.log(augmentedMessages);
+ // The autogenerated client can't handle streams, so have to go raw
+ return fetch(`${baseUrl}/chat/complete`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ messages: augmentedMessages,
+ }),
+ });
+ });
+ }
+ }, [chatMessages, dispatch, isStreaming, highlightedCode]);
+
+ const chatMessagesDiv = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ // Scroll to bottom
+ let interval = setInterval(() => {
+ if (chatMessagesDiv.current && !waitingForResponse) {
+ chatMessagesDiv.current.scrollTop += Math.max(
+ 4,
+ 0.05 * chatMessagesDiv.current.scrollHeight -
+ chatMessagesDiv.current.clientHeight -
+ chatMessagesDiv.current.scrollTop
+ );
+ if (
+ chatMessagesDiv.current.scrollTop >=
+ chatMessagesDiv.current.scrollHeight -
+ chatMessagesDiv.current.clientHeight
+ ) {
+ clearInterval(interval);
+ }
+ }
+ }, 10);
+ }, [chatMessages, chatMessagesDiv, waitingForResponse]);
+
+ return (
+ <ChatContainer>
+ <div className="mx-5 overflow-y-scroll" ref={chatMessagesDiv}>
+ <h1>Chat</h1>
+ <hr></hr>
+ <div>
+ {chatMessages.length > 0 ? (
+ chatMessages.map((message, idx) => {
+ return <MessageDiv key={idx} {...message}></MessageDiv>;
+ })
+ ) : (
+ <p className="text-gray-400 m-auto text-center">
+ You can ask questions about your codebase or ask for code written
+ directly in the editor.
+ </p>
+ )}
+ {waitingForResponse && <Loader></Loader>}
+ </div>
+ </div>
+
+ <BottomDiv>
+ <div className="h-12 bg-secondary-">
+ <div className="flex items-center p-2">
+ {/* <p className="mr-auto text-xs">
+ Highlighted code is automatically included in your chat message.
+ </p> */}
+ <BottomButton
+ className="ml-auto"
+ active={writeToEditor}
+ onClick={() => {
+ setWriteToEditor(!writeToEditor);
+ }}
+ >
+ {writeToEditor ? "Writing to editor" : "Write to editor"}
+ </BottomButton>
+
+ <BottomButton
+ active={includeHighlightedCode}
+ onClick={() => {
+ setIncludeHighlightedCode(!includeHighlightedCode);
+ }}
+ >
+ {includeHighlightedCode
+ ? "Including highlighted code"
+ : "Automatically finding relevant code"}
+ </BottomButton>
+ </div>
+ </div>
+ <TextEntryBar
+ type="text"
+ placeholder="Enter your message here"
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && e.currentTarget.value !== "") {
+ console.log("Sending message", e.currentTarget.value);
+ dispatch(
+ addMessage({ content: e.currentTarget.value, role: "user" })
+ );
+ (e.target as any).value = "";
+ setWaitingForResponse(true);
+ }
+ }}
+ ></TextEntryBar>
+ </BottomDiv>
+ </ChatContainer>
+ );
+}
+
+export default ChatTab;
diff --git a/extension/react-app/src/tabs/main.tsx b/extension/react-app/src/tabs/main.tsx
new file mode 100644
index 00000000..a8b3300d
--- /dev/null
+++ b/extension/react-app/src/tabs/main.tsx
@@ -0,0 +1,189 @@
+import React, { useEffect, useState } from "react";
+import { H3, TextArea, Button, Pre, Loader } from "../components";
+import styled from "styled-components";
+import { postVscMessage, withProgress } from "../vscode";
+import { useDebugContextValue } from "../redux/hooks";
+import CodeMultiselect from "../components/CodeMultiselect";
+import { useSelector } from "react-redux";
+import { selectDebugContext } from "../redux/selectors/debugContextSelectors";
+import { useDispatch } from "react-redux";
+import { updateValue } from "../redux/slices/debugContexSlice";
+import { setWorkspacePath } from "../redux/slices/configSlice";
+import { SerializedDebugContext } from "../../../src/client";
+import { useEditCache } from "../util/editCache";
+import { useApi } from "../util/api";
+
+const ButtonDiv = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 4px;
+ margin: 4px;
+ flex-wrap: wrap;
+
+ & button {
+ flex-grow: 1;
+ }
+`;
+
+function MainTab(props: any) {
+ const dispatch = useDispatch();
+
+ const [suggestion, setSuggestion] = useState("");
+ const [traceback, setTraceback] = useDebugContextValue("traceback", "");
+ const [selectedRanges, setSelectedRanges] = useDebugContextValue(
+ "rangesInFiles",
+ []
+ );
+
+ const editCache = useEditCache();
+ const { debugApi } = useApi();
+
+ const [responseLoading, setResponseLoading] = useState(false);
+
+ let debugContext = useSelector(selectDebugContext);
+
+ useEffect(() => {
+ editCache.preloadEdit(debugContext);
+ }, [debugContext]);
+
+ function postVscMessageWithDebugContext(
+ type: string,
+ overrideDebugContext: SerializedDebugContext | null = null
+ ) {
+ postVscMessage(type, {
+ debugContext: overrideDebugContext || debugContext,
+ });
+ }
+
+ function launchFindSuspiciousCode(newTraceback: string) {
+ // setTraceback's effects don't occur immediately, so we have to add it to the debug context manually
+ let updatedDebugContext = {
+ ...debugContext,
+ traceback: newTraceback,
+ };
+ postVscMessageWithDebugContext("findSuspiciousCode", updatedDebugContext);
+ postVscMessageWithDebugContext("preloadEdit", updatedDebugContext);
+ }
+
+ useEffect(() => {
+ const eventListener = (event: any) => {
+ switch (event.data.type) {
+ case "suggestFix":
+ case "explainCode":
+ case "listTenThings":
+ setSuggestion(event.data.value);
+ setResponseLoading(false);
+ break;
+ case "traceback":
+ setTraceback(event.data.value);
+ launchFindSuspiciousCode(event.data.value);
+ break;
+ case "workspacePath":
+ dispatch(setWorkspacePath(event.data.value));
+ break;
+ }
+ };
+ window.addEventListener("message", eventListener);
+
+ return () => window.removeEventListener("message", eventListener);
+ }, [debugContext, selectedRanges]);
+
+ return (
+ <div className="mx-5">
+ <h1>Debug Panel</h1>
+
+ <H3>Code Sections</H3>
+ <CodeMultiselect></CodeMultiselect>
+
+ <H3>Bug Description</H3>
+ <TextArea
+ id="bugDescription"
+ name="bugDescription"
+ className="bugDescription"
+ rows={4}
+ cols={50}
+ placeholder="Describe your bug..."
+ ></TextArea>
+
+ <H3>Stack Trace</H3>
+ <TextArea
+ id="traceback"
+ className="traceback"
+ name="traceback"
+ rows={4}
+ cols={50}
+ placeholder="Paste stack trace here"
+ onChange={(e) => {
+ setTraceback(e.target.value);
+ dispatch(updateValue({ key: "traceback", value: e.target.value }));
+ // postVscMessageWithDebugContext("findSuspiciousCode");
+ }}
+ onPaste={(e) => {
+ let pasted = e.clipboardData.getData("text");
+ console.log("PASTED", pasted);
+ setTraceback(pasted);
+ launchFindSuspiciousCode(pasted);
+ }}
+ value={traceback}
+ ></TextArea>
+
+ <select
+ hidden
+ id="relevantVars"
+ className="relevantVars"
+ name="relevantVars"
+ ></select>
+
+ <ButtonDiv>
+ <Button
+ onClick={() => {
+ postVscMessageWithDebugContext("explainCode");
+ setResponseLoading(true);
+ }}
+ >
+ Explain Code
+ </Button>
+ <Button
+ onClick={() => {
+ postVscMessageWithDebugContext("suggestFix");
+ setResponseLoading(true);
+ }}
+ >
+ Generate Ideas
+ </Button>
+ <Button
+ disabled={selectedRanges.length === 0}
+ onClick={async () => {
+ withProgress("Generating Fix", async () => {
+ let edits = await editCache.getEdit(debugContext);
+ postVscMessage("makeEdit", { edits });
+ });
+ }}
+ >
+ Suggest Fix
+ </Button>
+ <Button
+ disabled={selectedRanges.length === 0}
+ onClick={() => {
+ postVscMessageWithDebugContext("generateUnitTest");
+ }}
+ >
+ Create Test
+ </Button>
+ </ButtonDiv>
+ <Loader hidden={!responseLoading}></Loader>
+
+ <Pre
+ className="fixSuggestion"
+ hidden={!(typeof suggestion === "string" && suggestion.length > 0)}
+ >
+ {suggestion}
+ </Pre>
+
+ <br></br>
+ </div>
+ );
+}
+
+export default MainTab;
diff --git a/extension/react-app/src/tabs/notebook.tsx b/extension/react-app/src/tabs/notebook.tsx
new file mode 100644
index 00000000..a9c69c5b
--- /dev/null
+++ b/extension/react-app/src/tabs/notebook.tsx
@@ -0,0 +1,285 @@
+import styled from "styled-components";
+import {
+ Button,
+ defaultBorderRadius,
+ vscBackground,
+ MainTextInput,
+ Loader,
+} from "../components";
+import ContinueButton from "../components/ContinueButton";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { History } from "../../../schema/History";
+import { HistoryNode } from "../../../schema/HistoryNode";
+import StepContainer from "../components/StepContainer";
+import { useSelector } from "react-redux";
+import { RootStore } from "../redux/store";
+import useContinueWebsocket from "../hooks/useWebsocket";
+
+let TopNotebookDiv = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+`;
+
+let UserInputQueueItem = styled.div`
+ border-radius: ${defaultBorderRadius};
+ color: gray;
+ padding: 8px;
+ margin: 8px;
+ text-align: center;
+`;
+
+interface NotebookProps {
+ firstObservation?: any;
+}
+
+function Notebook(props: NotebookProps) {
+ const serverUrl = useSelector((state: RootStore) => state.config.apiUrl);
+
+ const [waitingForSteps, setWaitingForSteps] = useState(false);
+ const [userInputQueue, setUserInputQueue] = useState<string[]>([]);
+ const [history, setHistory] = useState<History | undefined>();
+ // {
+ // timeline: [
+ // {
+ // step: {
+ // name: "RunCodeStep",
+ // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // description:
+ // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`",
+ // },
+ // output: [
+ // {
+ // traceback: {
+ // frames: [
+ // {
+ // filepath:
+ // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // lineno: 7,
+ // function: "<module>",
+ // code: "print(sum(first, second))",
+ // },
+ // ],
+ // message: "unsupported operand type(s) for +: 'int' and 'str'",
+ // error_type:
+ // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError',
+ // full_traceback:
+ // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'",
+ // },
+ // },
+ // null,
+ // ],
+ // },
+ // {
+ // step: {
+ // name: "EditCodeStep",
+ // range_in_files: [
+ // {
+ // filepath:
+ // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // range: {
+ // start: {
+ // line: 0,
+ // character: 0,
+ // },
+ // end: {
+ // line: 6,
+ // character: 25,
+ // },
+ // },
+ // },
+ // ],
+ // prompt:
+ // "I ran into this problem with my Python code:\n\n Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'\n\n Below are the files that might need to be fixed:\n\n {code}\n\n This is what the code should be in order to avoid the problem:\n",
+ // description:
+ // "Editing files: /Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // },
+ // output: [
+ // null,
+ // {
+ // reversible: true,
+ // actions: [
+ // {
+ // reversible: true,
+ // filesystem: {},
+ // filepath:
+ // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // range: {
+ // start: {
+ // line: 0,
+ // character: 0,
+ // },
+ // end: {
+ // line: 6,
+ // character: 25,
+ // },
+ // },
+ // replacement:
+ // "\nfrom sum import sum\n\nfirst = 1\nsecond = 2\n\nprint(sum(first, second))",
+ // },
+ // ],
+ // },
+ // ],
+ // },
+ // {
+ // step: {
+ // name: "SolveTracebackStep",
+ // traceback: {
+ // frames: [
+ // {
+ // filepath:
+ // "/Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // lineno: 7,
+ // function: "<module>",
+ // code: "print(sum(first, second))",
+ // },
+ // ],
+ // message: "unsupported operand type(s) for +: 'int' and 'str'",
+ // error_type:
+ // ' ^^^^^^^^^^^^^^^^^^\n File "/Users/natesesti/Desktop/continue/extension/examples/python/sum.py", line 2, in sum\n return a + b\n ~~^~~\nTypeError',
+ // full_traceback:
+ // "Traceback (most recent call last):\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/main.py\", line 7, in <module>\n print(sum(first, second))\n ^^^^^^^^^^^^^^^^^^\n File \"/Users/natesesti/Desktop/continue/extension/examples/python/sum.py\", line 2, in sum\n return a + b\n ~~^~~\nTypeError: unsupported operand type(s) for +: 'int' and 'str'",
+ // },
+ // description: "Running step: SolveTracebackStep",
+ // },
+ // output: [null, null],
+ // },
+ // {
+ // step: {
+ // name: "RunCodeStep",
+ // cmd: "python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py",
+ // description:
+ // "Run `python3 /Users/natesesti/Desktop/continue/extension/examples/python/main.py`",
+ // },
+ // output: [null, null],
+ // },
+ // ],
+ // current_index: 0,
+ // } as any
+ // );
+
+ const { send: websocketSend } = useContinueWebsocket(serverUrl, (msg) => {
+ let data = JSON.parse(msg.data);
+ if (data.messageType === "state") {
+ setWaitingForSteps(data.state.active);
+ setHistory(data.state.history);
+ setUserInputQueue(data.state.user_input_queue);
+ }
+ });
+
+ // useEffect(() => {
+ // (async () => {
+ // if (sessionId && props.firstObservation) {
+ // let resp = await fetch(serverUrl + "/observation", {
+ // method: "POST",
+ // headers: new Headers({
+ // "x-continue-session-id": sessionId,
+ // }),
+ // body: JSON.stringify({
+ // observation: props.firstObservation,
+ // }),
+ // });
+ // }
+ // })();
+ // }, [props.firstObservation]);
+
+ const mainTextInputRef = useRef<HTMLTextAreaElement>(null);
+
+ useEffect(() => {
+ if (mainTextInputRef.current) {
+ mainTextInputRef.current.focus();
+ let handler = (event: any) => {
+ if (event.data.type === "focusContinueInput") {
+ mainTextInputRef.current?.focus();
+ }
+ };
+ window.addEventListener("message", handler);
+ return () => {
+ window.removeEventListener("message", handler);
+ };
+ }
+ }, [mainTextInputRef]);
+
+ const onMainTextInput = () => {
+ if (mainTextInputRef.current) {
+ let value = mainTextInputRef.current.value;
+ setWaitingForSteps(true);
+ websocketSend({
+ messageType: "main_input",
+ value: value,
+ });
+ setUserInputQueue((queue) => {
+ return [...queue, value];
+ });
+ mainTextInputRef.current.value = "";
+ mainTextInputRef.current.style.height = "";
+ }
+ };
+
+ const onStepUserInput = (input: string, index: number) => {
+ console.log("Sending step user input", input, index);
+ websocketSend({
+ messageType: "step_user_input",
+ value: input,
+ index,
+ });
+ };
+
+ // const iterations = useSelector(selectIterations);
+ return (
+ <TopNotebookDiv>
+ {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) => {
+ websocketSend({
+ messageType: "refinement_input",
+ value: input,
+ index,
+ });
+ }}
+ onReverse={() => {
+ websocketSend({
+ messageType: "reverse",
+ index,
+ });
+ }}
+ />
+ );
+ })}
+ {waitingForSteps && <Loader></Loader>}
+
+ <div>
+ {userInputQueue.map((input) => {
+ return <UserInputQueueItem>{input}</UserInputQueueItem>;
+ })}
+ </div>
+
+ <MainTextInput
+ ref={mainTextInputRef}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ onMainTextInput();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }}
+ rows={1}
+ onChange={() => {
+ let textarea = mainTextInputRef.current!;
+ textarea.style.height = ""; /* Reset the height*/
+ textarea.style.height =
+ Math.min(textarea.scrollHeight - 15, 500) + "px";
+ }}
+ ></MainTextInput>
+ <ContinueButton onClick={onMainTextInput}></ContinueButton>
+ </TopNotebookDiv>
+ );
+}
+
+export default Notebook;
diff --git a/extension/react-app/src/tabs/welcome.tsx b/extension/react-app/src/tabs/welcome.tsx
new file mode 100644
index 00000000..c29d260a
--- /dev/null
+++ b/extension/react-app/src/tabs/welcome.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+function WelcomeTab() {
+ return (
+ <div className="mx-5">
+ <h1>Welcome to Continue</h1>
+
+ <p>
+ Learn more in the{" "}
+ <a href="https://www.notion.so/continue-dev/Continue-User-Guide-1c6ad99887d0474d9e42206f6c98efa4">
+ Continue User Guide
+ </a>{" "}
+ </p>
+ <p>Send Nate or Ty your feedback:</p>
+ <p>1. What excites you about Continue?</p>
+ <p>2. What did you struggle with when using Continue?</p>
+ <p>3. How do you wish Continue worked?</p>
+ </div>
+ );
+}
+
+export default WelcomeTab;