Skip to content

Commit 6f89987

Browse files
committed
gen: (GPT-5-Codex) あなたは Google の Distinguished Engineer です。
CLI ツールを外部プロセスで実行して、その出力を Streamlit アプリからモニタリングするアプリを #file:codex_cli_runner.py に実装してください。以下の仕様を満たしてください。 - CLI の実行コマンドは sidebar から入力することができる - 外部プロセスの標準出力、標準エラー出力をモニタリングする。(エラーの場合は色分けされていると良い)
1 parent 883f65d commit 6f89987

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Simple Streamlit runner for executing shell commands and tailing their output."""
2+
3+
from __future__ import annotations
4+
5+
import html
6+
import subprocess
7+
import threading
8+
import time
9+
from datetime import datetime
10+
from queue import Empty, Queue
11+
12+
import streamlit as st
13+
14+
LogEntry = tuple[str, str]
15+
16+
17+
LOG_COLORS = {
18+
"stdout": "#e8f5e9",
19+
"stderr": "#ff7961",
20+
}
21+
LOG_FONT_FAMILY = "SFMono-Regular,Consolas,Menlo,monospace"
22+
MAX_LOG_LINES = 4000
23+
24+
25+
def _init_state() -> None:
26+
if "cli_runner" not in st.session_state:
27+
st.session_state.cli_runner = {
28+
"command": "",
29+
"process": None,
30+
"queue": Queue(),
31+
"logs": [],
32+
"start_time": None,
33+
"returncode": None,
34+
"auto_refresh": True,
35+
}
36+
37+
38+
def _stream_reader(stream, label: str, buffer: Queue) -> None:
39+
for raw_line in iter(stream.readline, ""):
40+
buffer.put((label, raw_line.rstrip("\n")))
41+
stream.close()
42+
43+
44+
def _start_process(command: str) -> None:
45+
runner = st.session_state.cli_runner
46+
if runner.get("process") and runner["process"].poll() is None:
47+
st.sidebar.warning("他のプロセスが実行中です。停止してから再度実行してください。")
48+
return
49+
50+
try:
51+
process = subprocess.Popen(
52+
command,
53+
shell=True,
54+
stdout=subprocess.PIPE,
55+
stderr=subprocess.PIPE,
56+
text=True,
57+
bufsize=1,
58+
universal_newlines=True,
59+
)
60+
except OSError as exc:
61+
st.sidebar.error(f"プロセスを起動できませんでした: {exc}")
62+
return
63+
64+
runner.update(
65+
{
66+
"command": command,
67+
"process": process,
68+
"queue": Queue(),
69+
"logs": [],
70+
"start_time": datetime.now(),
71+
"returncode": None,
72+
}
73+
)
74+
75+
if process.stdout:
76+
stdout_thread = threading.Thread(
77+
target=_stream_reader,
78+
args=(process.stdout, "stdout", runner["queue"]),
79+
daemon=True,
80+
)
81+
stdout_thread.start()
82+
83+
if process.stderr:
84+
stderr_thread = threading.Thread(
85+
target=_stream_reader,
86+
args=(process.stderr, "stderr", runner["queue"]),
87+
daemon=True,
88+
)
89+
stderr_thread.start()
90+
91+
92+
def _drain_queue() -> None:
93+
runner = st.session_state.cli_runner
94+
queue: Queue | None = runner.get("queue")
95+
if queue is None:
96+
return
97+
logs: list[LogEntry] = runner.get("logs", [])
98+
99+
while True:
100+
try:
101+
entry = queue.get_nowait()
102+
except Empty:
103+
break
104+
logs.append(entry)
105+
if len(logs) > MAX_LOG_LINES:
106+
logs.pop(0)
107+
108+
runner["logs"] = logs
109+
110+
111+
def _terminate_process() -> None:
112+
runner = st.session_state.cli_runner
113+
process = runner.get("process")
114+
if not process or process.poll() is not None:
115+
return
116+
117+
process.terminate()
118+
try:
119+
process.wait(timeout=5)
120+
except subprocess.TimeoutExpired:
121+
process.kill()
122+
runner["returncode"] = process.poll()
123+
124+
125+
def _render_logs(logs: list[LogEntry]) -> None:
126+
if not logs:
127+
st.info("まだ出力はありません。コマンドを実行してください。")
128+
return
129+
130+
log_container = st.container()
131+
lines = []
132+
for stream_label, line in logs:
133+
color = LOG_COLORS.get(stream_label, "#e0e0e0")
134+
safe_line = html.escape(line)
135+
lines.append(
136+
f"<div style='color:{color};font-family:{LOG_FONT_FAMILY};white-space:pre-wrap;margin:0;'>{safe_line}</div>"
137+
)
138+
log_container.markdown("\n".join(lines), unsafe_allow_html=True)
139+
140+
141+
_init_state()
142+
runner = st.session_state.cli_runner
143+
process = runner.get("process")
144+
is_running = bool(process) and process.poll() is None
145+
146+
st.title("Codex CLI Runner")
147+
148+
with st.sidebar:
149+
st.header("Command Settings")
150+
st.text("CLIコマンドを指定して実行します。")
151+
st.session_state.cli_runner["command"] = st.text_input(
152+
label="Command",
153+
key="cli_runner_command_input",
154+
value=runner.get("command", ""),
155+
placeholder="e.g. ls -la",
156+
)
157+
158+
run_clicked = st.button("Run", use_container_width=True)
159+
stop_clicked = st.button(
160+
"Stop",
161+
use_container_width=True,
162+
disabled=not is_running,
163+
)
164+
165+
runner["auto_refresh"] = st.checkbox(
166+
label="Auto refresh (1s)",
167+
value=runner.get("auto_refresh", True),
168+
)
169+
170+
if st.button("Clear Logs", use_container_width=True):
171+
runner["logs"] = []
172+
173+
if run_clicked:
174+
command = st.session_state.cli_runner.get("command", "").strip()
175+
if not command:
176+
st.sidebar.error("コマンドを入力してください。")
177+
else:
178+
_start_process(command)
179+
180+
if stop_clicked:
181+
_terminate_process()
182+
183+
_drain_queue()
184+
185+
process = runner.get("process")
186+
is_running = bool(process) and process.poll() is None
187+
188+
if process and not is_running and runner.get("returncode") is None:
189+
runner["returncode"] = process.poll()
190+
191+
status_placeholder = st.empty()
192+
if is_running:
193+
status_placeholder.info("プロセス実行中です…")
194+
else:
195+
return_code = runner.get("returncode")
196+
if return_code is None:
197+
status_placeholder.info("プロセスを待機しています。")
198+
elif return_code == 0:
199+
status_placeholder.success("プロセスが正常終了しました。")
200+
else:
201+
status_placeholder.error(f"プロセスが終了コード {return_code} で終了しました。")
202+
203+
if runner.get("start_time"):
204+
if is_running:
205+
elapsed = datetime.now() - runner["start_time"]
206+
st.caption(f"Started at {runner['start_time'].strftime('%Y-%m-%d %H:%M:%S')} | Elapsed: {elapsed}")
207+
else:
208+
st.caption(f"Started at {runner['start_time'].strftime('%Y-%m-%d %H:%M:%S')}")
209+
210+
_render_logs(runner.get("logs", []))
211+
212+
if is_running and runner.get("auto_refresh", True):
213+
time.sleep(1)
214+
st.experimental_rerun()

0 commit comments

Comments
 (0)