Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a2f0aee
Initial plan
Copilot Oct 30, 2025
4c57151
Add Ruby runtime implementation with ruby.wasm
Copilot Oct 30, 2025
00f3e85
Improve Ruby worker implementation with proper WASI stdout/stderr cap…
Copilot Oct 30, 2025
0b686e9
Address code review feedback - fix interrupt buffer and error handling
Copilot Oct 30, 2025
bb921ef
Implement interrupt with command history and VM restart
Copilot Oct 31, 2025
6fd45b7
Remove unused Ruby npm dependencies (loaded from CDN instead)
Copilot Oct 31, 2025
f722cf2
Merge branch 'main' of https://github.com/ut-code/my-code into copilo…
Copilot Oct 31, 2025
43d5db4
Add Ruby runtime tests to test suite
Copilot Oct 31, 2025
e0eaed0
Add Ruby worker headers configuration
Copilot Oct 31, 2025
7cd8b2a
Initial plan
Copilot Oct 31, 2025
f0b2004
Add JavaScript runtime implementation
Copilot Oct 31, 2025
6d2a86f
Fix interrupt implementation to properly wait for worker restart
Copilot Oct 31, 2025
dc2e3d2
Improve error handling in JavaScript runtime
Copilot Oct 31, 2025
6b0ede3
Add JavaScript syntax highlighting support
Copilot Oct 31, 2025
4730d8a
Fix JavaScript worker to use direct eval for state persistence
Copilot Oct 31, 2025
d3dd4f4
Fix code review issues: prevent infinite recursion and add timeout to…
Copilot Oct 31, 2025
bba92bd
ruby runtimeのエラーを修正, wasip1指定を消しdefaultrubyvmに変更
na-trium-144 Oct 31, 2025
6dc8c9f
eval行消さなくていいような
na-trium-144 Oct 31, 2025
bdfa2a1
Address PR feedback: fix payload types, move executedCommands to runt…
Copilot Oct 31, 2025
8b04d28
interrupt()のmutexについて
na-trium-144 Oct 31, 2025
35244c8
consoleとinterruptを修正
na-trium-144 Oct 31, 2025
2f11ed3
Merge remote-tracking branch 'origin/copilot/add-js-runtime-api' into…
na-trium-144 Nov 1, 2025
46b6161
workerを使うruntime各種をリファクタ
na-trium-144 Nov 1, 2025
dfee1eb
worker/ディレクトリに移動し、createRuntime()ではなくProviderを直接定義
na-trium-144 Nov 1, 2025
d3394ff
update readme
na-trium-144 Nov 1, 2025
6644621
テストページをterminal/page.tsxに統一
na-trium-144 Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions app/terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
* ランタイムの初期化が完了したか、不要である場合true
* mutex?: `MutexInterface`
* ランタイムに排他制御が必要な場合、MutexInterfaceのインスタンスを返してください。
* interrupt?: `() => Promise<void>`
* 実行中のコマンドを中断します。呼び出し側でmutexのロックはされません
* interrupt?: `() => void`
* 実行中のコマンドを中断します。
* 呼び出し側でmutexのロックはしません。interrupt()を呼ぶ際にはrunCommand()やrunFiles()が実行中であるためmutexはすでにロックされているはずです。
* interrupt()内で実行中の処理のPromiseをrejectしたあと、runtimeを再開する際の処理に必要であればmutexをロックすることも可能です。

### REPL用

Expand All @@ -25,7 +27,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ
* checkSyntax?: `(code: string) => Promise<SyntaxStatus>`
* コードの構文チェックを行います。行がコマンドとして完結していれば`complete`、次の行に続く場合(if文の条件式の途中など)は`incomplete`を返してください。
* REPLでEnterを押した際の動作に影響します。
* 呼び出し側でmutexのロックはされません
* 呼び出し側でmutexのロックはせず、必要であればcheckSyntax()内でロックします。
* splitReplExamples?: `(code: string) => ReplCommands[]`
* markdown内に記述されているREPLのサンプルコードをパースします。例えば
```
Expand All @@ -51,7 +53,7 @@ runtime.tsx の `useRuntime(lang)` は各言語のフックを呼び出し、そ

* runFiles: `(filenames: string[]) => Promise<ReplOutput[]>`
* 指定されたファイルを実行します。ファイルの中身はEmbedContextから取得されます。
* 呼び出し側でmutexのロックはされません
* 呼び出し側でmutexのロックはせず、必要であればrunFiles()内でロックします。
* getCommandlineStr: `(filenames: string[]) => string`
* 指定されたファイルを実行するためのコマンドライン引数文字列を返します。表示用です。

Expand Down Expand Up @@ -122,11 +124,20 @@ EditorComponent コンポーネントを提供します。

## 各言語の実装

### Pyodide (Python)
### Worker

Pyodide を web worker で動かしています。worker側のスクリプトは /public/python.worker.js にあります。
web worker でコードを実行する実装です。worker側のスクリプトは /public にあります。
workerとの通信部分は言語によらず共通なので、それをworker/runtime.tsxで定義しています。
Contextは言語ごとに分けて(worker/pyodide.ts などで)定義しています。

### Wandbox (C++)
Pythonの実行環境にはPyodideを使用しています。
PyodideにはKeyboardInterruptを送信する機能があるのでinterrupt()でそれを利用しています。

Rubyの実行環境にはruby.wasmを使用しています。

JavaScriptはeval()を使用しています。runFiles()のAPIだけ実装していません。

### Wandbox

wandbox.org のAPIを利用してC++コードを実行しています。C++以外にもいろいろな言語に対応しています。

Expand Down
10 changes: 9 additions & 1 deletion app/terminal/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const AceEditor = dynamic(
await import("ace-builds/src-min-noconflict/ext-language_tools");
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-ruby");
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");
Expand All @@ -28,16 +30,22 @@ 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" | "ruby" | "c_cpp" | "javascript" | "json" | "csv" | "text";
export function getAceLang(lang: string | undefined): AceLang {
// Markdownで指定される可能性のある言語名からAceLangを取得
switch (lang) {
case "python":
case "py":
return "python";
case "ruby":
case "rb":
return "ruby";
case "cpp":
case "c++":
return "c_cpp";
case "javascript":
case "js":
return "javascript";
case "json":
return "json";
case "csv":
Expand Down
2 changes: 1 addition & 1 deletion app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function ExecFile(props: ExecProps) {
▶ 実行
</button>
<code className="text-sm ml-4">
{getCommandlineStr(props.filenames)}
{getCommandlineStr?.(props.filenames)}
</code>
</div>
<div className="bg-base-300 p-4 pt-2 rounded-b-lg">
Expand Down
10 changes: 8 additions & 2 deletions app/terminal/highlight.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import Prism from "prismjs";
import chalk from "chalk";
import { RuntimeLang } from "./runtime";
// Python言語定義をインポート
// 言語定義をインポート
import "prismjs/components/prism-python";
import "prismjs/components/prism-ruby";
import "prismjs/components/prism-javascript";

type PrismLang = "python";
type PrismLang = "python" | "ruby" | "javascript";

function getPrismLanguage(language: RuntimeLang): PrismLang {
switch (language) {
case "python":
return "python";
case "ruby":
return "ruby";
case "javascript":
return "javascript";
case "cpp":
throw new Error(
`highlight for ${language} is disabled because it should not support REPL`
Expand Down
230 changes: 170 additions & 60 deletions app/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,144 @@
import { Heading } from "@/[docs_id]/markdown";
import "mocha/mocha.js";
import "mocha/mocha.css";
import { useEffect, useRef, useState } from "react";
import { usePyodide } from "./python/runtime";
import { Fragment, useEffect, useRef, useState } from "react";
import { useWandbox } from "./wandbox/runtime";
import { RuntimeContext, RuntimeLang } from "./runtime";
import { useEmbedContext } from "./embedContext";
import { defineTests } from "./tests";
import { usePyodide } from "./worker/pyodide";
import { useRuby } from "./worker/ruby";
import { useJSEval } from "./worker/jsEval";
import { ReplTerminal } from "./repl";
import { EditorComponent, getAceLang } from "./editor";
import { ExecFile } from "./exec";

export default function RuntimeTestPage() {
return (
<div className="p-4 mx-auto w-full max-w-200">
<Heading level={1}>Runtime Test Page</Heading>

<Heading level={2}>REPLとコード実行のサンプル</Heading>
{/* name of each tab group should be unique */}
<div className="tabs tabs-border">
{Object.entries(sampleConfig).map(([lang, config]) => (
<Fragment key={lang}>
<input
type="radio"
name="runtime-sample-tabs"
className="tab"
aria-label={lang}
/>
<div className="tab-content border-base-300 bg-base-100 p-4">
<RuntimeSample lang={lang as RuntimeLang} config={config} />
</div>
</Fragment>
))}
</div>

<Heading level={2}>自動テスト</Heading>
<MochaTest />
</div>
);
}

interface SampleConfig {
repl: boolean;
replInitContent?: string; // ReplOutput[] ではない。stringのパースはruntimeが行う
editor: Record<string, string> | false;
exec: string[] | false;
}
const sampleConfig: Record<RuntimeLang, SampleConfig> = {
python: {
repl: true,
replInitContent: '>>> print("Hello, World!")\nHello, World!',
editor: {
"main.py": 'print("Hello, World!")',
},
exec: ["main.py"],
},
ruby: {
repl: true,
replInitContent: '>> puts "Hello, World!"\nHello, World!',
editor: {
"main.rb": 'puts "Hello, World!"',
},
exec: ["main.rb"],
},
javascript: {
repl: true,
replInitContent: '> console.log("Hello, World!");\nHello, World!',
editor: false,
exec: false,
},
cpp: {
repl: false,
editor: {
"main.cpp": `#include <iostream>
#include "sub.h"

int main() {
std::cout << "Hello, World!" << std::endl;
}`,
"sub.h": ``,
"sub.cpp": ``,
},
exec: ["main.cpp", "sub.cpp"],
},
};
function RuntimeSample({
lang,
config,
}: {
lang: RuntimeLang;
config: SampleConfig;
}) {
return (
<div className="flex flex-col gap-4">
{config.repl && (
<ReplTerminal
terminalId="1"
language={lang}
initContent={config.replInitContent}
/>
)}
{config.editor &&
Object.entries(config.editor).map(([filename, initContent]) => (
<EditorComponent
key={filename}
language={getAceLang(lang)}
filename={filename}
initContent={initContent}
/>
))}
{config.exec && (
<ExecFile filenames={config.exec} language={lang} content="" />
)}
</div>
);
}

function MochaTest() {
const pyodide = usePyodide();
const ruby = useRuby();
const javascript = useJSEval();
const wandboxCpp = useWandbox("cpp");
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
runtimeRef.current = {
python: pyodide,
ruby: ruby,
javascript: javascript,
cpp: wandboxCpp,
};
const { files, writeFile } = useEmbedContext();
const filesRef = useRef<Record<string, string>>({});
filesRef.current = files;
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
"idle"
);

const [searchParams, setSearchParams] = useState<string>("");
useEffect(() => {
setSearchParams(window.location.search);
}, []);
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
"idle"
);
const { writeFile } = useEmbedContext();

const runTest = () => {
setMochaState("running");
Expand All @@ -44,59 +157,56 @@ export default function RuntimeTestPage() {
};

return (
<div className="p-4 mx-auto w-full max-w-200">
<Heading level={1}>Runtime Test Page</Heading>
<div className="border-1 border-transparent translate-x-0">
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
{mochaState === "idle" ? (
<button className="btn btn-primary mt-4" onClick={runTest}>
テストを実行
</button>
) : mochaState === "running" ? (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
fill="none"
viewBox="0 0 24 24"
></svg>
テストを実行中です...
</div>
) : (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-info h-6 w-6 shrink-0"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
テストが完了しました
</div>
<div className="border-1 border-transparent translate-x-0">
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
{mochaState === "idle" ? (
<button className="btn btn-primary mt-4" onClick={runTest}>
テストを実行
</button>
) : mochaState === "running" ? (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
fill="none"
viewBox="0 0 24 24"
></svg>
テストを実行中です...
</div>
) : (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-info h-6 w-6 shrink-0"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
テストが完了しました
</div>
)}
<p className="mt-8">
{new URLSearchParams(searchParams).has("grep") && (
<>
一部のテストだけを実行します:
<code className="ml-2 font-mono">
{new URLSearchParams(searchParams).get("grep")}
</code>
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a className="ml-4 link link-info" href="/terminal">
{/* aタグでページをリロードしないと動作しない。 */}
フィルタを解除
</a>
</>
)}
<p className="mt-8">
{new URLSearchParams(searchParams).has("grep") && (
<>
一部のテストだけを実行します:
<code className="ml-2 font-mono">
{new URLSearchParams(searchParams).get("grep")}
</code>
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a className="ml-4 link link-info" href="/terminal">
{/* aタグでページをリロードしないと動作しない。 */}
フィルタを解除
</a>
</>
)}
</p>
<div className="m-0!" id="mocha" />
</div>
</p>
<div className="m-0!" id="mocha" />
</div>
);
}
Loading