From 7cd8b2a928e8dd7d93e60bcdd7b9c60066656103 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:00:33 +0000 Subject: [PATCH 1/9] Initial plan From f0b20049104887bb25b5a987b99706989bc80495 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:09:14 +0000 Subject: [PATCH 2/9] Add JavaScript runtime implementation Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/editor.tsx | 6 +- app/terminal/javascript/page.tsx | 21 +++ app/terminal/javascript/runtime.tsx | 240 ++++++++++++++++++++++++++++ app/terminal/runtime.tsx | 18 ++- app/terminal/tests.ts | 23 ++- public/_headers | 3 + public/javascript.worker.js | 149 +++++++++++++++++ 7 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 app/terminal/javascript/page.tsx create mode 100644 app/terminal/javascript/runtime.tsx create mode 100644 public/javascript.worker.js diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 0b8f932..566a229 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -12,6 +12,7 @@ const AceEditor = dynamic( await import("ace-builds/src-min-noconflict/ext-searchbox"); await import("ace-builds/src-min-noconflict/mode-python"); await import("ace-builds/src-min-noconflict/mode-c_cpp"); + await import("ace-builds/src-min-noconflict/mode-javascript"); await import("ace-builds/src-min-noconflict/mode-json"); await import("ace-builds/src-min-noconflict/mode-csv"); await import("ace-builds/src-min-noconflict/mode-text"); @@ -28,7 +29,7 @@ import { langConstants } from "./runtime"; // snippetを有効化するにはsnippetもimportする必要がある: import "ace-builds/src-min-noconflict/snippets/python"; // mode-xxxx.js のファイル名と、AceEditorの mode プロパティの値が対応する -export type AceLang = "python" | "c_cpp" | "json" | "csv" | "text"; +export type AceLang = "python" | "c_cpp" | "javascript" | "json" | "csv" | "text"; export function getAceLang(lang: string | undefined): AceLang { // Markdownで指定される可能性のある言語名からAceLangを取得 switch (lang) { @@ -38,6 +39,9 @@ export function getAceLang(lang: string | undefined): AceLang { case "cpp": case "c++": return "c_cpp"; + case "javascript": + case "js": + return "javascript"; case "json": return "json"; case "csv": diff --git a/app/terminal/javascript/page.tsx b/app/terminal/javascript/page.tsx new file mode 100644 index 0000000..7f9861c --- /dev/null +++ b/app/terminal/javascript/page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { EditorComponent } from "../editor"; +import { ReplTerminal } from "../repl"; + +export default function JavaScriptPage() { + return ( +
+ console.log('hello, world!')\nhello, world!"} + /> + +
+ ); +} diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx new file mode 100644 index 0000000..55967c5 --- /dev/null +++ b/app/terminal/javascript/runtime.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { + useState, + useRef, + useCallback, + ReactNode, + createContext, + useContext, + useEffect, +} from "react"; +import { SyntaxStatus, ReplOutput, ReplCommand } from "../repl"; +import { Mutex, MutexInterface } from "async-mutex"; +import { RuntimeContext } from "../runtime"; + +const JavaScriptContext = createContext(null!); + +export function useJavaScript(): RuntimeContext { + const context = useContext(JavaScriptContext); + if (!context) { + throw new Error("useJavaScript must be used within a JavaScriptProvider"); + } + return context; +} + +type MessageToWorker = + | { + type: "init"; + } + | { + type: "runJavaScript"; + payload: { code: string }; + } + | { + type: "checkSyntax"; + payload: { code: string }; + } + | { + type: "restoreState"; + }; + +type MessageFromWorker = + | { id: number; payload: unknown } + | { id: number; error: string }; + +type InitPayloadFromWorker = { success: boolean }; +type RunPayloadFromWorker = { + output: ReplOutput[]; + updatedFiles: [string, string][]; +}; +type StatusPayloadFromWorker = { status: SyntaxStatus }; + +export function JavaScriptProvider({ children }: { children: ReactNode }) { + const workerRef = useRef(null); + const [ready, setReady] = useState(false); + const mutex = useRef(new Mutex()); + const messageCallbacks = useRef< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Map void, (error: string) => void]> + >(new Map()); + const nextMessageId = useRef(0); + const isInterrupted = useRef(false); + + function postMessage({ type, payload }: MessageToWorker) { + const id = nextMessageId.current++; + return new Promise((resolve, reject) => { + messageCallbacks.current.set(id, [resolve, reject]); + workerRef.current?.postMessage({ id, type, payload }); + }); + } + + const initializeWorker = useCallback(() => { + const worker = new Worker("/javascript.worker.js"); + workerRef.current = worker; + + worker.onmessage = (event) => { + const data = event.data as MessageFromWorker; + if (messageCallbacks.current.has(data.id)) { + const [resolve, reject] = messageCallbacks.current.get(data.id)!; + if ("error" in data) { + reject(data.error); + } else { + resolve(data.payload); + } + messageCallbacks.current.delete(data.id); + } + }; + + postMessage({ + type: "init", + }).then(({ success }) => { + if (success) { + setReady(true); + } + }); + + return worker; + }, []); + + useEffect(() => { + const worker = initializeWorker(); + + return () => { + worker.terminate(); + }; + }, [initializeWorker]); + + const interrupt = useCallback(async () => { + // Since we can't interrupt JavaScript execution directly, + // we terminate the worker and restart it, then restore state + isInterrupted.current = true; + + // Terminate the current worker + workerRef.current?.terminate(); + + // Clear pending callbacks + messageCallbacks.current.clear(); + + // Reset ready state + setReady(false); + + // Create a new worker + initializeWorker(); + + // Wait for initialization + await new Promise((resolve) => { + const checkReady = () => { + if (ready) { + resolve(); + } else { + setTimeout(checkReady, 50); + } + }; + checkReady(); + }); + + // Restore state by re-executing previous commands + await postMessage({ + type: "restoreState", + }); + + isInterrupted.current = false; + }, [initializeWorker, ready]); + + const runCommand = useCallback( + async (code: string): Promise => { + if (!mutex.current.isLocked()) { + throw new Error( + "mutex of JavaScriptContext must be locked for runCommand" + ); + } + if (!workerRef.current || !ready) { + return [{ type: "error", message: "JavaScript runtime is not ready yet." }]; + } + + try { + const { output } = await postMessage({ + type: "runJavaScript", + payload: { code }, + }); + return output; + } catch (error) { + // If interrupted, return a message indicating interruption + if (isInterrupted.current) { + return [{ type: "error", message: "実行が中断されました" }]; + } + throw error; + } + }, + [ready] + ); + + const checkSyntax = useCallback( + async (code: string): Promise => { + if (!workerRef.current || !ready) return "invalid"; + const { status } = await mutex.current.runExclusive(() => + postMessage({ + type: "checkSyntax", + payload: { code }, + }) + ); + return status; + }, + [ready] + ); + + const runFiles = useCallback( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (_filenames: string[]): Promise => { + return [ + { + type: "error", + message: "JavaScript file execution is not supported in this runtime", + }, + ]; + }, + [] + ); + + const splitReplExamples = useCallback((content: string): ReplCommand[] => { + const initCommands: { command: string; output: ReplOutput[] }[] = []; + for (const line of content.split("\n")) { + if (line.startsWith("> ")) { + // Remove the prompt from the command + initCommands.push({ command: line.slice(2), output: [] }); + } else { + // Lines without prompt are output from the previous command + if (initCommands.length > 0) { + initCommands[initCommands.length - 1].output.push({ + type: "stdout", + message: line, + }); + } + } + } + return initCommands; + }, []); + + const getCommandlineStr = useCallback( + (filenames: string[]) => `node ${filenames[0]}`, + [] + ); + + return ( + + {children} + + ); +} diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 7292c19..fb94e45 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -2,6 +2,7 @@ import { MutexInterface } from "async-mutex"; import { ReplOutput, SyntaxStatus, ReplCommand } from "./repl"; import { PyodideProvider, usePyodide } from "./python/runtime"; import { useWandbox, WandboxProvider } from "./wandbox/runtime"; +import { JavaScriptProvider, useJavaScript } from "./javascript/runtime"; import { AceLang } from "./editor"; import { ReactNode } from "react"; @@ -28,7 +29,7 @@ export interface LangConstants { prompt?: string; promptMore?: string; } -export type RuntimeLang = "python" | "cpp"; +export type RuntimeLang = "python" | "cpp" | "javascript"; export function getRuntimeLang( lang: string | undefined @@ -41,6 +42,9 @@ export function getRuntimeLang( case "cpp": case "c++": return "cpp"; + case "javascript": + case "js": + return "javascript"; default: console.warn(`Unsupported language for runtime: ${lang}`); return undefined; @@ -50,12 +54,15 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { // すべての言語のcontextをインスタンス化 const pyodide = usePyodide(); const wandboxCpp = useWandbox("cpp"); + const javascript = useJavaScript(); switch (language) { case "python": return pyodide; case "cpp": return wandboxCpp; + case "javascript": + return javascript; default: language satisfies never; throw new Error(`Runtime not implemented for language: ${language}`); @@ -64,7 +71,9 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { export function RuntimeProvider({ children }: { children: ReactNode }) { return ( - {children} + + {children} + ); } @@ -77,6 +86,11 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { prompt: ">>> ", promptMore: "... ", }; + case "javascript": + return { + tabSize: 2, + prompt: "> ", + }; case "c_cpp": case "cpp": return { diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 82620ae..d80b4a6 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -13,6 +13,7 @@ export function defineTests( { python: 2000, cpp: 10000, + javascript: 2000, } as Record )[lang] ); @@ -31,6 +32,7 @@ export function defineTests( { python: `print("${msg}")`, cpp: null, + javascript: `console.log("${msg}")`, } satisfies Record )[lang]; if (!printCode) { @@ -55,6 +57,7 @@ export function defineTests( { python: [`${varName} = ${value}`, `print(${varName})`], cpp: [null, null], + javascript: [`var ${varName} = ${value}`, `console.log(${varName})`], } satisfies Record )[lang]; if (!setIntVarCode || !printIntVarCode) { @@ -81,6 +84,7 @@ export function defineTests( { python: `raise Exception("${errorMsg}")`, cpp: null, + javascript: `throw new Error("${errorMsg}")`, } satisfies Record )[lang]; if (!errorCode) { @@ -100,6 +104,7 @@ export function defineTests( { python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`], cpp: [null, null, null], + javascript: [`var testVar = 42`, `while(true) {}`, `console.log(testVar)`], } satisfies Record )[lang]; if (!setIntVarCode || !infLoopCode || !printIntVarCode) { @@ -136,8 +141,12 @@ export function defineTests( "test.cpp", `#include \nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`, ], - } satisfies Record + javascript: [null, null], + } satisfies Record )[lang]; + if (!filename || !code) { + this.skip(); + } writeFile(filename, code); // use setTimeout to wait for writeFile to propagate. await new Promise((resolve) => setTimeout(resolve, 100)); @@ -160,8 +169,12 @@ export function defineTests( "test_error.cpp", `#include \nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`, ], - } satisfies Record + javascript: [null, null], + } satisfies Record )[lang]; + if (!filename || !code) { + this.skip(); + } writeFile(filename, code); await new Promise((resolve) => setTimeout(resolve, 100)); const result = await runtimeRef.current[lang].runFiles([filename]); @@ -192,8 +205,12 @@ export function defineTests( }, ["test_multi_main.cpp", "test_multi_sub.cpp"], ], - } satisfies Record, string[]]> + javascript: [null, null], + } satisfies Record, string[]] | [null, null]> )[lang]; + if (!codes || !execFiles) { + this.skip(); + } for (const [filename, code] of Object.entries(codes)) { writeFile(filename, code); } diff --git a/public/_headers b/public/_headers index 2472c04..68f3e33 100644 --- a/public/_headers +++ b/public/_headers @@ -3,3 +3,6 @@ /pyodide.worker.js Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp +/javascript.worker.js + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp diff --git a/public/javascript.worker.js b/public/javascript.worker.js new file mode 100644 index 0000000..fd2045d --- /dev/null +++ b/public/javascript.worker.js @@ -0,0 +1,149 @@ +// JavaScript web worker +let jsOutput = []; +let executedCommands = []; // Store successfully executed commands for state recovery +let globalScope = {}; // Store global variables and functions + +// Helper function to capture console output +function createConsoleProxy() { + return { + log: (...args) => { + jsOutput.push({ type: "stdout", message: args.join(" ") }); + }, + error: (...args) => { + jsOutput.push({ type: "stderr", message: args.join(" ") }); + }, + warn: (...args) => { + jsOutput.push({ type: "stderr", message: args.join(" ") }); + }, + info: (...args) => { + jsOutput.push({ type: "stdout", message: args.join(" ") }); + }, + }; +} + +async function init(id) { + // Initialize the worker + executedCommands = []; + globalScope = {}; + self.postMessage({ id, payload: { success: true } }); +} + +async function runJavaScript(id, payload) { + const { code } = payload; + try { + // Create a console proxy to capture output + const console = createConsoleProxy(); + + // Execute the code with eval in the global scope + // Use Function constructor with global scope to maintain state across calls + const func = new Function('console', 'globalScope', ` + with (globalScope) { + return eval(${JSON.stringify(code)}); + } + `); + const result = func(console, globalScope); + + if (result !== undefined) { + jsOutput.push({ + type: "return", + message: String(result), + }); + } + + // Save the successfully executed command for state recovery + executedCommands.push(code); + } catch (e) { + console.log(e); + if (e instanceof Error) { + jsOutput.push({ + type: "error", + message: `${e.name}: ${e.message}`, + }); + } else { + jsOutput.push({ + type: "error", + message: `予期せぬエラー: ${String(e)}`, + }); + } + } + + const output = [...jsOutput]; + jsOutput = []; // Clear output + + self.postMessage({ + id, + payload: { output, updatedFiles: [] }, + }); +} + +async function checkSyntax(id, payload) { + const { code } = payload; + + try { + // Try to create a Function to check syntax + new Function(code); + self.postMessage({ id, payload: { status: "complete" } }); + } catch (e) { + // Check if it's a syntax error or if more input is expected + if (e instanceof SyntaxError) { + // Simple heuristic: check for "Unexpected end of input" + if (e.message.includes("Unexpected end of input") || + e.message.includes("expected expression")) { + self.postMessage({ id, payload: { status: "incomplete" } }); + } else { + self.postMessage({ id, payload: { status: "invalid" } }); + } + } else { + self.postMessage({ id, payload: { status: "invalid" } }); + } + } +} + +async function restoreState(id) { + // Re-execute all previously successful commands to restore state + const commandsToRestore = [...executedCommands]; + executedCommands = []; // Clear before re-executing + jsOutput = []; // Clear output for restoration + const newGlobalScope = {}; // Create a fresh global scope + + for (const command of commandsToRestore) { + try { + const console = createConsoleProxy(); + const func = new Function('console', 'globalScope', ` + with (globalScope) { + return eval(${JSON.stringify(command)}); + } + `); + func(console, newGlobalScope); + executedCommands.push(command); + } catch (e) { + // If restoration fails, we still continue with other commands + console.error("Failed to restore command:", command, e); + } + } + + globalScope = newGlobalScope; // Update the global scope + jsOutput = []; // Clear any output from restoration + self.postMessage({ id, payload: { success: true } }); +} + +self.onmessage = async (event) => { + const { id, type, payload } = event.data; + switch (type) { + case "init": + await init(id); + return; + case "runJavaScript": + await runJavaScript(id, payload); + return; + case "checkSyntax": + await checkSyntax(id, payload); + return; + case "restoreState": + await restoreState(id); + return; + default: + console.error(`Unknown message type: ${type}`); + return; + } +}; From 6d2a86ff102b8977a5995a130e21db6713cb76e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:10:48 +0000 Subject: [PATCH 3/9] Fix interrupt implementation to properly wait for worker restart Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/javascript/runtime.tsx | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx index 55967c5..810ad9d 100644 --- a/app/terminal/javascript/runtime.tsx +++ b/app/terminal/javascript/runtime.tsx @@ -122,25 +122,24 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { // Create a new worker initializeWorker(); - // Wait for initialization + // Wait for initialization - use a different approach await new Promise((resolve) => { - const checkReady = () => { - if (ready) { - resolve(); - } else { - setTimeout(checkReady, 50); + const checkInterval = setInterval(() => { + if (workerRef.current) { + // Try to initialize and restore + postMessage({ + type: "restoreState", + }).then(() => { + clearInterval(checkInterval); + isInterrupted.current = false; + resolve(); + }).catch(() => { + // Keep trying + }); } - }; - checkReady(); - }); - - // Restore state by re-executing previous commands - await postMessage({ - type: "restoreState", + }, 100); }); - - isInterrupted.current = false; - }, [initializeWorker, ready]); + }, [initializeWorker]); const runCommand = useCallback( async (code: string): Promise => { From dc2e3d22a55fa2d99e8a9a176b34ffc332cf2ade Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:12:12 +0000 Subject: [PATCH 4/9] Improve error handling in JavaScript runtime Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/javascript/runtime.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx index 810ad9d..c86bc00 100644 --- a/app/terminal/javascript/runtime.tsx +++ b/app/terminal/javascript/runtime.tsx @@ -110,12 +110,14 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { // we terminate the worker and restart it, then restore state isInterrupted.current = true; + // Reject all pending callbacks before terminating + const error = "Worker interrupted"; + messageCallbacks.current.forEach(([, reject]) => reject(error)); + messageCallbacks.current.clear(); + // Terminate the current worker workerRef.current?.terminate(); - // Clear pending callbacks - messageCallbacks.current.clear(); - // Reset ready state setReady(false); @@ -159,11 +161,15 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { }); return output; } catch (error) { - // If interrupted, return a message indicating interruption + // If interrupted or worker was terminated, return appropriate message if (isInterrupted.current) { return [{ type: "error", message: "実行が中断されました" }]; } - throw error; + // Handle other errors + if (error instanceof Error) { + return [{ type: "error", message: error.message }]; + } + return [{ type: "error", message: String(error) }]; } }, [ready] From 6b0ede3d5100adf7d617f2a0d8001a301a2cd83e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:16:07 +0000 Subject: [PATCH 5/9] Add JavaScript syntax highlighting support Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/highlight.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/terminal/highlight.ts b/app/terminal/highlight.ts index e5be201..2232cc3 100644 --- a/app/terminal/highlight.ts +++ b/app/terminal/highlight.ts @@ -3,13 +3,16 @@ import chalk from "chalk"; import { RuntimeLang } from "./runtime"; // Python言語定義をインポート import "prismjs/components/prism-python"; +import "prismjs/components/prism-javascript"; -type PrismLang = "python"; +type PrismLang = "python" | "javascript"; function getPrismLanguage(language: RuntimeLang): PrismLang { switch (language) { case "python": return "python"; + case "javascript": + return "javascript"; case "cpp": throw new Error( `highlight for ${language} is disabled because it should not support REPL` From 4730d8ac0e246bbe0d116edef4ae1796de848ff8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:19:18 +0000 Subject: [PATCH 6/9] Fix JavaScript worker to use direct eval for state persistence Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- public/javascript.worker.js | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/public/javascript.worker.js b/public/javascript.worker.js index fd2045d..b486dcf 100644 --- a/public/javascript.worker.js +++ b/public/javascript.worker.js @@ -1,7 +1,6 @@ // JavaScript web worker let jsOutput = []; let executedCommands = []; // Store successfully executed commands for state recovery -let globalScope = {}; // Store global variables and functions // Helper function to capture console output function createConsoleProxy() { @@ -24,7 +23,6 @@ function createConsoleProxy() { async function init(id) { // Initialize the worker executedCommands = []; - globalScope = {}; self.postMessage({ id, payload: { success: true } }); } @@ -34,14 +32,9 @@ async function runJavaScript(id, payload) { // Create a console proxy to capture output const console = createConsoleProxy(); - // Execute the code with eval in the global scope - // Use Function constructor with global scope to maintain state across calls - const func = new Function('console', 'globalScope', ` - with (globalScope) { - return eval(${JSON.stringify(code)}); - } - `); - const result = func(console, globalScope); + // Execute code directly with eval in the worker global scope + // This will preserve variables across calls + const result = eval(code); if (result !== undefined) { jsOutput.push({ @@ -104,17 +97,11 @@ async function restoreState(id) { const commandsToRestore = [...executedCommands]; executedCommands = []; // Clear before re-executing jsOutput = []; // Clear output for restoration - const newGlobalScope = {}; // Create a fresh global scope for (const command of commandsToRestore) { try { const console = createConsoleProxy(); - const func = new Function('console', 'globalScope', ` - with (globalScope) { - return eval(${JSON.stringify(command)}); - } - `); - func(console, newGlobalScope); + eval(command); executedCommands.push(command); } catch (e) { // If restoration fails, we still continue with other commands @@ -122,7 +109,6 @@ async function restoreState(id) { } } - globalScope = newGlobalScope; // Update the global scope jsOutput = []; // Clear any output from restoration self.postMessage({ id, payload: { success: true } }); } From d3dd4f4dd549d48f171fa013d9c829abf25350b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:21:30 +0000 Subject: [PATCH 7/9] Fix code review issues: prevent infinite recursion and add timeout to interrupt Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/javascript/runtime.tsx | 18 ++++++++++++++---- public/javascript.worker.js | 6 ++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx index c86bc00..c0fc667 100644 --- a/app/terminal/javascript/runtime.tsx +++ b/app/terminal/javascript/runtime.tsx @@ -124,12 +124,22 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { // Create a new worker initializeWorker(); - // Wait for initialization - use a different approach - await new Promise((resolve) => { + // Wait for initialization with timeout + const maxRetries = 50; // 5 seconds total + let retries = 0; + + await new Promise((resolve, reject) => { const checkInterval = setInterval(() => { + retries++; + if (retries > maxRetries) { + clearInterval(checkInterval); + reject(new Error("Worker initialization timeout")); + return; + } + if (workerRef.current) { - // Try to initialize and restore - postMessage({ + // Try to restore state + postMessage<{ success: boolean }>({ type: "restoreState", }).then(() => { clearInterval(checkInterval); diff --git a/public/javascript.worker.js b/public/javascript.worker.js index b486dcf..104b391 100644 --- a/public/javascript.worker.js +++ b/public/javascript.worker.js @@ -46,7 +46,8 @@ async function runJavaScript(id, payload) { // Save the successfully executed command for state recovery executedCommands.push(code); } catch (e) { - console.log(e); + // Use self.console to avoid recursion with our console proxy + self.console.log(e); if (e instanceof Error) { jsOutput.push({ type: "error", @@ -105,7 +106,8 @@ async function restoreState(id) { executedCommands.push(command); } catch (e) { // If restoration fails, we still continue with other commands - console.error("Failed to restore command:", command, e); + // Use self.console to avoid recursion with our console proxy + self.console.error("Failed to restore command:", command, e); } } From bdfa2a1921e4de972613b779e6f6ce30eaef7b40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:25:36 +0000 Subject: [PATCH 8/9] Address PR feedback: fix payload types, move executedCommands to runtime, use mutex for interrupt, remove isInterrupted Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/javascript/runtime.tsx | 88 ++++++++++++----------------- public/javascript.worker.js | 15 ++--- 2 files changed, 39 insertions(+), 64 deletions(-) diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx index c0fc667..3c5313a 100644 --- a/app/terminal/javascript/runtime.tsx +++ b/app/terminal/javascript/runtime.tsx @@ -26,6 +26,7 @@ export function useJavaScript(): RuntimeContext { type MessageToWorker = | { type: "init"; + payload?: undefined; } | { type: "runJavaScript"; @@ -37,6 +38,7 @@ type MessageToWorker = } | { type: "restoreState"; + payload: { commands: string[] }; }; type MessageFromWorker = @@ -59,7 +61,7 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { Map void, (error: string) => void]> >(new Map()); const nextMessageId = useRef(0); - const isInterrupted = useRef(false); + const executedCommands = useRef([]); function postMessage({ type, payload }: MessageToWorker) { const id = nextMessageId.current++; @@ -86,70 +88,52 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { } }; - postMessage({ + return postMessage({ type: "init", }).then(({ success }) => { if (success) { setReady(true); } + return worker; }); - - return worker; }, []); useEffect(() => { - const worker = initializeWorker(); + let worker: Worker | null = null; + initializeWorker().then((w) => { + worker = w; + }); return () => { - worker.terminate(); + worker?.terminate(); }; }, [initializeWorker]); const interrupt = useCallback(async () => { // Since we can't interrupt JavaScript execution directly, // we terminate the worker and restart it, then restore state - isInterrupted.current = true; - - // Reject all pending callbacks before terminating - const error = "Worker interrupted"; - messageCallbacks.current.forEach(([, reject]) => reject(error)); - messageCallbacks.current.clear(); - - // Terminate the current worker - workerRef.current?.terminate(); - - // Reset ready state - setReady(false); - - // Create a new worker - initializeWorker(); - - // Wait for initialization with timeout - const maxRetries = 50; // 5 seconds total - let retries = 0; - - await new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - retries++; - if (retries > maxRetries) { - clearInterval(checkInterval); - reject(new Error("Worker initialization timeout")); - return; - } - - if (workerRef.current) { - // Try to restore state - postMessage<{ success: boolean }>({ - type: "restoreState", - }).then(() => { - clearInterval(checkInterval); - isInterrupted.current = false; - resolve(); - }).catch(() => { - // Keep trying - }); - } - }, 100); + await mutex.current.runExclusive(async () => { + // Reject all pending callbacks before terminating + const error = "Worker interrupted"; + messageCallbacks.current.forEach(([, reject]) => reject(error)); + messageCallbacks.current.clear(); + + // Terminate the current worker + workerRef.current?.terminate(); + + // Reset ready state + setReady(false); + + // Create a new worker and wait for it to be ready + await initializeWorker(); + + // Restore state by re-executing previous commands + if (executedCommands.current.length > 0) { + await postMessage<{ success: boolean }>({ + type: "restoreState", + payload: { commands: executedCommands.current }, + }); + } }); }, [initializeWorker]); @@ -169,13 +153,11 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { type: "runJavaScript", payload: { code }, }); + // Save successfully executed command + executedCommands.current.push(code); return output; } catch (error) { - // If interrupted or worker was terminated, return appropriate message - if (isInterrupted.current) { - return [{ type: "error", message: "実行が中断されました" }]; - } - // Handle other errors + // Handle errors (including "Worker interrupted") if (error instanceof Error) { return [{ type: "error", message: error.message }]; } diff --git a/public/javascript.worker.js b/public/javascript.worker.js index 104b391..e32326d 100644 --- a/public/javascript.worker.js +++ b/public/javascript.worker.js @@ -1,6 +1,5 @@ // JavaScript web worker let jsOutput = []; -let executedCommands = []; // Store successfully executed commands for state recovery // Helper function to capture console output function createConsoleProxy() { @@ -22,7 +21,6 @@ function createConsoleProxy() { async function init(id) { // Initialize the worker - executedCommands = []; self.postMessage({ id, payload: { success: true } }); } @@ -42,9 +40,6 @@ async function runJavaScript(id, payload) { message: String(result), }); } - - // Save the successfully executed command for state recovery - executedCommands.push(code); } catch (e) { // Use self.console to avoid recursion with our console proxy self.console.log(e); @@ -93,17 +88,15 @@ async function checkSyntax(id, payload) { } } -async function restoreState(id) { +async function restoreState(id, payload) { // Re-execute all previously successful commands to restore state - const commandsToRestore = [...executedCommands]; - executedCommands = []; // Clear before re-executing + const { commands } = payload; jsOutput = []; // Clear output for restoration - for (const command of commandsToRestore) { + for (const command of commands) { try { const console = createConsoleProxy(); eval(command); - executedCommands.push(command); } catch (e) { // If restoration fails, we still continue with other commands // Use self.console to avoid recursion with our console proxy @@ -128,7 +121,7 @@ self.onmessage = async (event) => { await checkSyntax(id, payload); return; case "restoreState": - await restoreState(id); + await restoreState(id, payload); return; default: console.error(`Unknown message type: ${type}`); From 35244c81fc8d3e019af7e4e0bf19b089db2f7afb Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Sat, 1 Nov 2025 04:35:07 +0900 Subject: [PATCH 9/9] =?UTF-8?q?console=E3=81=A8interrupt=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/javascript/runtime.tsx | 27 +++++++++-------- public/javascript.worker.js | 47 ++++++++++++----------------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/app/terminal/javascript/runtime.tsx b/app/terminal/javascript/runtime.tsx index 3c5313a..ba80a90 100644 --- a/app/terminal/javascript/runtime.tsx +++ b/app/terminal/javascript/runtime.tsx @@ -109,21 +109,22 @@ export function JavaScriptProvider({ children }: { children: ReactNode }) { }; }, [initializeWorker]); - const interrupt = useCallback(async () => { + const interrupt = useCallback(() => { // Since we can't interrupt JavaScript execution directly, // we terminate the worker and restart it, then restore state - await mutex.current.runExclusive(async () => { - // Reject all pending callbacks before terminating - const error = "Worker interrupted"; - messageCallbacks.current.forEach(([, reject]) => reject(error)); - messageCallbacks.current.clear(); - - // Terminate the current worker - workerRef.current?.terminate(); - - // Reset ready state - setReady(false); - + + // Reject all pending callbacks before terminating + const error = "Worker interrupted"; + messageCallbacks.current.forEach(([, reject]) => reject(error)); + messageCallbacks.current.clear(); + + // Terminate the current worker + workerRef.current?.terminate(); + + // Reset ready state + setReady(false); + + mutex.current.runExclusive(async () => { // Create a new worker and wait for it to be ready await initializeWorker(); diff --git a/public/javascript.worker.js b/public/javascript.worker.js index e32326d..2bec3bc 100644 --- a/public/javascript.worker.js +++ b/public/javascript.worker.js @@ -2,22 +2,21 @@ let jsOutput = []; // Helper function to capture console output -function createConsoleProxy() { - return { - log: (...args) => { - jsOutput.push({ type: "stdout", message: args.join(" ") }); - }, - error: (...args) => { - jsOutput.push({ type: "stderr", message: args.join(" ") }); - }, - warn: (...args) => { - jsOutput.push({ type: "stderr", message: args.join(" ") }); - }, - info: (...args) => { - jsOutput.push({ type: "stdout", message: args.join(" ") }); - }, - }; -} +const originalConsole = globalThis.console; +globalThis.console = { + log: (...args) => { + jsOutput.push({ type: "stdout", message: args.join(" ") }); + }, + error: (...args) => { + jsOutput.push({ type: "stderr", message: args.join(" ") }); + }, + warn: (...args) => { + jsOutput.push({ type: "stderr", message: args.join(" ") }); + }, + info: (...args) => { + jsOutput.push({ type: "stdout", message: args.join(" ") }); + }, +}; async function init(id) { // Initialize the worker @@ -27,12 +26,9 @@ async function init(id) { async function runJavaScript(id, payload) { const { code } = payload; try { - // Create a console proxy to capture output - const console = createConsoleProxy(); - // Execute code directly with eval in the worker global scope // This will preserve variables across calls - const result = eval(code); + const result = globalThis.eval(code); if (result !== undefined) { jsOutput.push({ @@ -41,8 +37,7 @@ async function runJavaScript(id, payload) { }); } } catch (e) { - // Use self.console to avoid recursion with our console proxy - self.console.log(e); + originalConsole.log(e); if (e instanceof Error) { jsOutput.push({ type: "error", @@ -95,12 +90,10 @@ async function restoreState(id, payload) { for (const command of commands) { try { - const console = createConsoleProxy(); - eval(command); + globalThis.eval(command); } catch (e) { // If restoration fails, we still continue with other commands - // Use self.console to avoid recursion with our console proxy - self.console.error("Failed to restore command:", command, e); + originalConsole.error("Failed to restore command:", command, e); } } @@ -124,7 +117,7 @@ self.onmessage = async (event) => { await restoreState(id, payload); return; default: - console.error(`Unknown message type: ${type}`); + originalConsole.error(`Unknown message type: ${type}`); return; } };