diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index a2485ae..dde30a1 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -222,7 +222,7 @@ io.on("connection", async (socket) => { } }) - socket.on("createTerminal", ({ id }: { id: string }) => { + socket.on("createTerminal", (id: string, callback) => { console.log("creating terminal", id) if (terminals[id]) { console.log("Terminal already exists.") @@ -243,6 +243,7 @@ io.on("connection", async (socket) => { console.log("ondata") socket.emit("terminalResponse", { // data: Buffer.from(data, "utf-8").toString("base64"), + id, data, }) }) @@ -256,6 +257,8 @@ io.on("connection", async (socket) => { onData, onExit, } + + callback(true) }) socket.on("terminalData", (id: string, data: string) => { @@ -271,6 +274,19 @@ io.on("connection", async (socket) => { } }) + socket.on("closeTerminal", (id: string, callback) => { + if (!terminals[id]) { + console.log("tried to close, but term does not exist. terminals", terminals) + return + } + + terminals[id].onData.dispose() + terminals[id].onExit.dispose() + delete terminals[id] + + callback(true) + }) + socket.on( "generateCode", async ( @@ -311,10 +327,9 @@ io.on("connection", async (socket) => { if (data.isOwner) { Object.entries(terminals).forEach((t) => { const { terminal, onData, onExit } = t[1] - if (os.platform() !== "win32") terminal.kill() - onData.dispose() - onExit.dispose() - delete terminals[t[0]] + onData.dispose() + onExit.dispose() + delete terminals[t[0]] }) // console.log("The owner disconnected.") diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index b8c30a7..8234f37 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -6,6 +6,7 @@ import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"; import { io } from "socket.io-client"; import { toast } from "sonner"; import { useClerk } from "@clerk/nextjs"; +import { createId } from "@paralleldrive/cuid2"; import * as Y from "yjs"; import LiveblocksProvider from "@liveblocks/yjs"; @@ -52,9 +53,7 @@ export default function CodeEditor({ const [tabs, setTabs] = useState([]); const [editorLanguage, setEditorLanguage] = useState("plaintext"); const [activeFileId, setActiveFileId] = useState(""); - const [activeFileContent, setActiveFileContent] = useState( - null - ); + const [activeFileContent, setActiveFileContent] = useState(""); const [cursorLine, setCursorLine] = useState(0); const [generate, setGenerate] = useState<{ show: boolean; @@ -74,12 +73,16 @@ export default function CodeEditor({ terminal: Terminal | null; }[] >([]); + const [activeTerminalId, setActiveTerminalId] = useState(""); + const [creatingTerminal, setCreatingTerminal] = useState(false); const [provider, setProvider] = useState(); const [ai, setAi] = useState(false); const isOwner = sandboxData.userId === userData.id; const clerk = useClerk(); const room = useRoom(); + const activeTerminal = terminals.find((t) => t.id === activeTerminalId); + console.log("activeTerminal", activeTerminal ? activeTerminal.id : "none"); // const editorRef = useRef(null) const [editorRef, setEditorRef] = @@ -354,9 +357,8 @@ export default function CodeEditor({ useEffect(() => { const onConnect = () => { console.log("connected"); - setTimeout(() => { - socket.emit("createTerminal", { id: "testId" }); - }, 1000); + + createTerminal(); }; const onDisconnect = () => {}; @@ -395,9 +397,17 @@ export default function CodeEditor({ // Helper functions: const createTerminal = () => { - const id = "testId"; - - socket.emit("createTerminal", { id }); + setCreatingTerminal(true); + const id = createId(); + setActiveTerminalId(id); + setTimeout(() => { + socket.emit("createTerminal", id, (res: boolean) => { + if (res) { + setTerminals((prev) => [...prev, { id, terminal: null }]); + } + }); + }, 1000); + setCreatingTerminal(false); }; const selectFile = (tab: TTab) => { @@ -446,6 +456,36 @@ export default function CodeEditor({ } }; + const closeTerminal = (term: { id: string; terminal: Terminal | null }) => { + const numTerminals = terminals.length; + const index = terminals.findIndex((t) => t.id === term.id); + if (index === -1) return; + + socket.emit("closeTerminal", term.id, (res: boolean) => { + if (res) { + const nextId = + activeTerminalId === term.id + ? numTerminals === 1 + ? null + : index < numTerminals - 1 + ? terminals[index + 1].id + : terminals[index - 1].id + : activeTerminalId; + + setTerminals((prev) => prev.filter((t) => t.id !== term.id)); + + if (!nextId) { + setActiveTerminalId(""); + } else { + const nextTerminal = terminals.find((t) => t.id === nextId); + if (nextTerminal) { + setActiveTerminalId(nextTerminal.id); + } + } + } + }); + }; + const handleRename = ( id: string, newName: string, @@ -624,7 +664,7 @@ export default function CodeEditor({ fontFamily: "var(--font-geist-mono)", }} theme="vs-dark" - value={activeFileContent ?? ""} + value={activeFileContent} /> ) : ( @@ -673,29 +713,56 @@ export default function CodeEditor({ {isOwner ? ( <>
- - - Shell - + {terminals.map((term) => ( + setActiveTerminalId(term.id)} + onClose={() => closeTerminal(term)} + selected={activeTerminalId === term.id} + > + + Shell + + ))}
- {/* {socket ? : null} */} + {socket && activeTerminal ? ( + { + setTerminals((prev) => + prev.map((term) => + term.id === activeTerminal.id + ? { ...term, terminal: t } + : term + ) + ); + }} + /> + ) : null}
) : ( diff --git a/frontend/components/editor/terminal/index.tsx b/frontend/components/editor/terminal/index.tsx index 6bf157d..73fb3d9 100644 --- a/frontend/components/editor/terminal/index.tsx +++ b/frontend/components/editor/terminal/index.tsx @@ -10,10 +10,12 @@ import { Loader2 } from "lucide-react"; export default function EditorTerminal({ socket, + id, term, setTerm, }: { socket: Socket; + id: string; term: Terminal | null; setTerm: (term: Terminal) => void; }) { @@ -41,14 +43,7 @@ export default function EditorTerminal({ useEffect(() => { if (!term) return; - // const onTerminalResponse = (response: { data: string }) => { - // const res = response.data; - // term.write(res); - // }; - if (terminalRef.current) { - // socket.on("terminalResponse", onTerminalResponse); - const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(terminalRef.current); @@ -57,7 +52,7 @@ export default function EditorTerminal({ } const disposable = term.onData((data) => { console.log("sending data", data); - socket.emit("terminalData", "testId", data); + socket.emit("terminalData", id, data); }); // socket.emit("terminalData", "\n"); @@ -68,13 +63,15 @@ export default function EditorTerminal({ }, [term, terminalRef.current]); return ( -
- {term === null ? ( -
- - Connecting to terminal... -
- ) : null} -
+ <> +
+ {term === null ? ( +
+ + Connecting to terminal... +
+ ) : null} +
+ ); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0dd2316..8f77411 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@liveblocks/react": "^1.12.0", "@liveblocks/yjs": "^1.12.0", "@monaco-editor/react": "^4.6.0", + "@paralleldrive/cuid2": "^2.2.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", @@ -609,6 +610,17 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -641,6 +653,14 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@peculiar/asn1-schema": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index e18df8c..a6d0cd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@liveblocks/react": "^1.12.0", "@liveblocks/yjs": "^1.12.0", "@monaco-editor/react": "^4.6.0", + "@paralleldrive/cuid2": "^2.2.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5",