feat: shell terminal, virtual keyboard fixes, settings font slider

- Add shell terminal: new tmux window spawned at project cwd, opened
  from the Terminal icon in the ChatView header; ShellTerminalView lets
  you navigate back to chat or jump to the Claude tmux terminal mode
- Fix system keyboard appearing on first terminal launch: ghostty-web
  calls focus() on a contenteditable div inside open(), triggering iOS
  keyboard before we can suppress it; patch HTMLElement.prototype.focus
  as a no-op for the synchronous duration of terminal.open()
- Add keyboard font-size slider in Settings
- Fix virtual keyboard button corner radius and depth scaling with key height
- Fix keybar gap/padding proportionality via heightScale factor
- Cap keybar font size so it does not grow when bar height is increased

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 18:04:44 -04:00
parent 2ded310472
commit c61712d8c4
15 changed files with 4959 additions and 25 deletions
+75
View File
@@ -0,0 +1,75 @@
import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
import { STORAGE } from '../lib/storage-keys';
import { WebTerminal } from '../lib/terminal/WebTerminal';
import type { TerminalConfig } from '../lib/terminal/terminal-config';
interface TerminalViewProps {
sessionId: string;
adapter?: string;
style?: React.CSSProperties;
}
export interface TerminalViewHandle {
closeKeyboard(): void;
}
function buildTerminalWsUrl(sessionId: string, adapter?: string): string {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const token = localStorage.getItem(STORAGE.TOKEN) ?? '';
const adapterParam = adapter ? `&adapter=${encodeURIComponent(adapter)}` : '';
return `${proto}://${location.host}/terminal/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(token)}${adapterParam}`;
}
const TERMINAL_CONFIG: TerminalConfig = {
fontSize: 14,
};
export const TerminalView = forwardRef<TerminalViewHandle, TerminalViewProps>(
function TerminalView({ sessionId, adapter, style }, ref) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<WebTerminal | null>(null);
useImperativeHandle(ref, () => ({
closeKeyboard() {
termRef.current?.closeKeyboard();
},
}));
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const wsUrl = buildTerminalWsUrl(sessionId, adapter);
let cancelled = false;
WebTerminal.create(el, wsUrl, TERMINAL_CONFIG).then((t) => {
if (cancelled) {
t.dispose();
return;
}
termRef.current = t;
}).catch((err) => {
console.error('[TerminalView] Failed to create terminal:', err);
});
return () => {
cancelled = true;
termRef.current?.dispose();
termRef.current = null;
};
}, [sessionId, adapter]);
return (
<div
ref={containerRef}
style={{
width: '100%',
height: '100%',
backgroundColor: '#000',
overflow: 'hidden',
...style,
}}
/>
);
}
);