Bjo53 commited on
Commit
b6bea82
Β·
verified Β·
1 Parent(s): f2a4f07

Restore original UI + add persistence

Browse files
Files changed (1) hide show
  1. static/index.html +122 -18
static/index.html CHANGED
@@ -5,17 +5,20 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
6
  <title>OpenCode AI Terminal</title>
7
 
 
8
  <meta name="theme-color" content="#27c93f"/>
9
  <meta name="apple-mobile-web-app-capable" content="yes"/>
10
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
11
  <meta name="apple-mobile-web-app-title" content="OpenCode"/>
12
- <meta name="description" content="Persistent web terminal"/>
13
 
 
14
  <link rel="manifest" href="/static/manifest.json"/>
15
  <link rel="apple-touch-icon" href="/static/icon-192.png"/>
16
  <link rel="icon" type="image/png" sizes="192x192" href="/static/icon-192.png"/>
17
  <link rel="icon" type="image/png" sizes="512x512" href="/static/icon-512.png"/>
18
 
 
19
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
20
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
21
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
@@ -42,6 +45,19 @@
42
  }
43
  #pass-screen .err { color: #ff5f56; font-size: 12px; }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  #status-bar {
46
  display: none; background: #161b22; padding: 6px 12px; border-bottom: 1px solid #30363d;
47
  gap: 10px; align-items: center; font-size: 11px;
@@ -55,8 +71,7 @@
55
  #status-dot.reconnecting { background: #ffbd2e; animation: pulse 1s infinite; }
56
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
57
  #status-text { color: #768390; }
58
- #session-id { color: #58a6ff; margin-left: auto; }
59
- #pkg-count { color: #27c93f; }
60
 
61
  #offline-banner {
62
  display: none; background: #ff5f56; color: #fff; text-align: center;
@@ -64,7 +79,20 @@
64
  }
65
  #offline-banner.visible { display: block; }
66
 
67
- #terminal-container { width: 100%; height: calc(100% - 32px); display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  .xterm { padding: 4px; height: 100%; }
69
 
70
  #ctx-menu {
@@ -87,13 +115,28 @@
87
  <div class="err" id="pass-err"></div>
88
  </div>
89
 
90
- <div id="offline-banner">&#x26A0; OFFLINE &mdash; Reconnecting automatically...</div>
 
 
 
 
 
 
 
 
91
 
92
  <div id="status-bar">
93
  <div id="status-dot"></div>
94
  <span id="status-text">Connecting...</span>
95
- <span id="pkg-count"></span>
96
- <span id="session-id"></span>
 
 
 
 
 
 
 
97
  </div>
98
 
99
  <div id="terminal-container"></div>
@@ -106,6 +149,40 @@
106
  </div>
107
 
108
  <script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  const SESSION_KEY = "opencode_session_id";
110
  let PASS_HASH = "";
111
  let term = null;
@@ -124,6 +201,7 @@
124
  if (val === PASS_HASH && PASS_HASH !== "") {
125
  document.getElementById("pass-screen").classList.add("hidden");
126
  document.getElementById("status-bar").classList.add("active");
 
127
  document.getElementById("terminal-container").style.display = "block";
128
  initTerminal();
129
  } else {
@@ -204,6 +282,8 @@
204
  setStatus("connected", "Connected");
205
  const saved = getSavedSession();
206
  ws.send(JSON.stringify({ type: "attach", session_id: saved || undefined }));
 
 
207
  };
208
 
209
  ws.onmessage = (e) => {
@@ -212,7 +292,7 @@
212
  const msg = JSON.parse(e.data);
213
  if (msg.type === "session_info") {
214
  saveSession(msg.session_id);
215
- document.getElementById("session-id").textContent = `Session: ${msg.session_id}`;
216
  } else if (msg.type === "output") {
217
  term.write(msg.data);
218
  }
@@ -260,13 +340,26 @@
260
  });
261
 
262
  connect();
263
-
264
- // Fetch package count
265
- fetch("/api/sessions").then(r => r.json()).then(d => {
266
- document.getElementById("pkg-count").textContent = `${d.length} sessions`;
267
- }).catch(() => {});
268
  }
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  // Context menu
271
  const ctxMenu = document.getElementById("ctx-menu");
272
  document.getElementById("terminal-container").addEventListener("contextmenu", (e) => {
@@ -275,20 +368,25 @@
275
  ctxMenu.style.left = `${e.pageX}px`;
276
  ctxMenu.style.top = `${e.pageY}px`;
277
  });
 
278
  document.addEventListener("click", () => { ctxMenu.style.display = "none"; });
279
 
280
  document.getElementById("ctx-copy").onclick = () => {
281
- const sel = term?.getSelection();
282
  if (sel) navigator.clipboard.writeText(sel);
283
  };
 
284
  document.getElementById("ctx-paste").onclick = async () => {
285
  try {
286
  const text = await navigator.clipboard.readText();
287
- if (text && ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "input", data: text }));
 
 
288
  } catch(e) {}
289
  };
290
- document.getElementById("ctx-selectall").onclick = () => { term?.selectAll(); };
291
- document.getElementById("ctx-clear").onclick = () => { term?.clear(); };
 
292
 
293
  // Keyboard shortcuts
294
  document.addEventListener("keydown", (e) => {
@@ -300,9 +398,15 @@
300
  if (e.ctrlKey && e.key === "v") {
301
  e.preventDefault();
302
  navigator.clipboard.readText().then(text => {
303
- if (text && ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "input", data: text }));
 
 
304
  });
305
  }
 
 
 
 
306
  });
307
  </script>
308
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
6
  <title>OpenCode AI Terminal</title>
7
 
8
+ <!-- PWA Meta -->
9
  <meta name="theme-color" content="#27c93f"/>
10
  <meta name="apple-mobile-web-app-capable" content="yes"/>
11
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
12
  <meta name="apple-mobile-web-app-title" content="OpenCode"/>
13
+ <meta name="description" content="Full screen web terminal"/>
14
 
15
+ <!-- PWA Links -->
16
  <link rel="manifest" href="/static/manifest.json"/>
17
  <link rel="apple-touch-icon" href="/static/icon-192.png"/>
18
  <link rel="icon" type="image/png" sizes="192x192" href="/static/icon-192.png"/>
19
  <link rel="icon" type="image/png" sizes="512x512" href="/static/icon-512.png"/>
20
 
21
+ <!-- Fonts & Terminal -->
22
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
23
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
24
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
 
45
  }
46
  #pass-screen .err { color: #ff5f56; font-size: 12px; }
47
 
48
+ #install-banner {
49
+ display: none; position: fixed; bottom: 0; left: 0; width: 100%;
50
+ background: #161b22; border-top: 1px solid #30363d; padding: 12px 16px;
51
+ z-index: 10000; flex-direction: row; align-items: center; justify-content: space-between;
52
+ }
53
+ #install-banner.show { display: flex; }
54
+ #install-banner span { color: #e6edf3; font-size: 13px; }
55
+ #install-banner button {
56
+ background: #27c93f; color: #000; border: none; padding: 8px 20px;
57
+ font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: bold;
58
+ }
59
+ #install-banner .close { background: none; color: #768390; padding: 8px; }
60
+
61
  #status-bar {
62
  display: none; background: #161b22; padding: 6px 12px; border-bottom: 1px solid #30363d;
63
  gap: 10px; align-items: center; font-size: 11px;
 
71
  #status-dot.reconnecting { background: #ffbd2e; animation: pulse 1s infinite; }
72
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
73
  #status-text { color: #768390; }
74
+ #session-info { color: #58a6ff; margin-left: auto; }
 
75
 
76
  #offline-banner {
77
  display: none; background: #ff5f56; color: #fff; text-align: center;
 
79
  }
80
  #offline-banner.visible { display: block; }
81
 
82
+ #toolbar {
83
+ display: none; background: #161b22; padding: 6px 12px; border-bottom: 1px solid #30363d;
84
+ gap: 8px; align-items: center;
85
+ }
86
+ #toolbar.active { display: flex; }
87
+ #toolbar button {
88
+ background: #27c93f22; border: 1px solid #27c93f55; border-radius: 6px;
89
+ color: #27c93f; font-size: 11px; padding: 5px 12px; cursor: pointer;
90
+ font-family: 'JetBrains Mono', monospace;
91
+ }
92
+ #toolbar button:hover { background: #27c93f44; }
93
+ #toolbar .sep { width: 1px; height: 20px; background: #30363d; }
94
+
95
+ #terminal-container { width: 100%; height: calc(100% - 36px); display: none; }
96
  .xterm { padding: 4px; height: 100%; }
97
 
98
  #ctx-menu {
 
115
  <div class="err" id="pass-err"></div>
116
  </div>
117
 
118
+ <div id="install-banner">
119
+ <span>Install OpenCode Terminal</span>
120
+ <div>
121
+ <button id="install-btn">Install</button>
122
+ <button class="close" id="install-close">βœ•</button>
123
+ </div>
124
+ </div>
125
+
126
+ <div id="offline-banner">&#x26A0; OFFLINE β€” Reconnecting automatically...</div>
127
 
128
  <div id="status-bar">
129
  <div id="status-dot"></div>
130
  <span id="status-text">Connecting...</span>
131
+ <span id="session-info"></span>
132
+ </div>
133
+
134
+ <div id="toolbar">
135
+ <button id="btn-copy">πŸ“‹ Copy</button>
136
+ <button id="btn-paste">πŸ“„ Paste</button>
137
+ <button id="btn-selectall">πŸ“ Select All</button>
138
+ <div class="sep"></div>
139
+ <button id="btn-clear">πŸ—‘ Clear</button>
140
  </div>
141
 
142
  <div id="terminal-container"></div>
 
149
  </div>
150
 
151
  <script>
152
+ // PWA Install
153
+ let deferredPrompt;
154
+ const installBanner = document.getElementById('install-banner');
155
+
156
+ window.addEventListener('beforeinstallprompt', (e) => {
157
+ e.preventDefault();
158
+ deferredPrompt = e;
159
+ installBanner.classList.add('show');
160
+ });
161
+
162
+ document.getElementById('install-btn').onclick = async () => {
163
+ if (deferredPrompt) {
164
+ deferredPrompt.prompt();
165
+ const { outcome } = await deferredPrompt.userChoice;
166
+ deferredPrompt = null;
167
+ installBanner.classList.remove('show');
168
+ }
169
+ };
170
+
171
+ document.getElementById('install-close').onclick = () => {
172
+ installBanner.classList.remove('show');
173
+ };
174
+
175
+ window.addEventListener('appinstalled', () => {
176
+ installBanner.classList.remove('show');
177
+ deferredPrompt = null;
178
+ });
179
+
180
+ // Register Service Worker
181
+ if ('serviceWorker' in navigator) {
182
+ navigator.serviceWorker.register('/static/sw.js');
183
+ }
184
+
185
+ // Session persistence
186
  const SESSION_KEY = "opencode_session_id";
187
  let PASS_HASH = "";
188
  let term = null;
 
201
  if (val === PASS_HASH && PASS_HASH !== "") {
202
  document.getElementById("pass-screen").classList.add("hidden");
203
  document.getElementById("status-bar").classList.add("active");
204
+ document.getElementById("toolbar").classList.add("active");
205
  document.getElementById("terminal-container").style.display = "block";
206
  initTerminal();
207
  } else {
 
282
  setStatus("connected", "Connected");
283
  const saved = getSavedSession();
284
  ws.send(JSON.stringify({ type: "attach", session_id: saved || undefined }));
285
+ ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
286
+ term.focus();
287
  };
288
 
289
  ws.onmessage = (e) => {
 
292
  const msg = JSON.parse(e.data);
293
  if (msg.type === "session_info") {
294
  saveSession(msg.session_id);
295
+ document.getElementById("session-info").textContent = `Session: ${msg.session_id}`;
296
  } else if (msg.type === "output") {
297
  term.write(msg.data);
298
  }
 
340
  });
341
 
342
  connect();
 
 
 
 
 
343
  }
344
 
345
+ // Toolbar buttons
346
+ document.getElementById("btn-copy").onclick = () => {
347
+ const sel = term.getSelection();
348
+ if (sel) navigator.clipboard.writeText(sel);
349
+ };
350
+
351
+ document.getElementById("btn-paste").onclick = async () => {
352
+ try {
353
+ const text = await navigator.clipboard.readText();
354
+ if (text && ws && ws.readyState === WebSocket.OPEN) {
355
+ ws.send(JSON.stringify({ type: "input", data: text }));
356
+ }
357
+ } catch(e) {}
358
+ };
359
+
360
+ document.getElementById("btn-selectall").onclick = () => { term.selectAll(); };
361
+ document.getElementById("btn-clear").onclick = () => { term.clear(); };
362
+
363
  // Context menu
364
  const ctxMenu = document.getElementById("ctx-menu");
365
  document.getElementById("terminal-container").addEventListener("contextmenu", (e) => {
 
368
  ctxMenu.style.left = `${e.pageX}px`;
369
  ctxMenu.style.top = `${e.pageY}px`;
370
  });
371
+
372
  document.addEventListener("click", () => { ctxMenu.style.display = "none"; });
373
 
374
  document.getElementById("ctx-copy").onclick = () => {
375
+ const sel = term.getSelection();
376
  if (sel) navigator.clipboard.writeText(sel);
377
  };
378
+
379
  document.getElementById("ctx-paste").onclick = async () => {
380
  try {
381
  const text = await navigator.clipboard.readText();
382
+ if (text && ws && ws.readyState === WebSocket.OPEN) {
383
+ ws.send(JSON.stringify({ type: "input", data: text }));
384
+ }
385
  } catch(e) {}
386
  };
387
+
388
+ document.getElementById("ctx-selectall").onclick = () => { term.selectAll(); };
389
+ document.getElementById("ctx-clear").onclick = () => { term.clear(); };
390
 
391
  // Keyboard shortcuts
392
  document.addEventListener("keydown", (e) => {
 
398
  if (e.ctrlKey && e.key === "v") {
399
  e.preventDefault();
400
  navigator.clipboard.readText().then(text => {
401
+ if (text && ws && ws.readyState === WebSocket.OPEN) {
402
+ ws.send(JSON.stringify({ type: "input", data: text }));
403
+ }
404
  });
405
  }
406
+ if (e.ctrlKey && e.key === "a") {
407
+ e.preventDefault();
408
+ term.selectAll();
409
+ }
410
  });
411
  </script>
412
  </body>