feat: add codex web terminal

This commit is contained in:
Felipe Coury
2026-05-02 17:16:20 -03:00
parent 35aaa5d9fc
commit 7be5328f34
25 changed files with 2147 additions and 51 deletions
+138
View File
@@ -0,0 +1,138 @@
import { WTerm } from "@wterm/dom";
import { GhosttyCore } from "@wterm/ghostty";
import "./styles.css";
const INPUT_FRAME = 0x00;
const RESIZE_FRAME = 0x01;
function requireElement(id: string): HTMLElement {
const element = document.getElementById(id);
if (!(element instanceof HTMLElement)) {
throw new Error(`Missing ${id} element`);
}
return element;
}
const terminalElement = requireElement("terminal");
const statusElement = requireElement("status");
const encoder = new TextEncoder();
let socket: WebSocket | null = null;
const pendingFrames: Uint8Array[] = [];
function showStatus(message: string): void {
statusElement.textContent = message;
statusElement.classList.remove("hidden");
}
function clearStatus(): void {
statusElement.textContent = "";
statusElement.classList.add("hidden");
}
function encodeInput(data: string): Uint8Array {
const encoded = encoder.encode(data);
const frame = new Uint8Array(1 + encoded.length);
frame[0] = INPUT_FRAME;
frame.set(encoded, 1);
return frame;
}
function encodeResize(cols: number, rows: number): Uint8Array {
const frame = new Uint8Array(5);
const view = new DataView(frame.buffer);
view.setUint8(0, RESIZE_FRAME);
view.setUint16(1, cols, false);
view.setUint16(3, rows, false);
return frame;
}
function sendFrame(frame: Uint8Array): void {
const payload = frame.buffer.slice(
frame.byteOffset,
frame.byteOffset + frame.byteLength,
) as ArrayBuffer;
if (socket?.readyState === WebSocket.OPEN) {
socket.send(payload);
return;
}
pendingFrames.push(new Uint8Array(payload));
}
function flushPendingFrames(): void {
while (socket?.readyState === WebSocket.OPEN && pendingFrames.length > 0) {
const frame = pendingFrames.shift();
if (frame) {
const payload = frame.buffer.slice(
frame.byteOffset,
frame.byteOffset + frame.byteLength,
) as ArrayBuffer;
socket.send(payload);
}
}
}
function websocketUrl(): string {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}/api/pty`;
}
async function main(): Promise<void> {
const core = await GhosttyCore.load({ scrollbackLimit: 10000 });
const term = new WTerm(terminalElement, {
core,
autoResize: true,
cursorBlink: false,
onData(data) {
sendFrame(encodeInput(data));
},
onResize(cols, rows) {
sendFrame(encodeResize(cols, rows));
},
onTitle(title) {
document.title = title
? `${title} - Codex Web Terminal`
: "Codex Web Terminal";
},
});
socket = new WebSocket(websocketUrl());
socket.binaryType = "arraybuffer";
socket.addEventListener("open", () => {
clearStatus();
flushPendingFrames();
term.focus();
});
socket.addEventListener("message", (event) => {
if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data));
return;
}
if (event.data instanceof Blob) {
void event.data.arrayBuffer().then((buffer) => {
term.write(new Uint8Array(buffer));
});
return;
}
term.write(String(event.data));
});
socket.addEventListener("close", () => {
showStatus("Terminal session disconnected.");
});
socket.addEventListener("error", () => {
showStatus("Terminal connection failed.");
});
await term.init();
term.focus();
}
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
showStatus(`Failed to start terminal: ${message}`);
});
+19
View File
@@ -0,0 +1,19 @@
@import "tailwindcss";
@import "@wterm/dom/css";
html,
body {
margin: 0;
min-height: 100%;
}
body {
overflow: hidden;
}
#terminal {
padding: max(env(safe-area-inset-top), 0.5rem)
max(env(safe-area-inset-right), 0.5rem)
max(env(safe-area-inset-bottom), 0.5rem)
max(env(safe-area-inset-left), 0.5rem);
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />