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;
}
};