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" />
|
<PanelResizeHandle className="h-1 bg-border transition-colors hover:bg-accent" />
|
||||||
<Panel defaultSize={30} minSize={10} maxSize={70}>
|
<Panel defaultSize={30} minSize={10} maxSize={70}>
|
||||||
<TerminalPanel />
|
<TerminalPanel workspaceId={workspaceId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,10 +2,23 @@ import { FitAddon } from "@xterm/addon-fit";
|
|||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export function TerminalPanel() {
|
import { useProcessWebSocket } from "@/lib/api/process";
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [failed, setFailed] = useState(false);
|
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -28,12 +41,8 @@ export function TerminalPanel() {
|
|||||||
fitAddon = new FitAddon();
|
fitAddon = new FitAddon();
|
||||||
terminal.loadAddon(fitAddon);
|
terminal.loadAddon(fitAddon);
|
||||||
terminal.open(container);
|
terminal.open(container);
|
||||||
terminal.writeln("codespace terminal");
|
terminalRef.current = terminal;
|
||||||
terminal.writeln("Mock session initialized. No process is attached.");
|
fitAddonRef.current = fitAddon;
|
||||||
terminal.writeln("");
|
|
||||||
terminal.writeln("$ pnpm test");
|
|
||||||
terminal.writeln("mock: 3 files checked, 0 failures");
|
|
||||||
terminal.write("$ ");
|
|
||||||
} catch {
|
} catch {
|
||||||
setFailed(true);
|
setFailed(true);
|
||||||
terminal?.dispose();
|
terminal?.dispose();
|
||||||
@@ -72,9 +81,57 @@ export function TerminalPanel() {
|
|||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
terminal?.dispose();
|
terminal?.dispose();
|
||||||
fitAddon?.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) {
|
if (failed) {
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full w-full items-center justify-center bg-background text-sm text-muted-foreground">
|
<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 (
|
return (
|
||||||
<section className="h-full w-full overflow-hidden bg-[#171717] p-2">
|
<section className="flex h-full w-full flex-col overflow-hidden bg-[#171717]">
|
||||||
<div ref={containerRef} className="h-full w-full" />
|
{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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user