feat: add pty monitoring for claude code
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% set active = "pty" %}
|
||||
{% block title %}PTY {{ session_id }} · beaver-gateway{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
/* xterm.js styles inlined from the CDN bundle — keep them tight and
|
||||
constrained to the terminal block so they don't leak into the rest
|
||||
of the admin UI. */
|
||||
.term-wrap {
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.term-host {
|
||||
height: 70vh;
|
||||
}
|
||||
.term-host .xterm {
|
||||
height: 100%;
|
||||
}
|
||||
.term-toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.term-toolbar .status {
|
||||
margin-left: auto;
|
||||
font-size: 0.85em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.term-toolbar .status.connected { color: #027a48; }
|
||||
.term-toolbar .status.dropped { color: var(--danger); }
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
||||
|
||||
<h2>PTY <code>{{ session_id }}</code></h2>
|
||||
<p class="muted">
|
||||
Live view of the claude subprocess TUI. Keystrokes you type here go
|
||||
straight into its stdin — Enter, arrows, Ctrl-C, paste, all work.
|
||||
<a href="{{ p }}/pty">← back to list</a>
|
||||
</p>
|
||||
|
||||
<div class="term-toolbar">
|
||||
<button id="reconnect-btn" type="button">Reconnect</button>
|
||||
<button id="enter-btn" type="button" title="Send a single \r — handy if a paste is stuck in the input box">Send Enter</button>
|
||||
<button id="ctrl-c-btn" type="button">Send Ctrl-C</button>
|
||||
<span id="status" class="status">connecting…</span>
|
||||
</div>
|
||||
|
||||
<div class="term-wrap">
|
||||
<div id="term" class="term-host"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const sessionId = {{ session_id|tojson }};
|
||||
const wsBase =
|
||||
(location.protocol === "https:" ? "wss://" : "ws://") +
|
||||
location.host +
|
||||
{{ p|tojson }} +
|
||||
"/pty/" + encodeURIComponent(sessionId) + "/ws";
|
||||
|
||||
const term = new Terminal({
|
||||
fontFamily: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
|
||||
fontSize: 13,
|
||||
theme: {
|
||||
background: "#000000",
|
||||
foreground: "#d0d0d0",
|
||||
},
|
||||
convertEol: false,
|
||||
cursorBlink: true,
|
||||
scrollback: 5000,
|
||||
});
|
||||
const fit = new FitAddon.FitAddon();
|
||||
term.loadAddon(fit);
|
||||
term.open(document.getElementById("term"));
|
||||
// Defer fit until after layout settles; xterm's measurement reads
|
||||
// computed CSS that isn't stable until the page paints once.
|
||||
requestAnimationFrame(() => fit.fit());
|
||||
window.addEventListener("resize", () => fit.fit());
|
||||
|
||||
const statusEl = document.getElementById("status");
|
||||
function setStatus(text, cls) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = "status" + (cls ? " " + cls : "");
|
||||
}
|
||||
|
||||
let ws = null;
|
||||
let manuallyClosed = false;
|
||||
|
||||
function connect() {
|
||||
manuallyClosed = false;
|
||||
setStatus("connecting…", "");
|
||||
ws = new WebSocket(wsBase);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onopen = () => setStatus("connected", "connected");
|
||||
ws.onclose = () => {
|
||||
if (!manuallyClosed) setStatus("disconnected", "dropped");
|
||||
};
|
||||
ws.onerror = () => setStatus("error", "dropped");
|
||||
ws.onmessage = (ev) => {
|
||||
if (typeof ev.data === "string") {
|
||||
term.write(ev.data);
|
||||
} else {
|
||||
// ArrayBuffer — feed raw bytes to xterm. It expects either
|
||||
// string or Uint8Array; the latter preserves byte boundaries
|
||||
// exactly, which matters for ANSI sequences split across frames.
|
||||
term.write(new Uint8Array(ev.data));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Keystrokes -> server. xterm.js gives us the exact byte sequence
|
||||
// the terminal would emit (e.g. Enter -> "\r", arrows -> "\x1b[A"
|
||||
// etc.), so we just pass it through.
|
||||
term.onData((data) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("reconnect-btn").addEventListener("click", () => {
|
||||
if (ws) {
|
||||
manuallyClosed = true;
|
||||
ws.close();
|
||||
}
|
||||
connect();
|
||||
});
|
||||
document.getElementById("enter-btn").addEventListener("click", () => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send("\r");
|
||||
term.focus();
|
||||
});
|
||||
document.getElementById("ctrl-c-btn").addEventListener("click", () => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send("\x03");
|
||||
term.focus();
|
||||
});
|
||||
|
||||
connect();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user