feat: 2.14.0 — Eithan terminal toolbar, voice UX, Dross improvements framework
- 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>
This commit is contained in:
@@ -1,21 +1,61 @@
|
||||
// #/terminal — embeds the CT 300 web terminal (ttyd → persistent tmux/claude),
|
||||
// same-origin under /terminal so it shares the Void's CF Access session.
|
||||
// #/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' }, '◆ Terminal'),
|
||||
el('span', { class: 'muted', style: { fontSize: '11px' } }, 'claude @ ct300 · persistent tmux'),
|
||||
el('button', { class: 'ghost', style: { marginLeft: 'auto' }, onclick: () => {
|
||||
const f = document.getElementById('term-frame'); if (f) f.src = f.src;
|
||||
} }, '⟳ Reconnect')
|
||||
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 }, '⟳')
|
||||
)
|
||||
),
|
||||
el('iframe', {
|
||||
id: 'term-frame',
|
||||
src: '/terminal/',
|
||||
class: 'term-frame',
|
||||
allow: 'clipboard-read; clipboard-write'
|
||||
})
|
||||
frame
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user