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:
@@ -0,0 +1,106 @@
|
||||
import * as nodePty from 'node-pty';
|
||||
import type { IPty } from 'node-pty';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
interface PtyEntry {
|
||||
pty: IPty;
|
||||
sessionId: string;
|
||||
windowId: string;
|
||||
dataListeners: Set<(data: string) => void>;
|
||||
exitListeners: Set<() => void>;
|
||||
onDataHandler: (data: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages PTY processes for terminal sessions. Each call to getOrCreate
|
||||
* spawns `tmux attach-session -t clawtap:<windowId>` as a node-pty process,
|
||||
* giving a full bidirectional terminal stream into the existing tmux pane.
|
||||
*
|
||||
* One PTY per (sessionId, clientId) tuple — multiple clients attach
|
||||
* independently via separate tmux attach processes.
|
||||
*/
|
||||
export class PtySessionManager extends EventEmitter {
|
||||
private ptys = new Map<string, PtyEntry>(); // key = `${sessionId}:${clientId}`
|
||||
|
||||
getOrCreate(sessionId: string, windowId: string, clientId: string, cols = 80, rows = 24): PtyEntry {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
const existing = this.ptys.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
const pty = nodePty.spawn('tmux', ['attach-session', '-t', `clawtap:${windowId}`], {
|
||||
name: 'xterm-256color',
|
||||
cols,
|
||||
rows,
|
||||
env: process.env as Record<string, string>,
|
||||
});
|
||||
|
||||
const entry: PtyEntry = {
|
||||
pty,
|
||||
sessionId,
|
||||
windowId,
|
||||
dataListeners: new Set(),
|
||||
exitListeners: new Set(),
|
||||
onDataHandler: () => {},
|
||||
};
|
||||
|
||||
entry.onDataHandler = (data: string) => {
|
||||
for (const cb of entry.dataListeners) cb(data);
|
||||
};
|
||||
|
||||
pty.onData(entry.onDataHandler);
|
||||
pty.onExit(() => {
|
||||
this.ptys.delete(key);
|
||||
for (const cb of entry.exitListeners) cb();
|
||||
});
|
||||
|
||||
this.ptys.set(key, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
onData(sessionId: string, clientId: string, cb: (data: string) => void): () => void {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
const entry = this.ptys.get(key);
|
||||
if (!entry) return () => {};
|
||||
entry.dataListeners.add(cb);
|
||||
return () => entry.dataListeners.delete(cb);
|
||||
}
|
||||
|
||||
onExit(sessionId: string, clientId: string, cb: () => void): () => void {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
const entry = this.ptys.get(key);
|
||||
if (!entry) return () => {};
|
||||
entry.exitListeners.add(cb);
|
||||
return () => entry.exitListeners.delete(cb);
|
||||
}
|
||||
|
||||
write(sessionId: string, clientId: string, data: string): void {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
this.ptys.get(key)?.pty.write(data);
|
||||
}
|
||||
|
||||
resize(sessionId: string, clientId: string, cols: number, rows: number): void {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
this.ptys.get(key)?.pty.resize(cols, rows);
|
||||
}
|
||||
|
||||
destroy(sessionId: string, clientId: string): void {
|
||||
const key = `${sessionId}:${clientId}`;
|
||||
const entry = this.ptys.get(key);
|
||||
if (!entry) return;
|
||||
entry.dataListeners.clear();
|
||||
entry.exitListeners.clear();
|
||||
try { entry.pty.kill(); } catch {}
|
||||
this.ptys.delete(key);
|
||||
}
|
||||
|
||||
destroyAll(): void {
|
||||
for (const [key, entry] of this.ptys) {
|
||||
entry.dataListeners.clear();
|
||||
entry.exitListeners.clear();
|
||||
try { entry.pty.kill(); } catch {}
|
||||
this.ptys.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ptyManager = new PtySessionManager();
|
||||
Reference in New Issue
Block a user