mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 19:05:18 +00:00
feat: improve inspector UI for processes and fix PTY terminal
- Simplify ProcessRunTab layout: compact form with collapsible Advanced section for timeout/maxOutputBytes - Rewrite ProcessesTab: collapsible create form, lightweight list items with status dots, clean detail panel with tabs - Extract error details: use problem.detail instead of generic "Stream Error" title for better error messages - Fix GhosttyTerminal binary frame parsing: handle server's binary ArrayBuffer control frames (ready/exit/error) - Enable WebSocket proxying in Vite dev server with ws: true - Set TERM=xterm-256color default for TTY processes so tools like tmux, vim, htop work out of the box - Remove orange gradient background from terminal container for cleaner look - Remove orange left border from selected process list items - Update inspector CSS with new process/terminal styles Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c3a95c3611
commit
6dbc871db9
31 changed files with 6881 additions and 207 deletions
|
|
@ -0,0 +1,165 @@
|
|||
import { ChevronDown, ChevronRight, Loader2, Play } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { SandboxAgentError } from "sandbox-agent";
|
||||
import type { ProcessRunResponse, SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const parseArgs = (value: string): string[] => value.split("\n").map((part) => part.trim()).filter(Boolean);
|
||||
|
||||
const ProcessRunTab = ({
|
||||
getClient,
|
||||
}: {
|
||||
getClient: () => SandboxAgent;
|
||||
}) => {
|
||||
const [command, setCommand] = useState("");
|
||||
const [argsText, setArgsText] = useState("");
|
||||
const [cwd, setCwd] = useState("");
|
||||
const [timeoutMs, setTimeoutMs] = useState("30000");
|
||||
const [maxOutputBytes, setMaxOutputBytes] = useState("");
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<ProcessRunResponse | null>(null);
|
||||
|
||||
const handleRun = async () => {
|
||||
const trimmedCommand = command.trim();
|
||||
if (!trimmedCommand) {
|
||||
setError("Command is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
setRunning(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await getClient().runProcess({
|
||||
command: trimmedCommand,
|
||||
args: parseArgs(argsText),
|
||||
cwd: cwd.trim() || undefined,
|
||||
timeoutMs: timeoutMs.trim() ? Number(timeoutMs) : undefined,
|
||||
maxOutputBytes: maxOutputBytes.trim() ? Number(maxOutputBytes) : undefined,
|
||||
});
|
||||
setResult(response);
|
||||
} catch (runError) {
|
||||
const detail = runError instanceof SandboxAgentError ? runError.problem?.detail : undefined;
|
||||
setError(detail || (runError instanceof Error ? runError.message : "Unable to run process."));
|
||||
setResult(null);
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="process-run-container">
|
||||
<div className="process-run-form">
|
||||
<div className="process-run-row">
|
||||
<div className="process-run-field process-run-field-grow">
|
||||
<label className="label">Command</label>
|
||||
<input
|
||||
className="setup-input mono"
|
||||
value={command}
|
||||
onChange={(event) => {
|
||||
setCommand(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="bash"
|
||||
/>
|
||||
</div>
|
||||
<div className="process-run-field process-run-field-grow">
|
||||
<label className="label">Working Directory</label>
|
||||
<input
|
||||
className="setup-input mono"
|
||||
value={cwd}
|
||||
onChange={(event) => {
|
||||
setCwd(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="/workspace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="process-run-field">
|
||||
<label className="label">Arguments</label>
|
||||
<textarea
|
||||
className="setup-input mono"
|
||||
rows={2}
|
||||
value={argsText}
|
||||
onChange={(event) => {
|
||||
setArgsText(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder={"One argument per line, e.g.\n-lc"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="process-advanced-toggle"
|
||||
onClick={() => setShowAdvanced((prev) => !prev)}
|
||||
type="button"
|
||||
>
|
||||
{showAdvanced ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
Advanced
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="process-run-row">
|
||||
<div className="process-run-field process-run-field-grow">
|
||||
<label className="label">Timeout (ms)</label>
|
||||
<input
|
||||
className="setup-input mono"
|
||||
value={timeoutMs}
|
||||
onChange={(event) => {
|
||||
setTimeoutMs(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="30000"
|
||||
/>
|
||||
</div>
|
||||
<div className="process-run-field process-run-field-grow">
|
||||
<label className="label">Max Output Bytes</label>
|
||||
<input
|
||||
className="setup-input mono"
|
||||
value={maxOutputBytes}
|
||||
onChange={(event) => {
|
||||
setMaxOutputBytes(event.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
placeholder="Default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error ? <div className="banner error">{error}</div> : null}
|
||||
|
||||
<button className="button primary small" onClick={() => void handleRun()} disabled={running} style={{ alignSelf: "flex-start" }}>
|
||||
{running ? <Loader2 className="button-icon spinner-icon" /> : <Play className="button-icon" />}
|
||||
{running ? "Running..." : "Run"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{result ? (
|
||||
<div className="process-run-result">
|
||||
<div className="process-run-result-header">
|
||||
<span className={`pill ${result.timedOut ? "warning" : result.exitCode === 0 ? "success" : "danger"}`}>
|
||||
{result.timedOut ? "Timed Out" : `exit ${result.exitCode ?? "?"}`}
|
||||
</span>
|
||||
<span className="card-meta">{result.durationMs}ms</span>
|
||||
</div>
|
||||
|
||||
<div className="process-run-output">
|
||||
<div className="process-run-output-section">
|
||||
<div className="process-run-output-label">stdout{result.stdoutTruncated ? " (truncated)" : ""}</div>
|
||||
<pre className="process-log-block">{result.stdout || "(empty)"}</pre>
|
||||
</div>
|
||||
<div className="process-run-output-section">
|
||||
<div className="process-run-output-label">stderr{result.stderrTruncated ? " (truncated)" : ""}</div>
|
||||
<pre className="process-log-block">{result.stderr || "(empty)"}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessRunTab;
|
||||
Loading…
Add table
Add a link
Reference in a new issue