1/**
2 * The terminal SPA, served inline by the Worker so `alchemy dev` is a single
3 * command with no separate front-end build. It is a hand-rolled terminal (no
4 * xterm.js): a scrollback pane + a prompt input that opens a WebSocket to the
5 * session Durable Object, sends each command, and appends streamed output as
6 * it arrives.
7 */
8export const TERMINAL_HTML = `<!doctype html>
9<html lang="en">
10<head>
11<meta charset="utf-8" />
12<meta name="viewport" content="width=device-width, initial-scale=1" />
13<title>MicroVM Shell</title>
14<style>
15 :root { color-scheme: dark; }
16 * { box-sizing: border-box; }
17 body {
18 margin: 0; height: 100vh; display: flex; flex-direction: column;
19 background: #0b0e14; color: #d7dce2;
20 font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
21 }
22 header {
23 padding: 10px 16px; border-bottom: 1px solid #1c2230;
24 color: #7dcfff; letter-spacing: .04em; font-weight: 600;
25 display: flex; justify-content: space-between; align-items: center;
26 }
27 header .status { font-weight: 400; color: #565f73; }
28 header .status.live { color: #9ece6a; }
29 #screen {
30 flex: 1; overflow-y: auto; padding: 16px; white-space: pre-wrap;
31 word-break: break-word;
32 }
33 #screen .cmd { color: #7dcfff; }
34 #screen .sys { color: #565f73; }
35 #screen .exit { color: #e0af68; }
36 form { display: flex; border-top: 1px solid #1c2230; }
37 .prompt { padding: 12px 8px 12px 16px; color: #9ece6a; }
38 input {
39 flex: 1; background: transparent; border: 0; outline: 0;
40 color: #d7dce2; font: inherit; padding: 12px 16px 12px 0;
41 }
42</style>
43</head>
44<body>
45<header>
46 <span>microvm@shell</span>
47 <span class="status" id="status">connecting…</span>
48</header>
49<div id="screen"></div>
50<form id="form">
51 <span class="prompt">$</span>
52 <input id="input" autocomplete="off" autofocus placeholder="type a command, e.g. uname -a" />
53</form>
54<script type="module">
55 const screen = document.getElementById("screen");
56 const input = document.getElementById("input");
57 const form = document.getElementById("form");
58 const status = document.getElementById("status");
59
60 const append = (text, cls) => {
61 const span = document.createElement("span");
62 if (cls) span.className = cls;
63 span.textContent = text;
64 screen.appendChild(span);
65 screen.scrollTop = screen.scrollHeight;
66 };
67
68 const sessionId = Math.random().toString(36).slice(2, 10);
69 const wsUrl = new URL(location.href);
70 wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
71 wsUrl.pathname = "/session/" + sessionId + "/ws";
72 const ws = new WebSocket(wsUrl);
73
74 ws.addEventListener("open", () => {
75 status.textContent = "● live";
76 status.className = "status live";
77 input.focus();
78 });
79 ws.addEventListener("message", (e) => append(e.data));
80 ws.addEventListener("close", () => {
81 status.textContent = "closed";
82 status.className = "status";
83 append("[connection closed]\\n", "sys");
84 });
85 ws.addEventListener("error", () => append("[socket error]\\n", "sys"));
86
87 form.addEventListener("submit", (e) => {
88 e.preventDefault();
89 const command = input.value;
90 if (!command.trim()) return;
91 append("$ " + command + "\\n", "cmd");
92 ws.send(command);
93 input.value = "";
94 });
95</script>
96</body>
97</html>`;