- Terminal renamed Eithan: mobile font A−/A+ (per-URL ttyd opts), same-origin xterm Copy/Paste buttons, scroll-to-live, touch-default 17px - Dross voice: no keyboard pop after transcribe (fine-pointer only focus), autogrow textarea to ~5 lines, live amplitude meter on the mic while recording - Dross improvements: propose_improvement tool (CSS layer, exfil-sanitized, owner-approved, per-improvement rollback/restore), public /improvements.css, Settings panel. External MCP registry unchanged (no tool leak). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.8 KiB
JavaScript
62 lines
2.8 KiB
JavaScript
// #/terminal — "Eithan": the CT 300 web terminal (ttyd → persistent tmux/claude),
|
||
// same-origin under /terminal so it shares the Void's CF Access session AND lets
|
||
// us reach the xterm instance for mobile copy/paste.
|
||
import { el, mount } from '../dom.js';
|
||
|
||
const FS_KEY = 'void_term_fontsize';
|
||
|
||
export async function render(main) {
|
||
// bigger default on touch screens; user-adjustable, remembered
|
||
let fontSize = Number(localStorage.getItem(FS_KEY))
|
||
|| (matchMedia('(pointer: coarse)').matches ? 17 : 14);
|
||
|
||
const frame = el('iframe', {
|
||
id: 'term-frame',
|
||
class: 'term-frame',
|
||
allow: 'clipboard-read; clipboard-write'
|
||
});
|
||
const setSrc = () => { frame.src = `/terminal/?fontSize=${fontSize}`; };
|
||
setSrc();
|
||
|
||
// ttyd exposes its xterm as window.term; the same-origin proxy makes it reachable.
|
||
const term = () => { try { return frame.contentWindow?.term ?? null; } catch { return null; } };
|
||
const note = el('span', { class: 'muted', style: { fontSize: '11px' } },
|
||
'eithan @ ct300 · persistent tmux · swipe to scroll');
|
||
const flash = (msg) => { const old = note.textContent; note.textContent = msg;
|
||
setTimeout(() => { note.textContent = old; }, 1600); };
|
||
|
||
const bump = (d) => {
|
||
fontSize = Math.max(10, Math.min(24, fontSize + d));
|
||
localStorage.setItem(FS_KEY, String(fontSize));
|
||
setSrc(); // reload reattaches tmux; the session itself persists
|
||
};
|
||
|
||
mount(main,
|
||
el('div', { class: 'term-bar' },
|
||
el('span', { class: 'term-title' }, '◆ Eithan'),
|
||
note,
|
||
el('span', { style: { marginLeft: 'auto', display: 'flex', gap: '6px' } },
|
||
el('button', { class: 'ghost', title: 'smaller text', onclick: () => bump(-2) }, 'A−'),
|
||
el('button', { class: 'ghost', title: 'larger text', onclick: () => bump(+2) }, 'A+'),
|
||
el('button', { class: 'ghost', title: 'copy terminal selection', onclick: async () => {
|
||
const sel = term()?.getSelection?.();
|
||
if (!sel) return flash('select text first (touch: long-press, then drag)');
|
||
try { await navigator.clipboard.writeText(sel); flash('copied ✓'); }
|
||
catch { flash('clipboard needs the https domain'); }
|
||
} }, '⧉ Copy'),
|
||
el('button', { class: 'ghost', title: 'paste clipboard into terminal', onclick: async () => {
|
||
const t = term();
|
||
if (!t) return flash('terminal not ready');
|
||
try { t.paste(await navigator.clipboard.readText()); }
|
||
catch { flash('clipboard needs the https domain'); }
|
||
} }, '⇩ Paste'),
|
||
el('button', { class: 'ghost', title: 'jump to live output', onclick: () => {
|
||
term()?.scrollToBottom?.(); frame.contentWindow?.focus();
|
||
} }, '↓ Live'),
|
||
el('button', { class: 'ghost', title: 'reconnect', onclick: setSrc }, '⟳')
|
||
)
|
||
),
|
||
frame
|
||
);
|
||
}
|