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:
root
2026-06-11 23:35:32 +10:00
parent 859dedb668
commit 3bd8ea399c
18 changed files with 338 additions and 23 deletions

View File

@@ -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
);
}