feat(web): wire terminal panel to live process websocket
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -135,7 +135,7 @@ export function WorkspaceShell({ workspaceId }: WorkspaceShellProps) {
|
||||
<>
|
||||
<PanelResizeHandle className="h-1 bg-border transition-colors hover:bg-accent" />
|
||||
<Panel defaultSize={30} minSize={10} maxSize={70}>
|
||||
<TerminalPanel />
|
||||
<TerminalPanel workspaceId={workspaceId} />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,10 +2,23 @@ import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function TerminalPanel() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
import { useProcessWebSocket } from "@/lib/api/process";
|
||||
|
||||
interface TerminalPanelProps {
|
||||
workspaceId: string | null;
|
||||
}
|
||||
|
||||
export function TerminalPanel({ workspaceId }: TerminalPanelProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const terminalRef = useRef<Terminal | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const hasWelcomedRef = useRef(false);
|
||||
const isDeadRef = useRef(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [isDead, setIsDead] = useState(false);
|
||||
const { status, send, onData } = useProcessWebSocket(workspaceId);
|
||||
|
||||
// Initialize xterm once and keep it alive across WS status changes.
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
@@ -28,12 +41,8 @@ export function TerminalPanel() {
|
||||
fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(container);
|
||||
terminal.writeln("codespace terminal");
|
||||
terminal.writeln("Mock session initialized. No process is attached.");
|
||||
terminal.writeln("");
|
||||
terminal.writeln("$ pnpm test");
|
||||
terminal.writeln("mock: 3 files checked, 0 failures");
|
||||
terminal.write("$ ");
|
||||
terminalRef.current = terminal;
|
||||
fitAddonRef.current = fitAddon;
|
||||
} catch {
|
||||
setFailed(true);
|
||||
terminal?.dispose();
|
||||
@@ -72,9 +81,57 @@ export function TerminalPanel() {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
terminal?.dispose();
|
||||
fitAddon?.dispose();
|
||||
terminalRef.current = null;
|
||||
fitAddonRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reset per-workspace state and wire the active socket to xterm.
|
||||
useEffect(() => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal || !workspaceId) return;
|
||||
|
||||
hasWelcomedRef.current = false;
|
||||
isDeadRef.current = false;
|
||||
setIsDead(false);
|
||||
|
||||
const removeInputHandler = terminal.onData((input) => {
|
||||
if (!isDeadRef.current) {
|
||||
send(input);
|
||||
}
|
||||
}).dispose;
|
||||
|
||||
const unsubscribeData = onData((chunk) => {
|
||||
terminal.write(chunk);
|
||||
if (chunk.startsWith("[process exited")) {
|
||||
isDeadRef.current = true;
|
||||
setIsDead(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeInputHandler();
|
||||
unsubscribeData();
|
||||
};
|
||||
}, [workspaceId, send, onData]);
|
||||
|
||||
// Print a one-time welcome banner the first time the socket opens.
|
||||
useEffect(() => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal || !workspaceId || status !== "open") return;
|
||||
if (hasWelcomedRef.current) return;
|
||||
hasWelcomedRef.current = true;
|
||||
terminal.write(`\r\n[connected to ${workspaceId}]\r\n$ `);
|
||||
}, [status, workspaceId]);
|
||||
|
||||
if (!workspaceId) {
|
||||
return (
|
||||
<section className="flex h-full w-full items-center justify-center bg-background text-sm text-muted-foreground">
|
||||
Select a workspace to use the terminal.
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
return (
|
||||
<section className="flex h-full w-full items-center justify-center bg-background text-sm text-muted-foreground">
|
||||
@@ -83,9 +140,22 @@ export function TerminalPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
const statusHint = isDead
|
||||
? "process exited — use the toolbar to start"
|
||||
: status === "error"
|
||||
? "terminal disconnected"
|
||||
: status === "connecting"
|
||||
? "connecting…"
|
||||
: null;
|
||||
|
||||
return (
|
||||
<section className="h-full w-full overflow-hidden bg-[#171717] p-2">
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
<section className="flex h-full w-full flex-col overflow-hidden bg-[#171717]">
|
||||
{statusHint && (
|
||||
<div className="flex h-6 shrink-0 items-center px-2 text-[11px] text-muted-foreground">
|
||||
{statusHint}
|
||||
</div>
|
||||
)}
|
||||
<div ref={containerRef} className="min-h-0 flex-1 p-2" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user