Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/src/routes/room/publicRoom.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ export const publicRoomRouter = new Hono<AppContext>()
const sandbox = getSandbox(ctx.env.SANDBOX, sandboxId, { normalizeId: true });
const cols = parseInt(ctx.req.query('cols') || '80');
const rows = parseInt(ctx.req.query('rows') || '24');
return proxyTerminal(sandbox, 'default', ctx.req.raw, { cols, rows });
// Client picks the PTY session id so it can rotate to a fresh session (and
// thus a fresh shell) after the previous shell exits, instead of endlessly
// reconnecting to a dead session.
const sessionId = ctx.req.query('sessionId') || 'default';
return proxyTerminal(sandbox, sessionId, ctx.req.raw, { cols, rows });
})
.post(
'/preview/start',
Expand Down
20 changes: 16 additions & 4 deletions apps/web/src/components/room/terminal/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const Terminal = () => {
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<XTerminal | null>(null);
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
// PTY session id. Rotated whenever the backing shell exits (e.g. you exit the
// shell with Ctrl-D) so we spawn a fresh shell instead of endlessly
// reconnecting to a now-dead session.
const [sessionId, setSessionId] = useState('default');

useEffect(() => {
if (!terminalRef.current) return;
Expand Down Expand Up @@ -53,17 +57,25 @@ export const Terminal = () => {
const sandboxAddon = new SandboxAddon({
getWebSocketUrl: () => {
const wsUrl = API_URL.replace(/^http/, 'ws');
return `${wsUrl}/rooms/${roomId}/public/terminal`;
return `${wsUrl}/rooms/${roomId}/public/terminal?sessionId=${encodeURIComponent(sessionId)}`;
},
reconnect: true,
onStateChange: (state) => setConnectionState(state),
onStateChange: (state, error) => {
setConnectionState(state);
// The shell process ended (e.g. you exited the shell with Ctrl-D).
// Reconnecting to the same session just loops, so start a fresh one,
// which remounts this effect and spawns a new shell.
if (error && /exited/i.test(error.message)) {
setSessionId(`default-${crypto.randomUUID()}`);
}
},
});
term.loadAddon(sandboxAddon);

term.open(terminalRef.current);
fitAddon.fit();

sandboxAddon.connect({ sandboxId: `s_${roomId}` });
sandboxAddon.connect({ sandboxId: `s_${roomId}`, sessionId });

// Register programmatic input method for Run button
terminalInputRef.current = (cmd: string) => {
Expand All @@ -82,7 +94,7 @@ export const Terminal = () => {
term.dispose();
xtermRef.current = null;
};
}, [roomId, terminalInputRef]);
}, [roomId, terminalInputRef, sessionId]);

return (
<div className='relative h-full w-full'>
Expand Down
Loading