From d1e56fe25fa91a2b141daa899d3c0c7dd7a390b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:10:29 +0000 Subject: [PATCH 1/5] Initial plan From 783858a6e4e0c0d2c99e78bb65e6fe2deb8aa69c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:17:07 +0000 Subject: [PATCH 2/5] Refactor workers to stream output immediately via callbacks instead of accumulating in arrays Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/worker/jsEval.worker.ts | 58 +++++++++++++++------------ app/terminal/worker/pyodide.worker.ts | 49 +++++++++++++--------- app/terminal/worker/ruby.worker.ts | 52 +++++++++++++----------- app/terminal/worker/runtime.tsx | 33 ++++++++++----- 4 files changed, 114 insertions(+), 78 deletions(-) diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 1377410..ea0ca38 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -10,23 +10,31 @@ function format(...args: unknown[]): string { // https://nodejs.org/api/util.html#utilformatformat-args return args.map((a) => (typeof a === "string" ? a : inspect(a))).join(" "); } -let jsOutput: ReplOutput[] = []; +let currentOutputCallback: ((output: ReplOutput) => void) | null = null; // Helper function to capture console output const originalConsole = self.console; self.console = { ...originalConsole, log: (...args: unknown[]) => { - jsOutput.push({ type: "stdout", message: format(...args) }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stdout", message: format(...args) }); + } }, error: (...args: unknown[]) => { - jsOutput.push({ type: "stderr", message: format(...args) }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stderr", message: format(...args) }); + } }, warn: (...args: unknown[]) => { - jsOutput.push({ type: "stderr", message: format(...args) }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stderr", message: format(...args) }); + } }, info: (...args: unknown[]) => { - jsOutput.push({ type: "stdout", message: format(...args) }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stdout", message: format(...args) }); + } }, }; @@ -73,13 +81,16 @@ async function replLikeEval(code: string): Promise { } } -async function runCode(code: string): Promise<{ - output: ReplOutput[]; +async function runCode( + code: string, + onOutput: (output: ReplOutput) => void +): Promise<{ updatedFiles: Record; }> { + currentOutputCallback = onOutput; try { const result = await replLikeEval(code); - jsOutput.push({ + onOutput({ type: "return", message: inspect(result), }); @@ -87,51 +98,51 @@ async function runCode(code: string): Promise<{ originalConsole.log(e); // TODO: stack trace? if (e instanceof Error) { - jsOutput.push({ + onOutput({ type: "error", message: `${e.name}: ${e.message}`, }); } else { - jsOutput.push({ + onOutput({ type: "error", message: `${String(e)}`, }); } + } finally { + currentOutputCallback = null; } - const output = [...jsOutput]; - jsOutput = []; // Clear output - - return { output, updatedFiles: {} as Record }; + return { updatedFiles: {} as Record }; } function runFile( name: string, - files: Record -): { output: ReplOutput[]; updatedFiles: Record } { + files: Record, + onOutput: (output: ReplOutput) => void +): { updatedFiles: Record } { // pyodide worker などと異なり、複数ファイルを読み込んでimportのようなことをするのには対応していません。 + currentOutputCallback = onOutput; try { self.eval(files[name]); } catch (e) { originalConsole.log(e); // TODO: stack trace? if (e instanceof Error) { - jsOutput.push({ + onOutput({ type: "error", message: `${e.name}: ${e.message}`, }); } else { - jsOutput.push({ + onOutput({ type: "error", message: `${String(e)}`, }); } + } finally { + currentOutputCallback = null; } - const output = [...jsOutput]; - jsOutput = []; // Clear output - - return { output, updatedFiles: {} as Record }; + return { updatedFiles: {} as Record }; } async function checkSyntax( @@ -162,8 +173,6 @@ async function checkSyntax( async function restoreState(commands: string[]): Promise { // Re-execute all previously successful commands to restore state - jsOutput = []; // Clear output for restoration - for (const command of commands) { try { replLikeEval(command); @@ -173,7 +182,6 @@ async function restoreState(commands: string[]): Promise { } } - jsOutput = []; // Clear any output from restoration return {}; } diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index e6cb985..232f9a3 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -16,7 +16,7 @@ const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`; const HOME = `/home/pyodide/`; let pyodide: PyodideInterface; -let pyodideOutput: ReplOutput[] = []; +let currentOutputCallback: ((output: ReplOutput) => void) | null = null; // Helper function to read all files from the Pyodide file system function readAllFiles(): Record { @@ -48,12 +48,16 @@ async function init( pyodide.setStdout({ batched: (str: string) => { - pyodideOutput.push({ type: "stdout", message: str }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stdout", message: str }); + } }, }); pyodide.setStderr({ batched: (str: string) => { - pyodideOutput.push({ type: "stderr", message: str }); + if (currentOutputCallback) { + currentOutputCallback({ type: "stderr", message: str }); + } }, }); @@ -62,17 +66,20 @@ async function init( return { capabilities: { interrupt: "buffer" } }; } -async function runCode(code: string): Promise<{ - output: ReplOutput[]; +async function runCode( + code: string, + onOutput: (output: ReplOutput) => void +): Promise<{ updatedFiles: Record; }> { if (!pyodide) { throw new Error("Pyodide not initialized"); } + currentOutputCallback = onOutput; try { const result = await pyodide.runPythonAsync(code); if (result !== undefined) { - pyodideOutput.push({ + onOutput({ type: "return", message: String(result), }); @@ -86,7 +93,7 @@ async function runCode(code: string): Promise<{ const execLineIndex = lines.findIndex((line) => line.includes("") ); - pyodideOutput.push({ + onOutput({ type: "error", message: lines .slice(0, 1) @@ -95,33 +102,35 @@ async function runCode(code: string): Promise<{ .trim(), }); } else { - pyodideOutput.push({ + onOutput({ type: "error", message: `予期せぬエラー: ${e.message.trim()}`, }); } } else { - pyodideOutput.push({ + onOutput({ type: "error", message: `予期せぬエラー: ${String(e).trim()}`, }); } + } finally { + currentOutputCallback = null; } const updatedFiles = readAllFiles(); - const output = [...pyodideOutput]; - pyodideOutput = []; // 出力をクリア - return { output, updatedFiles }; + return { updatedFiles }; } async function runFile( name: string, - files: Record -): Promise<{ output: ReplOutput[]; updatedFiles: Record }> { + files: Record, + onOutput: (output: ReplOutput) => void +): Promise<{ updatedFiles: Record }> { if (!pyodide) { throw new Error("Pyodide not initialized"); } + currentOutputCallback = onOutput; try { // Use Pyodide FS API to write files to the file system for (const filename of Object.keys(files)) { @@ -142,7 +151,7 @@ async function runFile( const execLineIndex = lines.findLastIndex((line) => line.includes("") ); - pyodideOutput.push({ + onOutput({ type: "error", message: lines .slice(0, 1) @@ -151,23 +160,23 @@ async function runFile( .trim(), }); } else { - pyodideOutput.push({ + onOutput({ type: "error", message: `予期せぬエラー: ${e.message.trim()}`, }); } } else { - pyodideOutput.push({ + onOutput({ type: "error", message: `予期せぬエラー: ${String(e).trim()}`, }); } + } finally { + currentOutputCallback = null; } const updatedFiles = readAllFiles(); - const output = [...pyodideOutput]; - pyodideOutput = []; // 出力をクリア - return { output, updatedFiles }; + return { updatedFiles }; } async function checkSyntax( diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index ccc4a99..20c2547 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -10,7 +10,7 @@ import type { ReplOutput } from "../repl"; import init_rb from "./ruby/init.rb?raw"; let rubyVM: RubyVM | null = null; -let rubyOutput: ReplOutput[] = []; +let currentOutputCallback: ((output: ReplOutput) => void) | null = null; let stdoutBuffer = ""; let stderrBuffer = ""; @@ -55,28 +55,30 @@ async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ } function flushOutput() { + if (!currentOutputCallback) return; + if (stdoutBuffer) { const lines = stdoutBuffer.split("\n"); for (let i = 0; i < lines.length - 1; i++) { - rubyOutput.push({ type: "stdout", message: lines[i] }); + currentOutputCallback({ type: "stdout", message: lines[i] }); } stdoutBuffer = lines[lines.length - 1]; } // Final flush if there's remaining non-empty text if (stdoutBuffer && stdoutBuffer.trim()) { - rubyOutput.push({ type: "stdout", message: stdoutBuffer }); + currentOutputCallback({ type: "stdout", message: stdoutBuffer }); } stdoutBuffer = ""; if (stderrBuffer) { const lines = stderrBuffer.split("\n"); for (let i = 0; i < lines.length - 1; i++) { - rubyOutput.push({ type: "stderr", message: lines[i] }); + currentOutputCallback({ type: "stderr", message: lines[i] }); } stderrBuffer = lines[lines.length - 1]; } if (stderrBuffer && stderrBuffer.trim()) { - rubyOutput.push({ type: "stderr", message: stderrBuffer }); + currentOutputCallback({ type: "stderr", message: stderrBuffer }); } stderrBuffer = ""; } @@ -101,16 +103,18 @@ function formatRubyError(error: unknown, isFile: boolean): string { return errorMessage; } -async function runCode(code: string): Promise<{ - output: ReplOutput[]; +async function runCode( + code: string, + onOutput: (output: ReplOutput) => void +): Promise<{ updatedFiles: Record; }> { if (!rubyVM) { throw new Error("Ruby VM not initialized"); } + currentOutputCallback = onOutput; try { - rubyOutput = []; stdoutBuffer = ""; stderrBuffer = ""; @@ -122,7 +126,7 @@ async function runCode(code: string): Promise<{ const resultStr = await result.callAsync("inspect"); // Add result to output if it's not nil and not empty - rubyOutput.push({ + onOutput({ type: "return", message: resultStr.toString(), }); @@ -130,29 +134,30 @@ async function runCode(code: string): Promise<{ console.log(e); flushOutput(); - rubyOutput.push({ + onOutput({ type: "error", message: formatRubyError(e, false), }); + } finally { + currentOutputCallback = null; } const updatedFiles = readAllFiles(); - const output = [...rubyOutput]; - rubyOutput = []; - return { output, updatedFiles }; + return { updatedFiles }; } async function runFile( name: string, - files: Record -): Promise<{ output: ReplOutput[]; updatedFiles: Record }> { + files: Record, + onOutput: (output: ReplOutput) => void +): Promise<{ updatedFiles: Record }> { if (!rubyVM) { throw new Error("Ruby VM not initialized"); } + currentOutputCallback = onOutput; try { - rubyOutput = []; stdoutBuffer = ""; stderrBuffer = ""; @@ -179,17 +184,17 @@ async function runFile( console.log(e); flushOutput(); - rubyOutput.push({ + onOutput({ type: "error", message: formatRubyError(e, true), }); + } finally { + currentOutputCallback = null; } const updatedFiles = readAllFiles(); - const output = [...rubyOutput]; - rubyOutput = []; - return { output, updatedFiles }; + return { updatedFiles }; } async function checkSyntax( @@ -266,7 +271,6 @@ async function restoreState(commands: string[]): Promise { throw new Error("Ruby VM not initialized"); } - rubyOutput = []; // Clear output for restoration stdoutBuffer = ""; stderrBuffer = ""; @@ -279,9 +283,9 @@ async function restoreState(commands: string[]): Promise { } } - // Clear any output from restoration - flushOutput(); - rubyOutput = []; + // Clear any buffered output from restoration + stdoutBuffer = ""; + stderrBuffer = ""; return {}; } diff --git a/app/terminal/worker/runtime.tsx b/app/terminal/worker/runtime.tsx index 99c2bf0..abc0291 100644 --- a/app/terminal/worker/runtime.tsx +++ b/app/terminal/worker/runtime.tsx @@ -9,7 +9,7 @@ import { useRef, useState, } from "react"; -import { wrap, Remote } from "comlink"; +import { wrap, Remote, proxy } from "comlink"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput, SyntaxStatus } from "../repl"; import { Mutex, MutexInterface } from "async-mutex"; @@ -26,12 +26,14 @@ export interface WorkerAPI { interruptBuffer: Uint8Array ): Promise<{ capabilities: WorkerCapabilities }>; runCode( - code: string - ): Promise<{ output: ReplOutput[]; updatedFiles: Record }>; + code: string, + onOutput: (output: ReplOutput) => void + ): Promise<{ updatedFiles: Record }>; runFile( name: string, - files: Record - ): Promise<{ output: ReplOutput[]; updatedFiles: Record }>; + files: Record, + onOutput: (output: ReplOutput) => void + ): Promise<{ updatedFiles: Record }>; checkSyntax(code: string): Promise<{ status: SyntaxStatus }>; restoreState(commands: string[]): Promise; } @@ -191,8 +193,14 @@ export function WorkerProvider({ } try { - const { output, updatedFiles } = await trackPromise( - workerApiRef.current.runCode(code) + const output: ReplOutput[] = []; + const { updatedFiles } = await trackPromise( + workerApiRef.current.runCode( + code, + proxy((item: ReplOutput) => { + output.push(item); + }) + ) ); writeFile(updatedFiles); @@ -255,8 +263,15 @@ export function WorkerProvider({ interruptBuffer.current[0] = 0; } return mutex.runExclusive(async () => { - const { output, updatedFiles } = await trackPromise( - workerApiRef.current!.runFile(filenames[0], files) + const output: ReplOutput[] = []; + const { updatedFiles } = await trackPromise( + workerApiRef.current!.runFile( + filenames[0], + files, + proxy((item: ReplOutput) => { + output.push(item); + }) + ) ); writeFile(updatedFiles); return output; From 9a0880376a1aec55a225bb4ae98c66aa7dfbb93c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:42:24 +0000 Subject: [PATCH 3/5] Address PR feedback: use optional chaining, flush lines immediately, move flushOutput to finally Co-authored-by: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> --- app/terminal/worker/jsEval.worker.ts | 16 +++------ app/terminal/worker/ruby.worker.ts | 50 +++++++++++++--------------- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index ea0ca38..940a78a 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -17,24 +17,16 @@ const originalConsole = self.console; self.console = { ...originalConsole, log: (...args: unknown[]) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stdout", message: format(...args) }); - } + currentOutputCallback?.({ type: "stdout", message: format(...args) }); }, error: (...args: unknown[]) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stderr", message: format(...args) }); - } + currentOutputCallback?.({ type: "stderr", message: format(...args) }); }, warn: (...args: unknown[]) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stderr", message: format(...args) }); - } + currentOutputCallback?.({ type: "stderr", message: format(...args) }); }, info: (...args: unknown[]) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stdout", message: format(...args) }); - } + currentOutputCallback?.({ type: "stdout", message: format(...args) }); }, }; diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index 20c2547..e39ce2a 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -21,11 +21,27 @@ declare global { self.stdout = { write(str: string) { stdoutBuffer += str; + // If buffer contains newlines, flush complete lines immediately + if (stdoutBuffer.includes("\n")) { + const lines = stdoutBuffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + currentOutputCallback?.({ type: "stdout", message: lines[i] }); + } + stdoutBuffer = lines[lines.length - 1]; + } }, }; self.stderr = { write(str: string) { stderrBuffer += str; + // If buffer contains newlines, flush complete lines immediately + if (stderrBuffer.includes("\n")) { + const lines = stderrBuffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + currentOutputCallback?.({ type: "stderr", message: lines[i] }); + } + stderrBuffer = lines[lines.length - 1]; + } }, }; @@ -55,30 +71,14 @@ async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ } function flushOutput() { - if (!currentOutputCallback) return; - - if (stdoutBuffer) { - const lines = stdoutBuffer.split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - currentOutputCallback({ type: "stdout", message: lines[i] }); - } - stdoutBuffer = lines[lines.length - 1]; - } - // Final flush if there's remaining non-empty text + // Flush any remaining non-empty text without newlines if (stdoutBuffer && stdoutBuffer.trim()) { - currentOutputCallback({ type: "stdout", message: stdoutBuffer }); + currentOutputCallback?.({ type: "stdout", message: stdoutBuffer }); } stdoutBuffer = ""; - if (stderrBuffer) { - const lines = stderrBuffer.split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - currentOutputCallback({ type: "stderr", message: lines[i] }); - } - stderrBuffer = lines[lines.length - 1]; - } if (stderrBuffer && stderrBuffer.trim()) { - currentOutputCallback({ type: "stderr", message: stderrBuffer }); + currentOutputCallback?.({ type: "stderr", message: stderrBuffer }); } stderrBuffer = ""; } @@ -120,9 +120,6 @@ async function runCode( const result = rubyVM.eval(code); - // Flush any buffered output - flushOutput(); - const resultStr = await result.callAsync("inspect"); // Add result to output if it's not nil and not empty @@ -132,13 +129,14 @@ async function runCode( }); } catch (e) { console.log(e); - flushOutput(); onOutput({ type: "error", message: formatRubyError(e, false), }); } finally { + // Flush any buffered output + flushOutput(); currentOutputCallback = null; } @@ -177,18 +175,16 @@ async function runFile( // Run the specified file rubyVM.eval(`load ${JSON.stringify(name)}`); - - // Flush any buffered output - flushOutput(); } catch (e) { console.log(e); - flushOutput(); onOutput({ type: "error", message: formatRubyError(e, true), }); } finally { + // Flush any buffered output + flushOutput(); currentOutputCallback = null; } diff --git a/package-lock.json b/package-lock.json index 94e3088..f7bd4d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "remark-gfm": "^4.0.1", "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", - "typescript": "^5.9.3", "zod": "^4.0.17" }, "devDependencies": { @@ -63,6 +62,7 @@ "prisma": "^6.18.0", "tailwindcss": "^4", "tsx": "^4.20.6", + "typescript": "5.9.3", "wrangler": "^4.27.0" } }, diff --git a/package.json b/package.json index 383b17c..0f7993c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "remark-gfm": "^4.0.1", "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", - "typescript": "^5.9.3", "zod": "^4.0.17" }, "devDependencies": { @@ -71,6 +70,7 @@ "prisma": "^6.18.0", "tailwindcss": "^4", "tsx": "^4.20.6", + "typescript": "5.9.3", "wrangler": "^4.27.0" } } From 52abfd2b05e28075a3d402057af71f70f8df4d9b Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 17 Dec 2025 01:19:40 +0900 Subject: [PATCH 4/5] =?UTF-8?q?currentOutputCallback=E3=81=AE=E3=82=AA?= =?UTF-8?q?=E3=83=97=E3=82=B7=E3=83=A7=E3=83=8A=E3=83=AB=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E3=82=92=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/worker/jsEval.worker.ts | 20 ++++++++------------ app/terminal/worker/pyodide.worker.ts | 14 ++++---------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/app/terminal/worker/jsEval.worker.ts b/app/terminal/worker/jsEval.worker.ts index 940a78a..64e2302 100644 --- a/app/terminal/worker/jsEval.worker.ts +++ b/app/terminal/worker/jsEval.worker.ts @@ -16,18 +16,14 @@ let currentOutputCallback: ((output: ReplOutput) => void) | null = null; const originalConsole = self.console; self.console = { ...originalConsole, - log: (...args: unknown[]) => { - currentOutputCallback?.({ type: "stdout", message: format(...args) }); - }, - error: (...args: unknown[]) => { - currentOutputCallback?.({ type: "stderr", message: format(...args) }); - }, - warn: (...args: unknown[]) => { - currentOutputCallback?.({ type: "stderr", message: format(...args) }); - }, - info: (...args: unknown[]) => { - currentOutputCallback?.({ type: "stdout", message: format(...args) }); - }, + log: (...args: unknown[]) => + currentOutputCallback?.({ type: "stdout", message: format(...args) }), + error: (...args: unknown[]) => + currentOutputCallback?.({ type: "stderr", message: format(...args) }), + warn: (...args: unknown[]) => + currentOutputCallback?.({ type: "stderr", message: format(...args) }), + info: (...args: unknown[]) => + currentOutputCallback?.({ type: "stdout", message: format(...args) }), }; async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index 232f9a3..fbfbe7e 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -47,18 +47,12 @@ async function init( pyodide = await (self as any).loadPyodide({ indexURL: PYODIDE_CDN }); pyodide.setStdout({ - batched: (str: string) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stdout", message: str }); - } - }, + batched: (str: string) => + currentOutputCallback?.({ type: "stdout", message: str }), }); pyodide.setStderr({ - batched: (str: string) => { - if (currentOutputCallback) { - currentOutputCallback({ type: "stderr", message: str }); - } - }, + batched: (str: string) => + currentOutputCallback?.({ type: "stderr", message: str }), }); pyodide.setInterruptBuffer(interruptBuffer); From ace27ff28c17ac7d74394334a641d392506d6f53 Mon Sep 17 00:00:00 2001 From: na-trium-144 <100704180+na-trium-144@users.noreply.github.com> Date: Wed, 17 Dec 2025 01:23:58 +0900 Subject: [PATCH 5/5] =?UTF-8?q?ruby=E3=81=AEoutput=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/terminal/repl.tsx | 3 +- app/terminal/worker/ruby.worker.ts | 47 +++++++++++++++++------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index cc31d14..f1a9b46 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -17,8 +17,9 @@ import { useEmbedContext } from "./embedContext"; import { emptyMutex, langConstants, RuntimeLang, useRuntime } from "./runtime"; import clsx from "clsx"; +export type ReplOutputType = "stdout" | "stderr" | "error" | "return" | "trace" | "system"; export interface ReplOutput { - type: "stdout" | "stderr" | "error" | "return" | "trace" | "system"; // 出力の種類 + type: ReplOutputType; // 出力の種類 message: string; // 出力メッセージ } export interface ReplCommand { diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index e39ce2a..af6779d 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -5,7 +5,7 @@ import { expose } from "comlink"; import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser"; import type { RubyVM } from "@ruby/wasm-wasi/dist/vm"; import type { WorkerCapabilities } from "./runtime"; -import type { ReplOutput } from "../repl"; +import type { ReplOutput, ReplOutputType } from "../repl"; import init_rb from "./ruby/init.rb?raw"; @@ -21,29 +21,26 @@ declare global { self.stdout = { write(str: string) { stdoutBuffer += str; - // If buffer contains newlines, flush complete lines immediately - if (stdoutBuffer.includes("\n")) { - const lines = stdoutBuffer.split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - currentOutputCallback?.({ type: "stdout", message: lines[i] }); - } - stdoutBuffer = lines[lines.length - 1]; - } + stdoutBuffer = handleBatchOutput(stdoutBuffer, "stdout"); }, }; self.stderr = { write(str: string) { stderrBuffer += str; - // If buffer contains newlines, flush complete lines immediately - if (stderrBuffer.includes("\n")) { - const lines = stderrBuffer.split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - currentOutputCallback?.({ type: "stderr", message: lines[i] }); - } - stderrBuffer = lines[lines.length - 1]; - } + stderrBuffer = handleBatchOutput(stderrBuffer, "stderr"); }, }; +function handleBatchOutput(buffer: string, type: ReplOutputType): string { + // If buffer contains newlines, flush complete lines immediately + if (buffer.includes("\n")) { + const lines = buffer.split("\n"); + for (let i = 0; i < lines.length - 1; i++) { + currentOutputCallback?.({ type, message: lines[i] }); + } + return lines[lines.length - 1]; + } + return buffer; +} async function init(/*_interruptBuffer?: Uint8Array*/): Promise<{ capabilities: WorkerCapabilities; @@ -122,6 +119,9 @@ async function runCode( const resultStr = await result.callAsync("inspect"); + // Flush any buffered output + flushOutput(); + // Add result to output if it's not nil and not empty onOutput({ type: "return", @@ -130,13 +130,14 @@ async function runCode( } catch (e) { console.log(e); + // Flush any buffered output + flushOutput(); + onOutput({ type: "error", message: formatRubyError(e, false), }); } finally { - // Flush any buffered output - flushOutput(); currentOutputCallback = null; } @@ -175,16 +176,20 @@ async function runFile( // Run the specified file rubyVM.eval(`load ${JSON.stringify(name)}`); + + // Flush any buffered output + flushOutput(); } catch (e) { console.log(e); + // Flush any buffered output + flushOutput(); + onOutput({ type: "error", message: formatRubyError(e, true), }); } finally { - // Flush any buffered output - flushOutput(); currentOutputCallback = null; }