Add interactive PTY Chat tab with xterm.js terminal emulator
Some checks failed
CI / lint-and-test (push) Successful in 33s
CI / build (push) Has been cancelled
Deploy Production / deploy (push) Has been cancelled

Browser-based interactive terminal sessions with agent CLIs via
WebSocket + node-pty. Supports full TUI rendering (colors, cursor,
ctrl-c) through xterm.js in the browser.

Architecture: xterm.js ←WebSocket→ pty-server.js ←PTY→ agent CLI

- Extract shared buildAgentEnv() from executor into agent-env.ts
- Add internal /api/agents/[id]/env endpoint for PTY server
- Add pty-server.js (WebSocket + node-pty, max 3 sessions, 2hr cleanup)
- Add custom server.js wrapping Next.js with WebSocket upgrade
- Add ChatTab component with agent selector and terminal
- Wire CHAT tab into dashboard nav and render
- Configure serverExternalPackages for node-pty
- Update Dockerfile with build tools and custom server
- Bump k8s memory limit 1Gi → 2Gi for PTY sessions
This commit is contained in:
Julia McGhee
2026-03-21 20:42:58 +00:00
parent f45fa64855
commit 7bb091d4b3
12 changed files with 672 additions and 38 deletions

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { getAgentConfig } from "@/lib/agents";
import { buildAgentEnv, buildInteractiveCommand } from "@/lib/agent-env";
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
// Only allow localhost access
const forwarded = _request.headers.get("x-forwarded-for");
const host = _request.headers.get("host") ?? "";
const isLocal =
!forwarded &&
(host.startsWith("localhost") || host.startsWith("127.0.0.1"));
if (!isLocal) {
return NextResponse.json({ error: "forbidden" }, { status: 403 });
}
const { id } = await params;
const config = await getAgentConfig(id);
if (!config) {
return NextResponse.json({ error: "agent not found" }, { status: 404 });
}
const env = await buildAgentEnv(config);
const { command, args } = buildInteractiveCommand(config);
// Strip process.env inherited values — only send what we explicitly set
const cleanEnv: Record<string, string> = {};
for (const [k, v] of Object.entries(env)) {
if (v !== undefined) cleanEnv[k] = v;
}
return NextResponse.json({
command,
args,
env: cleanEnv,
workDir: process.env.HARNESS_WORK_DIR || "/data/harness",
});
}