summaryrefslogtreecommitdiff
path: root/extension/react-app/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src')
-rw-r--r--extension/react-app/src/App.tsx29
-rw-r--r--extension/react-app/src/TestPage.tsx33
-rw-r--r--extension/react-app/src/assets/Hubot-Sans.woff2bin0 -> 165932 bytes
-rw-r--r--extension/react-app/src/assets/Mona-Sans.woff2bin0 -> 133748 bytes
-rw-r--r--extension/react-app/src/assets/react.svg1
-rw-r--r--extension/react-app/src/components/CodeBlock.tsx57
-rw-r--r--extension/react-app/src/components/CodeMultiselect.tsx276
-rw-r--r--extension/react-app/src/components/ContinueButton.tsx37
-rw-r--r--extension/react-app/src/components/DebugPanel.tsx121
-rw-r--r--extension/react-app/src/components/IterationContainer.tsx77
-rw-r--r--extension/react-app/src/components/StepContainer.tsx208
-rw-r--r--extension/react-app/src/components/SubContainer.tsx24
-rw-r--r--extension/react-app/src/components/VSCodeFileLink.tsx17
-rw-r--r--extension/react-app/src/components/index.ts136
-rw-r--r--extension/react-app/src/continue_arrow.pngbin0 -> 1350 bytes
-rw-r--r--extension/react-app/src/highlight/dark.min.css53
-rw-r--r--extension/react-app/src/hooks/useArrayState.ts29
-rw-r--r--extension/react-app/src/hooks/useWebsocket.ts67
-rw-r--r--extension/react-app/src/index.css34
-rw-r--r--extension/react-app/src/main.tsx10
-rw-r--r--extension/react-app/src/redux/hooks.ts21
-rw-r--r--extension/react-app/src/redux/selectors/chatSelectors.ts11
-rw-r--r--extension/react-app/src/redux/selectors/debugContextSelectors.ts29
-rw-r--r--extension/react-app/src/redux/selectors/miscSelectors.ts5
-rw-r--r--extension/react-app/src/redux/slices/chatSlice.ts93
-rw-r--r--extension/react-app/src/redux/slices/configSlice.ts45
-rw-r--r--extension/react-app/src/redux/slices/debugContexSlice.ts149
-rw-r--r--extension/react-app/src/redux/slices/miscSlice.ts16
-rw-r--r--extension/react-app/src/redux/store.ts43
-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
-rw-r--r--extension/react-app/src/util/api.ts43
-rw-r--r--extension/react-app/src/util/editCache.ts89
-rw-r--r--extension/react-app/src/util/index.ts27
-rw-r--r--extension/react-app/src/vite-env.d.ts1
-rw-r--r--extension/react-app/src/vscode/index.ts47
40 files changed, 2682 insertions, 0 deletions
diff --git a/extension/react-app/src/App.tsx b/extension/react-app/src/App.tsx
new file mode 100644
index 00000000..0c40ced1
--- /dev/null
+++ b/extension/react-app/src/App.tsx
@@ -0,0 +1,29 @@
+import DebugPanel from "./components/DebugPanel";
+import MainTab from "./tabs/main";
+import { Provider } from "react-redux";
+import store from "./redux/store";
+import WelcomeTab from "./tabs/welcome";
+import ChatTab from "./tabs/chat";
+import Notebook from "./tabs/notebook";
+
+function App() {
+ return (
+ <>
+ <Provider store={store}>
+ <DebugPanel
+ tabs={[
+ {
+ element: <Notebook />,
+ title: "Notebook",
+ },
+ // { element: <MainTab />, title: "Debug Panel" },
+ // { element: <WelcomeTab />, title: "Welcome" },
+ // { element: <ChatTab />, title: "Chat" },
+ ]}
+ ></DebugPanel>
+ </Provider>
+ </>
+ );
+}
+
+export default App;
diff --git a/extension/react-app/src/TestPage.tsx b/extension/react-app/src/TestPage.tsx
new file mode 100644
index 00000000..d104980b
--- /dev/null
+++ b/extension/react-app/src/TestPage.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import styled from "styled-components";
+
+const SideBySideDiv = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr;
+ grid-template-areas: "left right";
+`;
+
+const LeftDiv = styled.div`
+ grid-area: left;
+`;
+
+const RightDiv = styled.div`
+ grid-area: right;
+`;
+
+function TestPage() {
+ return (
+ <div>
+ <h1>Continue</h1>
+ <SideBySideDiv>
+ <LeftDiv>
+ <h2>Left</h2>
+ </LeftDiv>
+ <RightDiv>
+ <h2>Right</h2>
+ </RightDiv>
+ </SideBySideDiv>
+ </div>
+ );
+}
diff --git a/extension/react-app/src/assets/Hubot-Sans.woff2 b/extension/react-app/src/assets/Hubot-Sans.woff2
new file mode 100644
index 00000000..5089fc47
--- /dev/null
+++ b/extension/react-app/src/assets/Hubot-Sans.woff2
Binary files differ
diff --git a/extension/react-app/src/assets/Mona-Sans.woff2 b/extension/react-app/src/assets/Mona-Sans.woff2
new file mode 100644
index 00000000..8208a500
--- /dev/null
+++ b/extension/react-app/src/assets/Mona-Sans.woff2
Binary files differ
diff --git a/extension/react-app/src/assets/react.svg b/extension/react-app/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/extension/react-app/src/assets/react.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file
diff --git a/extension/react-app/src/components/CodeBlock.tsx b/extension/react-app/src/components/CodeBlock.tsx
new file mode 100644
index 00000000..4c10ab23
--- /dev/null
+++ b/extension/react-app/src/components/CodeBlock.tsx
@@ -0,0 +1,57 @@
+import hljs from "highlight.js";
+import { useEffect } from "react";
+import styled from "styled-components";
+import { defaultBorderRadius, vscBackground } from ".";
+
+import { Clipboard } from "@styled-icons/heroicons-outline";
+
+const StyledPre = styled.pre`
+ overflow: scroll;
+ border: 1px solid gray;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${vscBackground};
+`;
+
+const StyledCode = styled.code`
+ background-color: ${vscBackground};
+`;
+
+const StyledCopyButton = styled.button`
+ float: right;
+ border: none;
+ background-color: ${vscBackground};
+ cursor: pointer;
+ padding: 0;
+ margin: 4px;
+ &:hover {
+ color: #fff;
+ }
+`;
+
+function CopyButton(props: { textToCopy: string }) {
+ return (
+ <>
+ <StyledCopyButton
+ onClick={() => {
+ navigator.clipboard.writeText(props.textToCopy);
+ }}
+ >
+ <Clipboard color="white" size="1.4em" />
+ </StyledCopyButton>
+ </>
+ );
+}
+
+function CodeBlock(props: { language?: string; children: string }) {
+ useEffect(() => {
+ hljs.highlightAll();
+ }, [props.children]);
+ return (
+ <StyledPre>
+ <CopyButton textToCopy={props.children} />
+ <StyledCode>{props.children}</StyledCode>
+ </StyledPre>
+ );
+}
+
+export default CodeBlock;
diff --git a/extension/react-app/src/components/CodeMultiselect.tsx b/extension/react-app/src/components/CodeMultiselect.tsx
new file mode 100644
index 00000000..626ae42f
--- /dev/null
+++ b/extension/react-app/src/components/CodeMultiselect.tsx
@@ -0,0 +1,276 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+import { Button, buttonColor, defaultBorderRadius, secondaryDark } from ".";
+import { useSelector } from "react-redux";
+import {
+ selectDebugContext,
+ selectAllRangesInFiles,
+ selectRangesMask,
+} from "../redux/selectors/debugContextSelectors";
+import "../highlight/dark.min.css";
+import hljs from "highlight.js";
+import { postVscMessage } from "../vscode";
+import { RootStore } from "../redux/store";
+import { useDispatch } from "react-redux";
+import {
+ addRangeInFile,
+ deleteRangeInFileAt,
+ toggleSelectionAt,
+ updateFileSystem,
+} from "../redux/slices/debugContexSlice";
+import { RangeInFile } from "../../../src/client";
+import { readRangeInVirtualFileSystem } from "../util";
+
+//#region Styled Components
+
+const MultiSelectContainer = styled.div`
+ border-radius: ${defaultBorderRadius};
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`;
+
+const MultiSelectHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: left;
+ border-bottom: 1px solid gray;
+ padding-left: 4px;
+ padding-right: 4px;
+ & p {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ -ms-wrap-flow: break-word;
+ overflow: hidden;
+ }
+`;
+
+const MultiSelectOption = styled.div`
+ border-radius: ${defaultBorderRadius};
+ padding-top: 4px;
+ cursor: pointer;
+ background-color: ${secondaryDark};
+`;
+
+const DeleteSelectedRangeButton = styled(Button)`
+ align-self: right;
+ padding: 0px;
+ margin-top: 0;
+ aspect-ratio: 1/1;
+ height: 28px;
+`;
+
+const ToggleHighlightButton = styled(Button)`
+ display: grid;
+ justify-content: center;
+ align-items: center;
+ grid-template-columns: 30px 1fr;
+ margin-left: 20px;
+ order: 1;
+ width: fit-content;
+`;
+
+//#endregion
+
+//#region Path Formatting
+
+const filenameToLanguageMap: any = {
+ py: "python",
+ js: "javascript",
+ ts: "typescript",
+ html: "html",
+ css: "css",
+ java: "java",
+ c: "c",
+ cpp: "cpp",
+ cs: "csharp",
+ go: "go",
+ rb: "ruby",
+ rs: "rust",
+ swift: "swift",
+ php: "php",
+ scala: "scala",
+ kt: "kotlin",
+ dart: "dart",
+ hs: "haskell",
+ lua: "lua",
+ pl: "perl",
+ r: "r",
+ sql: "sql",
+ vb: "vb",
+ xml: "xml",
+ yaml: "yaml",
+};
+
+function filenameToLanguage(filename: string): string {
+ const extension = filename.split(".").pop();
+ if (extension === undefined) {
+ return "";
+ }
+ return filenameToLanguageMap[extension] || "";
+}
+
+function formatPathRelativeToWorkspace(
+ path: string,
+ workspacePath: string | undefined
+) {
+ if (workspacePath === undefined) {
+ return path;
+ }
+ if (path.startsWith(workspacePath)) {
+ return path.substring(workspacePath.length + 1);
+ } else {
+ return path;
+ }
+}
+
+function formatFileRange(
+ rangeInFile: RangeInFile,
+ workspacePath: string | undefined
+) {
+ return `${formatPathRelativeToWorkspace(
+ rangeInFile.filepath,
+ workspacePath
+ )} (lines ${rangeInFile.range.start.line + 1}-${
+ rangeInFile.range.end.line + 1
+ })`;
+ // +1 because VSCode Ranges are 0-indexed
+}
+
+//#endregion
+
+function CodeMultiselect(props: {}) {
+ // State
+ const [highlightLocked, setHighlightLocked] = useState(true);
+
+ // Redux
+ const dispatch = useDispatch();
+ const workspacePath = useSelector(
+ (state: RootStore) => state.config.workspacePath
+ );
+ const debugContext = useSelector(selectDebugContext);
+ const rangesInFiles = useSelector(selectAllRangesInFiles);
+ const rangesInFilesMask = useSelector(selectRangesMask);
+
+ useEffect(() => {
+ let eventListener = (event: any) => {
+ switch (event.data.type) {
+ case "highlightedCode":
+ if (!highlightLocked) {
+ dispatch(
+ addRangeInFile({
+ rangeInFile: event.data.rangeInFile,
+ canUpdateLast: true,
+ })
+ );
+ dispatch(updateFileSystem(event.data.filesystem));
+ }
+ break;
+ case "findSuspiciousCode":
+ for (let c of event.data.codeLocations) {
+ dispatch(addRangeInFile({ rangeInFile: c, canUpdateLast: false }));
+ }
+ dispatch(updateFileSystem(event.data.filesystem));
+ postVscMessage("listTenThings", { debugContext });
+ break;
+ }
+ };
+ window.addEventListener("message", eventListener);
+ return () => window.removeEventListener("message", eventListener);
+ }, [debugContext, highlightLocked]);
+
+ useEffect(() => {
+ hljs.highlightAll();
+ }, [rangesInFiles]);
+
+ return (
+ <MultiSelectContainer>
+ {rangesInFiles.map((range: RangeInFile, index: number) => {
+ return (
+ <MultiSelectOption
+ key={index}
+ style={{
+ border: `1px solid ${
+ rangesInFilesMask[index] ? buttonColor : "gray"
+ }`,
+ }}
+ onClick={() => {
+ dispatch(toggleSelectionAt(index));
+ }}
+ >
+ <MultiSelectHeader>
+ <p style={{ margin: "4px" }}>
+ {formatFileRange(range, workspacePath)}
+ </p>
+ <DeleteSelectedRangeButton
+ onClick={() => dispatch(deleteRangeInFileAt(index))}
+ >
+ x
+ </DeleteSelectedRangeButton>
+ </MultiSelectHeader>
+ <pre>
+ <code
+ className={"language-" + filenameToLanguage(range.filepath)}
+ >
+ {readRangeInVirtualFileSystem(range, debugContext.filesystem)}
+ </code>
+ </pre>
+ </MultiSelectOption>
+ );
+ })}
+ {rangesInFiles.length === 0 && (
+ <>
+ <p>Highlight relevant code in the editor.</p>
+ </>
+ )}
+ <ToggleHighlightButton
+ onClick={() => {
+ setHighlightLocked(!highlightLocked);
+ }}
+ >
+ {highlightLocked ? (
+ <>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20px"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth="1.5"
+ stroke="currentColor"
+ className="w-6 h-6"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
+ />
+ </svg>{" "}
+ Enable Highlight
+ </>
+ ) : (
+ <>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20px"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth="1.5"
+ stroke="currentColor"
+ className="w-6 h-6"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
+ />
+ </svg>{" "}
+ Disable Highlight
+ </>
+ )}
+ </ToggleHighlightButton>
+ </MultiSelectContainer>
+ );
+}
+
+export default CodeMultiselect;
diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx
new file mode 100644
index 00000000..11dc7a92
--- /dev/null
+++ b/extension/react-app/src/components/ContinueButton.tsx
@@ -0,0 +1,37 @@
+import styled, { keyframes } from "styled-components";
+import { Button } from ".";
+import { Play } from "@styled-icons/heroicons-outline";
+
+let StyledButton = styled(Button)`
+ margin: auto;
+ display: grid;
+ grid-template-columns: 30px 1fr;
+ align-items: center;
+ background: linear-gradient(
+ 95.23deg,
+ #be1a55 14.44%,
+ rgba(203, 27, 90, 0.4) 82.21%
+ );
+
+ &:hover {
+ transition-delay: 0.5s;
+ transition-property: background;
+ background: linear-gradient(
+ 45deg,
+ #be1a55 14.44%,
+ rgba(203, 27, 90, 0.4) 82.21%
+ );
+ }
+`;
+
+function ContinueButton(props: { onClick?: () => void }) {
+ return (
+ <StyledButton className="m-auto" onClick={props.onClick}>
+ <Play />
+ {/* <img src={"/continue_arrow.png"} width="16px"></img> */}
+ Continue
+ </StyledButton>
+ );
+}
+
+export default ContinueButton;
diff --git a/extension/react-app/src/components/DebugPanel.tsx b/extension/react-app/src/components/DebugPanel.tsx
new file mode 100644
index 00000000..ed00571b
--- /dev/null
+++ b/extension/react-app/src/components/DebugPanel.tsx
@@ -0,0 +1,121 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+import { postVscMessage } from "../vscode";
+import { useDispatch } from "react-redux";
+import {
+ setApiUrl,
+ setVscMachineId,
+ setSessionId,
+} from "../redux/slices/configSlice";
+import { setHighlightedCode } from "../redux/slices/miscSlice";
+import { updateFileSystem } from "../redux/slices/debugContexSlice";
+import { buttonColor, defaultBorderRadius, vscBackground } from ".";
+interface DebugPanelProps {
+ tabs: {
+ element: React.ReactElement;
+ title: string;
+ }[];
+}
+
+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; */
+ margin: 0;
+ height: 100%;
+ /* border: 1px solid white; */
+ border-radius: ${defaultBorderRadius};
+`;
+
+const MainDiv = styled.div`
+ height: 100%;
+ border-radius: ${defaultBorderRadius};
+ overflow: scroll;
+ scrollbar-base-color: transparent;
+ /* background: ${vscBackground}; */
+ background-color: #1e1e1ede;
+`;
+
+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%;
+`;
+
+function DebugPanel(props: DebugPanelProps) {
+ const dispatch = useDispatch();
+ useEffect(() => {
+ const eventListener = (event: any) => {
+ switch (event.data.type) {
+ case "onLoad":
+ dispatch(setApiUrl(event.data.apiUrl));
+ dispatch(setVscMachineId(event.data.vscMachineId));
+ dispatch(setSessionId(event.data.sessionId));
+ break;
+ case "highlightedCode":
+ dispatch(setHighlightedCode(event.data.rangeInFile));
+ dispatch(updateFileSystem(event.data.filesystem));
+ break;
+ }
+ };
+ window.addEventListener("message", eventListener);
+ postVscMessage("onLoad", {});
+ return () => window.removeEventListener("message", eventListener);
+ }, []);
+
+ 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>
+ )}
+ {props.tabs.map((tab, index) => {
+ return (
+ <div
+ key={index}
+ hidden={index !== currentTab}
+ className={
+ tab.title === "Chat" ? "overflow-hidden" : "overflow-scroll"
+ }
+ >
+ {tab.element}
+ </div>
+ );
+ })}
+ </TabsAndBodyDiv>
+ </MainDiv>
+ </GradientContainer>
+ );
+}
+
+export default DebugPanel;
diff --git a/extension/react-app/src/components/IterationContainer.tsx b/extension/react-app/src/components/IterationContainer.tsx
new file mode 100644
index 00000000..a0053519
--- /dev/null
+++ b/extension/react-app/src/components/IterationContainer.tsx
@@ -0,0 +1,77 @@
+import { useState } from "react";
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ MainContainerWithBorder,
+ secondaryDark,
+ vscBackground,
+} from ".";
+import { RangeInFile, FileEdit } from "../../../src/client";
+import CodeBlock from "./CodeBlock";
+import SubContainer from "./SubContainer";
+
+import { ChevronDown, ChevronRight } from "@styled-icons/heroicons-outline";
+
+export interface IterationContext {
+ codeSelections: RangeInFile[];
+ instruction: string;
+ suggestedChanges: FileEdit[];
+ status: "waiting" | "accepted" | "rejected";
+ summary?: string;
+ action: string;
+ error?: string;
+}
+
+interface IterationContainerProps {
+ iterationContext: IterationContext;
+}
+
+const IterationContainerDiv = styled.div<{ open: boolean }>`
+ background-color: ${(props) => (props.open ? vscBackground : secondaryDark)};
+ border-radius: ${defaultBorderRadius};
+ padding: ${(props) => (props.open ? "2px" : "8px")};
+`;
+
+function IterationContainer(props: IterationContainerProps) {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <MainContainerWithBorder className="m-2 overflow-hidden">
+ <IterationContainerDiv open={open}>
+ <p
+ className="m-2 cursor-pointer"
+ onClick={() => setOpen((prev) => !prev)}
+ >
+ {open ? <ChevronDown size="1.4em" /> : <ChevronRight size="1.4em" />}
+ {props.iterationContext.summary ||
+ props.iterationContext.codeSelections
+ .map((cs) => cs.filepath)
+ .join("\n")}
+ </p>
+
+ {open && (
+ <>
+ <SubContainer title="Action">
+ {props.iterationContext.action}
+ </SubContainer>
+ {props.iterationContext.error && (
+ <SubContainer title="Error">
+ <CodeBlock>{props.iterationContext.error}</CodeBlock>
+ </SubContainer>
+ )}
+ {props.iterationContext.suggestedChanges.map((sc) => {
+ return (
+ <SubContainer title="Suggested Change">
+ {sc.filepath}
+ <CodeBlock>{sc.replacement}</CodeBlock>
+ </SubContainer>
+ );
+ })}
+ </>
+ )}
+ </IterationContainerDiv>
+ </MainContainerWithBorder>
+ );
+}
+
+export default IterationContainer;
diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx
new file mode 100644
index 00000000..03649b66
--- /dev/null
+++ b/extension/react-app/src/components/StepContainer.tsx
@@ -0,0 +1,208 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import styled, { keyframes } from "styled-components";
+import {
+ appear,
+ buttonColor,
+ defaultBorderRadius,
+ MainContainerWithBorder,
+ MainTextInput,
+ secondaryDark,
+ vscBackground,
+ GradientBorder,
+} from ".";
+import { RangeInFile, FileEdit } from "../../../src/client";
+import CodeBlock from "./CodeBlock";
+import SubContainer from "./SubContainer";
+
+import {
+ ChevronDown,
+ ChevronRight,
+ Backward,
+} from "@styled-icons/heroicons-outline";
+import { HistoryNode } from "../../../schema/HistoryNode";
+import ReactMarkdown from "react-markdown";
+import ContinueButton from "./ContinueButton";
+
+interface StepContainerProps {
+ historyNode: HistoryNode;
+ onReverse: () => void;
+ inFuture: boolean;
+ onRefinement: (input: string) => void;
+ onUserInput: (input: string) => void;
+}
+
+const MainDiv = styled.div<{ stepDepth: number; inFuture: boolean }>`
+ opacity: ${(props) => (props.inFuture ? 0.3 : 1)};
+ animation: ${appear} 0.3s ease-in-out;
+ /* padding-left: ${(props) => props.stepDepth * 20}px; */
+ overflow: hidden;
+`;
+
+const StepContainerDiv = styled.div<{ open: boolean }>`
+ background-color: ${(props) => (props.open ? vscBackground : secondaryDark)};
+ border-radius: ${defaultBorderRadius};
+ padding: 8px;
+`;
+
+const HeaderDiv = styled.div`
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+`;
+
+const HeaderButton = styled.button`
+ background-color: transparent;
+ border: 1px solid white;
+ border-radius: ${defaultBorderRadius};
+ padding: 2px;
+ cursor: pointer;
+ color: white;
+
+ &:hover {
+ background-color: white;
+ color: black;
+ }
+`;
+
+const OnHoverDiv = styled.div`
+ text-align: center;
+ padding: 10px;
+ animation: ${appear} 0.3s ease-in-out;
+`;
+
+const NaturalLanguageInput = styled(MainTextInput)`
+ width: 80%;
+`;
+
+function StepContainer(props: StepContainerProps) {
+ const [open, setOpen] = useState(false);
+ const [isHovered, setIsHovered] = useState(false);
+ const naturalLanguageInputRef = useRef<HTMLTextAreaElement>(null);
+
+ useEffect(() => {
+ if (isHovered) {
+ naturalLanguageInputRef.current?.focus();
+ }
+ }, [isHovered]);
+
+ const onTextInput = useCallback(() => {
+ if (naturalLanguageInputRef.current) {
+ props.onRefinement(naturalLanguageInputRef.current.value);
+ naturalLanguageInputRef.current.value = "";
+ }
+ }, [naturalLanguageInputRef]);
+
+ return (
+ <MainDiv
+ stepDepth={(props.historyNode.depth as any) || 0}
+ inFuture={props.inFuture}
+ onMouseEnter={() => {
+ setIsHovered(true);
+ }}
+ onMouseLeave={() => {
+ setIsHovered(false);
+ }}
+ hidden={props.historyNode.step.hide as any}
+ >
+ <GradientBorder
+ className="m-2 overflow-hidden cursor-pointer"
+ onClick={() => setOpen((prev) => !prev)}
+ >
+ <StepContainerDiv open={open}>
+ <HeaderDiv>
+ <h4 className="m-2 cursor-pointer">
+ {open ? (
+ <ChevronDown size="1.4em" />
+ ) : (
+ <ChevronRight size="1.4em" />
+ )}
+ {props.historyNode.step.name as any}:
+ </h4>
+ <HeaderButton
+ onClick={(e) => {
+ e.stopPropagation();
+ props.onReverse();
+ }}
+ >
+ <Backward size="1.6em" onClick={props.onReverse}></Backward>
+ </HeaderButton>
+ </HeaderDiv>
+
+ <ReactMarkdown key={1} className="overflow-scroll">
+ {props.historyNode.step.description as any}
+ </ReactMarkdown>
+
+ {props.historyNode.step.name === "Waiting for user input" && (
+ <input
+ className="m-auto p-2 rounded-md border-1 border-solid text-white w-3/4 border-gray-200 bg-vsc-background"
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ props.onUserInput(e.currentTarget.value);
+ }
+ }}
+ type="text"
+ onSubmit={(ev) => {
+ props.onUserInput(ev.currentTarget.value);
+ }}
+ />
+ )}
+ {props.historyNode.step.name === "Waiting for user confirmation" && (
+ <>
+ <input
+ type="button"
+ value="Cancel"
+ className="m-4 p-2 rounded-md border border-solid text-white border-gray-200 bg-vsc-background cursor-pointer hover:bg-white hover:text-black"
+ ></input>
+ <input
+ className="m-4 p-2 rounded-md border border-solid text-white border-gray-200 bg-vsc-background cursor-pointer hover:bg-white hover:text-black"
+ onClick={(e) => {
+ props.onUserInput("ok");
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ type="button"
+ value="Confirm"
+ />
+ </>
+ )}
+
+ {open && (
+ <>
+ {/* {props.historyNode.observation && (
+ <SubContainer title="Error">
+ <CodeBlock>Error Here</CodeBlock>
+ </SubContainer>
+ )} */}
+ {/* {props.iterationContext.suggestedChanges.map((sc) => {
+ return (
+ <SubContainer title="Suggested Change">
+ {sc.filepath}
+ <CodeBlock>{sc.replacement}</CodeBlock>
+ </SubContainer>
+ );
+ })} */}
+ </>
+ )}
+ </StepContainerDiv>
+ </GradientBorder>
+
+ <OnHoverDiv hidden={!open}>
+ <NaturalLanguageInput
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ onTextInput();
+ }
+ }}
+ ref={naturalLanguageInputRef}
+ onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ ></NaturalLanguageInput>
+ <ContinueButton onClick={onTextInput}></ContinueButton>
+ </OnHoverDiv>
+ </MainDiv>
+ );
+}
+
+export default StepContainer;
diff --git a/extension/react-app/src/components/SubContainer.tsx b/extension/react-app/src/components/SubContainer.tsx
new file mode 100644
index 00000000..87e2094c
--- /dev/null
+++ b/extension/react-app/src/components/SubContainer.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import styled from "styled-components";
+import { defaultBorderRadius, secondaryDark, appear } from ".";
+
+const SubContainerDiv = styled.div`
+ margin: 4px;
+ padding: 8px;
+ border-radius: ${defaultBorderRadius};
+ background-color: ${secondaryDark};
+
+ animation: ${appear} 0.3s ease-in-out;
+`;
+
+function SubContainer(props: { children: React.ReactNode; title: string }) {
+ return (
+ <SubContainerDiv>
+ <b className="mb-12">{props.title}</b>
+ <br></br>
+ {props.children}
+ </SubContainerDiv>
+ );
+}
+
+export default SubContainer;
diff --git a/extension/react-app/src/components/VSCodeFileLink.tsx b/extension/react-app/src/components/VSCodeFileLink.tsx
new file mode 100644
index 00000000..6219654d
--- /dev/null
+++ b/extension/react-app/src/components/VSCodeFileLink.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { postVscMessage } from "../vscode";
+
+function VSCodeFileLink(props: { path: string; text?: string }) {
+ return (
+ <a
+ href={`file://${props.path}`}
+ onClick={() => {
+ postVscMessage("openFile", { path: props.path });
+ }}
+ >
+ {props.text || props.path}
+ </a>
+ );
+}
+
+export default VSCodeFileLink;
diff --git a/extension/react-app/src/components/index.ts b/extension/react-app/src/components/index.ts
new file mode 100644
index 00000000..e37c97f3
--- /dev/null
+++ b/extension/react-app/src/components/index.ts
@@ -0,0 +1,136 @@
+import styled, { keyframes } from "styled-components";
+
+export const defaultBorderRadius = "5px";
+export const secondaryDark = "rgb(37 37 38)";
+export const vscBackground = "rgb(30 30 30)";
+export const buttonColor = "rgb(113 28 59)";
+export const buttonColorHover = "rgb(113 28 59 0.67)";
+
+export const Button = styled.button`
+ padding: 10px 12px;
+ margin: 8px 0;
+ border-radius: ${defaultBorderRadius};
+ cursor: pointer;
+
+ border: none;
+ color: white;
+ background-color: ${buttonColor};
+
+ &:disabled {
+ color: gray;
+ }
+
+ &:hover:enabled {
+ background-color: ${buttonColorHover};
+ }
+`;
+
+export const TextArea = styled.textarea`
+ width: 100%;
+ border-radius: ${defaultBorderRadius};
+ border: none;
+ background-color: ${secondaryDark};
+ resize: vertical;
+
+ padding: 4px;
+ caret-color: white;
+ color: white;
+
+ &:focus {
+ outline: 1px solid ${buttonColor};
+ }
+`;
+
+export const Pre = styled.pre`
+ border-radius: ${defaultBorderRadius};
+ padding: 8px;
+ max-height: 150px;
+ overflow: scroll;
+ margin: 0;
+ background-color: ${secondaryDark};
+ border: none;
+
+ /* text wrapping */
+ white-space: pre-wrap; /* Since CSS 2.1 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+`;
+
+export const H3 = styled.h3`
+ background-color: ${secondaryDark};
+ border-radius: ${defaultBorderRadius};
+ padding: 4px 8px;
+ width: fit-content;
+`;
+
+export const TextInput = styled.input.attrs({ type: "text" })`
+ width: 100%;
+ padding: 12px 20px;
+ margin: 8px 0;
+ box-sizing: border-box;
+ border-radius: ${defaultBorderRadius};
+ border: 2px solid gray;
+`;
+
+const spin = keyframes`
+ from {
+ -webkit-transform: rotate(0deg);
+ }
+ to {
+ -webkit-transform: rotate(360deg);
+ }
+`;
+
+export const Loader = styled.div`
+ border: 4px solid transparent;
+ border-radius: 50%;
+ border-top: 4px solid white;
+ width: 36px;
+ height: 36px;
+ -webkit-animation: ${spin} 1s ease-in-out infinite;
+ animation: ${spin} 1s ease-in-out infinite;
+ margin: auto;
+`;
+
+export const GradientBorder = styled.div<{ borderWidth?: string }>`
+ border-radius: ${defaultBorderRadius};
+ padding: ${(props) => props.borderWidth || "1px"};
+ background: linear-gradient(
+ 101.79deg,
+ #12887a 0%,
+ #87245c 37.64%,
+ #e12637 65.98%,
+ #ffb215 110.45%
+ );
+`;
+
+export const MainContainerWithBorder = styled.div<{ borderWidth?: string }>`
+ border-radius: ${defaultBorderRadius};
+ padding: ${(props) => props.borderWidth || "1px"};
+ background-color: white;
+`;
+
+export const MainTextInput = styled.textarea`
+ padding: 8px;
+ font-size: 16px;
+ border-radius: ${defaultBorderRadius};
+ border: 1px solid #ccc;
+ margin: 8px 8px;
+ background-color: ${vscBackground};
+ color: white;
+ outline: 1px solid orange;
+ resize: none;
+`;
+
+export const appear = keyframes`
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0px);
+ }
+`;
diff --git a/extension/react-app/src/continue_arrow.png b/extension/react-app/src/continue_arrow.png
new file mode 100644
index 00000000..3b16ddf9
--- /dev/null
+++ b/extension/react-app/src/continue_arrow.png
Binary files differ
diff --git a/extension/react-app/src/highlight/dark.min.css b/extension/react-app/src/highlight/dark.min.css
new file mode 100644
index 00000000..9268d7c9
--- /dev/null
+++ b/extension/react-app/src/highlight/dark.min.css
@@ -0,0 +1,53 @@
+pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em;
+}
+code.hljs {
+ padding: 3px 5px;
+}
+.hljs {
+ color: #ddd;
+ background: #252526;
+}
+.hljs-keyword,
+.hljs-link,
+.hljs-literal,
+.hljs-section,
+.hljs-selector-tag {
+ color: #fff;
+}
+.hljs-addition,
+.hljs-attribute,
+.hljs-built_in,
+.hljs-bullet,
+.hljs-name,
+.hljs-string,
+.hljs-symbol,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-title,
+.hljs-type,
+.hljs-variable {
+ color: #d88;
+}
+.hljs-comment,
+.hljs-deletion,
+.hljs-meta,
+.hljs-quote {
+ color: #979797;
+}
+.hljs-doctag,
+.hljs-keyword,
+.hljs-literal,
+.hljs-name,
+.hljs-section,
+.hljs-selector-tag,
+.hljs-strong,
+.hljs-title,
+.hljs-type {
+ font-weight: 700;
+}
+.hljs-emphasis {
+ font-style: italic;
+}
diff --git a/extension/react-app/src/hooks/useArrayState.ts b/extension/react-app/src/hooks/useArrayState.ts
new file mode 100644
index 00000000..d379e720
--- /dev/null
+++ b/extension/react-app/src/hooks/useArrayState.ts
@@ -0,0 +1,29 @@
+import { useState } from "react";
+
+function useArrayState<T>(initialValue: T[]) {
+ const [value, setValue] = useState(initialValue);
+
+ function add(item: any) {
+ setValue((prev) => [...prev, item]);
+ }
+
+ function remove(index: number) {
+ setValue((prev) => prev.filter((_, i) => i !== index));
+ }
+
+ function edit(editFn: (prev: T[]) => T[]) {
+ setValue((prev) => editFn(prev));
+ }
+
+ function replace(atIndex: number, withItem: T) {
+ setValue((prev) => {
+ let updated = [...prev];
+ updated[atIndex] = withItem;
+ return updated;
+ });
+ }
+
+ return { value, add, remove, edit, replace };
+}
+
+export default useArrayState;
diff --git a/extension/react-app/src/hooks/useWebsocket.ts b/extension/react-app/src/hooks/useWebsocket.ts
new file mode 100644
index 00000000..147172bd
--- /dev/null
+++ b/extension/react-app/src/hooks/useWebsocket.ts
@@ -0,0 +1,67 @@
+import React, { useEffect, useState } from "react";
+import { RootStore } from "../redux/store";
+import { useSelector } from "react-redux";
+
+function useContinueWebsocket(
+ serverUrl: string,
+ onMessage: (message: { data: any }) => void
+) {
+ const sessionId = useSelector((state: RootStore) => state.config.sessionId);
+ const [websocket, setWebsocket] = useState<WebSocket | undefined>(undefined);
+
+ async function connect() {
+ while (!sessionId) {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ }
+
+ console.log("Creating websocket", sessionId);
+
+ const wsUrl =
+ serverUrl.replace("http", "ws") +
+ "/notebook/ws?session_id=" +
+ encodeURIComponent(sessionId);
+
+ const ws = new WebSocket(wsUrl);
+ setWebsocket(ws);
+
+ // Set up callbacks
+ ws.onopen = () => {
+ console.log("Websocket opened");
+ ws.send(JSON.stringify({ sessionId }));
+ };
+
+ ws.onmessage = (msg) => {
+ onMessage(msg);
+ console.log("Got message", msg);
+ };
+
+ ws.onclose = (msg) => {
+ console.log("Websocket closed");
+ setWebsocket(undefined);
+ };
+
+ return ws;
+ }
+
+ async function getConnection() {
+ if (!websocket) {
+ return await connect();
+ }
+ return websocket;
+ }
+
+ async function send(message: object) {
+ let ws = await getConnection();
+ ws.send(JSON.stringify(message));
+ }
+
+ useEffect(() => {
+ if (!sessionId) {
+ return;
+ }
+ connect();
+ }, [sessionId]);
+
+ return { send };
+}
+export default useContinueWebsocket;
diff --git a/extension/react-app/src/index.css b/extension/react-app/src/index.css
new file mode 100644
index 00000000..dd38eec3
--- /dev/null
+++ b/extension/react-app/src/index.css
@@ -0,0 +1,34 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --secondary-dark: 37 37 38;
+ --vsc-background: 30 30 30;
+ --button-color: rgb(113, 28, 59);
+ --button-color-hover: rgba(113, 28, 59, 0.667);
+ --def-border-radius: 5px;
+}
+
+@font-face {
+ font-family: "Mona Sans";
+ src: url("assets/Mona-Sans.woff2") format("woff2 supports variations"),
+ url("assets/Mona-Sans.woff2") format("woff2-variations");
+ font-weight: 200 900;
+ font-stretch: 75% 85%;
+}
+
+html,
+body,
+#root {
+ height: calc(100% - 7px);
+}
+
+body {
+ background-color: var(--vsc-background);
+ padding: 0;
+ color: white;
+ font-family: "Mona Sans", "Arial", sans-serif;
+ padding: 0px;
+ margin: 0px;
+}
diff --git a/extension/react-app/src/main.tsx b/extension/react-app/src/main.tsx
new file mode 100644
index 00000000..791f139e
--- /dev/null
+++ b/extension/react-app/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>,
+)
diff --git a/extension/react-app/src/redux/hooks.ts b/extension/react-app/src/redux/hooks.ts
new file mode 100644
index 00000000..a6aef869
--- /dev/null
+++ b/extension/react-app/src/redux/hooks.ts
@@ -0,0 +1,21 @@
+import { useCallback } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { RootStore } from "./store";
+import { selectDebugContextValue } from "./selectors/debugContextSelectors";
+import { updateValue } from "./slices/debugContexSlice";
+import { SerializedDebugContext } from "../../../src/client";
+
+export function useDebugContextValue(
+ key: keyof SerializedDebugContext,
+ defaultValue: any
+): [any, (value: any) => void] {
+ const dispatch = useDispatch();
+ const state =
+ useSelector((state: RootStore) => selectDebugContextValue(state, key)) ||
+ defaultValue;
+ const boundAction = useCallback(
+ (value: any) => dispatch(updateValue({ key, value })),
+ [dispatch, key]
+ );
+ return [state, boundAction];
+}
diff --git a/extension/react-app/src/redux/selectors/chatSelectors.ts b/extension/react-app/src/redux/selectors/chatSelectors.ts
new file mode 100644
index 00000000..51e8a636
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/chatSelectors.ts
@@ -0,0 +1,11 @@
+import { RootStore } from "../store";
+
+const selectChatMessages = (state: RootStore) => {
+ return state.chat.messages;
+};
+
+const selectIsStreaming = (state: RootStore) => {
+ return state.chat.isStreaming;
+};
+
+export { selectChatMessages, selectIsStreaming };
diff --git a/extension/react-app/src/redux/selectors/debugContextSelectors.ts b/extension/react-app/src/redux/selectors/debugContextSelectors.ts
new file mode 100644
index 00000000..89201bb7
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/debugContextSelectors.ts
@@ -0,0 +1,29 @@
+import { RootStore } from "../store";
+
+const selectDebugContext = (state: RootStore) => {
+ return {
+ ...state.debugState.debugContext,
+ rangesInFiles: state.debugState.debugContext.rangesInFiles.filter(
+ (_, index) => state.debugState.rangesMask[index]
+ ),
+ };
+};
+
+const selectAllRangesInFiles = (state: RootStore) => {
+ return state.debugState.debugContext.rangesInFiles;
+};
+
+const selectRangesMask = (state: RootStore) => {
+ return state.debugState.rangesMask;
+};
+
+const selectDebugContextValue = (state: RootStore, key: string) => {
+ return (state.debugState.debugContext as any)[key];
+};
+
+export {
+ selectDebugContext,
+ selectDebugContextValue,
+ selectAllRangesInFiles,
+ selectRangesMask,
+};
diff --git a/extension/react-app/src/redux/selectors/miscSelectors.ts b/extension/react-app/src/redux/selectors/miscSelectors.ts
new file mode 100644
index 00000000..7dbaed09
--- /dev/null
+++ b/extension/react-app/src/redux/selectors/miscSelectors.ts
@@ -0,0 +1,5 @@
+import { RootStore } from "../store";
+
+const selectHighlightedCode = (state: RootStore) => state.misc.highlightedCode;
+
+export { selectHighlightedCode };
diff --git a/extension/react-app/src/redux/slices/chatSlice.ts b/extension/react-app/src/redux/slices/chatSlice.ts
new file mode 100644
index 00000000..848cccd4
--- /dev/null
+++ b/extension/react-app/src/redux/slices/chatSlice.ts
@@ -0,0 +1,93 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { ChatMessage, RootStore } from "../store";
+
+export const chatSlice = createSlice({
+ name: "chat",
+ initialState: {
+ messages: [],
+ isStreaming: false,
+ } as RootStore["chat"],
+ reducers: {
+ addMessage: (
+ state,
+ action: {
+ type: string;
+ payload: ChatMessage;
+ }
+ ) => {
+ return {
+ ...state,
+ messages: [...state.messages, action.payload],
+ };
+ },
+ setIsStreaming: (state, action) => {
+ return {
+ ...state,
+ isStreaming: action.payload,
+ };
+ },
+ streamUpdate: (state, action) => {
+ if (!state.isStreaming) {
+ return {
+ ...state,
+ messages: [
+ ...state.messages,
+ {
+ role: "assistant",
+ content: action.payload,
+ },
+ ],
+ isStreaming: true,
+ };
+ } else {
+ let lastMessage = state.messages[state.messages.length - 1];
+ if (lastMessage.role !== "assistant") {
+ return {
+ ...state,
+ messages: [
+ ...state.messages,
+ {
+ role: "assistant",
+ content: action.payload,
+ },
+ ],
+ isStreaming: true,
+ };
+ }
+ return {
+ ...state,
+ messages: [
+ ...state.messages.slice(0, state.messages.length - 1),
+ {
+ ...lastMessage,
+ content: lastMessage.content + action.payload,
+ },
+ ],
+ isStreaming: true,
+ };
+ }
+ },
+ closeStream: (state) => {
+ return {
+ ...state,
+ isStreaming: false,
+ };
+ },
+ clearChat: (state) => {
+ return {
+ ...state,
+ messages: [],
+ isStreaming: false,
+ };
+ },
+ },
+});
+
+export const {
+ addMessage,
+ streamUpdate,
+ closeStream,
+ clearChat,
+ setIsStreaming,
+} = chatSlice.actions;
+export default chatSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/configSlice.ts b/extension/react-app/src/redux/slices/configSlice.ts
new file mode 100644
index 00000000..a6a641e6
--- /dev/null
+++ b/extension/react-app/src/redux/slices/configSlice.ts
@@ -0,0 +1,45 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { RootStore } from "../store";
+
+export const configSlice = createSlice({
+ name: "config",
+ initialState: {
+ apiUrl: "http://localhost:8000",
+ } as RootStore["config"],
+ reducers: {
+ setWorkspacePath: (
+ state: RootStore["config"],
+ action: { type: string; payload: string }
+ ) => {
+ return {
+ ...state,
+ workspacePath: action.payload,
+ };
+ },
+ setApiUrl: (
+ state: RootStore["config"],
+ action: { type: string; payload: string }
+ ) => ({
+ ...state,
+ apiUrl: action.payload,
+ }),
+ setVscMachineId: (
+ state: RootStore["config"],
+ action: { type: string; payload: string }
+ ) => ({
+ ...state,
+ vscMachineId: action.payload,
+ }),
+ setSessionId: (
+ state: RootStore["config"],
+ action: { type: string; payload: string }
+ ) => ({
+ ...state,
+ sessionId: action.payload,
+ }),
+ },
+});
+
+export const { setVscMachineId, setApiUrl, setWorkspacePath, setSessionId } =
+ configSlice.actions;
+export default configSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/debugContexSlice.ts b/extension/react-app/src/redux/slices/debugContexSlice.ts
new file mode 100644
index 00000000..647440d5
--- /dev/null
+++ b/extension/react-app/src/redux/slices/debugContexSlice.ts
@@ -0,0 +1,149 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { RangeInFile, SerializedDebugContext } from "../../../../src/client";
+import { RootStore } from "../store";
+
+export const debugStateSlice = createSlice({
+ name: "debugState",
+ initialState: {
+ debugContext: {
+ rangesInFiles: [],
+ filesystem: {},
+ traceback: undefined,
+ description: undefined,
+ },
+ rangesMask: [],
+ } as RootStore["debugState"],
+ reducers: {
+ updateValue: (
+ state: RootStore["debugState"],
+ action: {
+ type: string;
+ payload: { key: keyof SerializedDebugContext; value: any };
+ }
+ ) => {
+ return {
+ ...state,
+ debugContext: {
+ ...state.debugContext,
+ [action.payload.key]: action.payload.value,
+ },
+ };
+ },
+ addRangeInFile: (
+ state: RootStore["debugState"],
+ action: {
+ type: string;
+ payload: {
+ rangeInFile: RangeInFile;
+ canUpdateLast: boolean;
+ };
+ }
+ ) => {
+ let rangesInFiles = state.debugContext.rangesInFiles;
+ // If identical to existing range, don't add. Ideally you check for overlap of ranges.
+ for (let range of rangesInFiles) {
+ if (
+ range.filepath === action.payload.rangeInFile.filepath &&
+ range.range.start.line ===
+ action.payload.rangeInFile.range.start.line &&
+ range.range.end.line === action.payload.rangeInFile.range.end.line
+ ) {
+ return state;
+ }
+ }
+
+ if (
+ action.payload.canUpdateLast &&
+ rangesInFiles.length > 0 &&
+ rangesInFiles[rangesInFiles.length - 1].filepath ===
+ action.payload.rangeInFile.filepath
+ ) {
+ return {
+ ...state,
+ debugContext: {
+ ...state.debugContext,
+ rangesInFiles: [
+ ...rangesInFiles.slice(0, rangesInFiles.length - 1),
+ action.payload.rangeInFile,
+ ],
+ },
+ };
+ } else {
+ return {
+ ...state,
+ debugContext: {
+ ...state.debugContext,
+ rangesInFiles: [
+ ...state.debugContext.rangesInFiles,
+ action.payload.rangeInFile,
+ ],
+ },
+ rangesMask: [...state.rangesMask, true],
+ };
+ }
+ },
+ deleteRangeInFileAt: (
+ state: RootStore["debugState"],
+ action: {
+ type: string;
+ payload: number;
+ }
+ ) => {
+ return {
+ ...state,
+ debugContext: {
+ ...state.debugContext,
+ rangesInFiles: state.debugContext.rangesInFiles.filter(
+ (_, index) => index !== action.payload
+ ),
+ },
+ rangesMask: state.rangesMask.filter(
+ (_, index) => index !== action.payload
+ ),
+ };
+ },
+ toggleSelectionAt: (
+ state: RootStore["debugState"],
+ action: {
+ type: string;
+ payload: number;
+ }
+ ) => {
+ return {
+ ...state,
+ rangesMask: state.rangesMask.map((_, index) =>
+ index === action.payload
+ ? !state.rangesMask[index]
+ : state.rangesMask[index]
+ ),
+ };
+ },
+ updateFileSystem: (
+ state: RootStore["debugState"],
+ action: {
+ type: string;
+ payload: { [filepath: string]: string };
+ }
+ ) => {
+ return {
+ ...state,
+ debugContext: {
+ ...state.debugContext,
+ filesystem: {
+ ...state.debugContext.filesystem,
+ ...action.payload,
+ },
+ },
+ };
+ },
+ },
+});
+
+export const {
+ updateValue,
+ updateFileSystem,
+ addRangeInFile,
+ deleteRangeInFileAt,
+ toggleSelectionAt,
+} = debugStateSlice.actions;
+export default debugStateSlice.reducer;
diff --git a/extension/react-app/src/redux/slices/miscSlice.ts b/extension/react-app/src/redux/slices/miscSlice.ts
new file mode 100644
index 00000000..c59cc4eb
--- /dev/null
+++ b/extension/react-app/src/redux/slices/miscSlice.ts
@@ -0,0 +1,16 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+export const miscSlice = createSlice({
+ name: "misc",
+ initialState: {
+ highlightedCode: "",
+ },
+ reducers: {
+ setHighlightedCode: (state, action) => {
+ state.highlightedCode = action.payload;
+ },
+ },
+});
+
+export const { setHighlightedCode } = miscSlice.actions;
+export default miscSlice.reducer;
diff --git a/extension/react-app/src/redux/store.ts b/extension/react-app/src/redux/store.ts
new file mode 100644
index 00000000..f9eb0517
--- /dev/null
+++ b/extension/react-app/src/redux/store.ts
@@ -0,0 +1,43 @@
+import { configureStore } from "@reduxjs/toolkit";
+import debugStateReducer from "./slices/debugContexSlice";
+import chatReducer from "./slices/chatSlice";
+import configReducer from "./slices/configSlice";
+import miscReducer from "./slices/miscSlice";
+import { RangeInFile, SerializedDebugContext } from "../../../src/client";
+
+export interface ChatMessage {
+ role: "system" | "user" | "assistant";
+ content: string;
+}
+
+export interface RootStore {
+ debugState: {
+ debugContext: SerializedDebugContext;
+ rangesMask: boolean[];
+ };
+ config: {
+ workspacePath: string | undefined;
+ apiUrl: string;
+ vscMachineId: string | undefined;
+ sessionId: string | undefined;
+ sessionStarted: number | undefined;
+ };
+ chat: {
+ messages: ChatMessage[];
+ isStreaming: boolean;
+ };
+ misc: {
+ highlightedCode: RangeInFile | undefined;
+ };
+}
+
+const store = configureStore({
+ reducer: {
+ debugState: debugStateReducer,
+ chat: chatReducer,
+ config: configReducer,
+ misc: miscReducer,
+ },
+});
+
+export default store;
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;
diff --git a/extension/react-app/src/util/api.ts b/extension/react-app/src/util/api.ts
new file mode 100644
index 00000000..bdec1d20
--- /dev/null
+++ b/extension/react-app/src/util/api.ts
@@ -0,0 +1,43 @@
+import {
+ Configuration,
+ DebugApi,
+ UnittestApi,
+ ChatApi,
+} from "../../../src/client";
+import { useSelector } from "react-redux";
+import { useEffect, useState } from "react";
+import { RootStore } from "../redux/store";
+
+export function useApi() {
+ const apiUrl = useSelector((state: RootStore) => state.config.apiUrl);
+ const vscMachineId = useSelector(
+ (state: RootStore) => state.config.vscMachineId
+ );
+ const [debugApi, setDebugApi] = useState<DebugApi>();
+ const [unittestApi, setUnittestApi] = useState<UnittestApi>();
+ const [chatApi, setChatApi] = useState<ChatApi>();
+
+ useEffect(() => {
+ if (apiUrl && vscMachineId) {
+ let config = new Configuration({
+ basePath: apiUrl,
+ fetchApi: fetch,
+ middleware: [
+ {
+ pre: async (context) => {
+ context.init.headers = {
+ ...context.init.headers,
+ "x-vsc-machine-id": vscMachineId,
+ };
+ },
+ },
+ ],
+ });
+ setDebugApi(new DebugApi(config));
+ setUnittestApi(new UnittestApi(config));
+ setChatApi(new ChatApi(config));
+ }
+ }, [apiUrl, vscMachineId]);
+
+ return { debugApi, unittestApi, chatApi };
+}
diff --git a/extension/react-app/src/util/editCache.ts b/extension/react-app/src/util/editCache.ts
new file mode 100644
index 00000000..b8071127
--- /dev/null
+++ b/extension/react-app/src/util/editCache.ts
@@ -0,0 +1,89 @@
+import { useApi } from "../util/api";
+import { FileEdit, SerializedDebugContext } from "../../../src/client";
+import { useCallback, useEffect, useState } from "react";
+
+export function useEditCache() {
+ const { debugApi } = useApi();
+
+ const fetchNewEdit = useCallback(
+ async (debugContext: SerializedDebugContext) => {
+ return (
+ await debugApi?.editEndpointDebugEditPost({
+ serializedDebugContext: debugContext,
+ })
+ )?.completion;
+ },
+ [debugApi]
+ );
+
+ const [editCache, setEditCache] = useState(new EditCache(fetchNewEdit));
+
+ useEffect(() => {
+ setEditCache(new EditCache(fetchNewEdit));
+ }, [fetchNewEdit]);
+
+ return editCache;
+}
+
+/**
+ * Stores preloaded edits, invalidating based off of debug context changes
+ */
+class EditCache {
+ private _lastDebugContext: SerializedDebugContext | undefined;
+ private _cachedEdits: FileEdit[] | undefined;
+ private _fetchNewEdit: (
+ debugContext: SerializedDebugContext
+ ) => Promise<FileEdit[] | undefined>;
+ private _debounceTimer: NodeJS.Timeout | undefined;
+
+ private _debugContextChanged(debugContext: SerializedDebugContext): boolean {
+ if (!this._lastDebugContext) {
+ return true;
+ }
+
+ return (
+ JSON.stringify(this._lastDebugContext) !== JSON.stringify(debugContext)
+ );
+ }
+
+ private _debugContextComplete(debugContext: SerializedDebugContext): boolean {
+ return debugContext.rangesInFiles.length > 0;
+ }
+
+ public async preloadEdit(debugContext: SerializedDebugContext) {
+ if (this._debounceTimer) {
+ clearTimeout(this._debounceTimer);
+ }
+ if (
+ this._debugContextComplete(debugContext) &&
+ this._debugContextChanged(debugContext)
+ ) {
+ this._debounceTimer = setTimeout(async () => {
+ console.log("Preloading edits");
+ this._cachedEdits = await this._fetchNewEdit(debugContext);
+ this._lastDebugContext = debugContext;
+ }, 200);
+ }
+ }
+
+ public async getEdit(
+ debugContext: SerializedDebugContext
+ ): Promise<FileEdit[]> {
+ if (this._debugContextChanged(debugContext)) {
+ console.log("Cache miss");
+ this._cachedEdits = await this._fetchNewEdit(debugContext);
+ } else {
+ console.log("Cache hit");
+ }
+
+ return this._cachedEdits!;
+ }
+
+ constructor(
+ fetchNewEdit: (
+ debugContext: SerializedDebugContext
+ ) => Promise<FileEdit[] | undefined>
+ ) {
+ this._fetchNewEdit = fetchNewEdit;
+ }
+}
diff --git a/extension/react-app/src/util/index.ts b/extension/react-app/src/util/index.ts
new file mode 100644
index 00000000..458f9d95
--- /dev/null
+++ b/extension/react-app/src/util/index.ts
@@ -0,0 +1,27 @@
+import { RangeInFile } from "../../../src/client";
+
+export function readRangeInVirtualFileSystem(
+ rangeInFile: RangeInFile,
+ filesystem: { [filepath: string]: string }
+): string | undefined {
+ const range = rangeInFile.range;
+
+ let data = filesystem[rangeInFile.filepath];
+ if (data === undefined) {
+ console.log("File not found");
+ return undefined;
+ } else {
+ let lines = data.toString().split("\n");
+ if (range.start.line === range.end.line) {
+ return lines[rangeInFile.range.start.line].slice(
+ rangeInFile.range.start.character,
+ rangeInFile.range.end.character
+ );
+ } else {
+ let firstLine = lines[range.start.line].slice(range.start.character);
+ let lastLine = lines[range.end.line].slice(0, range.end.character);
+ let middleLines = lines.slice(range.start.line + 1, range.end.line);
+ return [firstLine, ...middleLines, lastLine].join("\n");
+ }
+ }
+}
diff --git a/extension/react-app/src/vite-env.d.ts b/extension/react-app/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/extension/react-app/src/vite-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/extension/react-app/src/vscode/index.ts b/extension/react-app/src/vscode/index.ts
new file mode 100644
index 00000000..7e373cd9
--- /dev/null
+++ b/extension/react-app/src/vscode/index.ts
@@ -0,0 +1,47 @@
+import { useEffect, useState } from "react";
+import "vscode-webview";
+
+declare const vscode: any;
+
+export function postVscMessage(type: string, data: any) {
+ if (typeof vscode === "undefined") {
+ return;
+ }
+ vscode.postMessage({
+ type,
+ ...data,
+ });
+}
+
+export async function vscRequest(type: string, data: any): Promise<any> {
+ return new Promise((resolve) => {
+ const handler = (event: any) => {
+ if (event.data.type === type) {
+ window.removeEventListener("message", handler);
+ resolve(event.data);
+ }
+ };
+ window.addEventListener("message", handler);
+ postVscMessage(type, data);
+ });
+}
+
+export function useVscMessageValue(
+ messageType: string | string[],
+ initialValue?: any
+) {
+ const [value, setValue] = useState<any>(initialValue);
+ window.addEventListener("message", (event) => {
+ if (event.data.type === messageType) {
+ setValue(event.data.value);
+ }
+ });
+ return [value, setValue];
+}
+
+export async function withProgress(title: string, fn: () => Promise<any>) {
+ postVscMessage("withProgress", { title, done: false });
+ return fn().finally(() => {
+ postVscMessage("withProgress", { title, done: true });
+ });
+}