Files
clawtap/server/terminal-session.ts
T
izackp c61712d8c4 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>
2026-06-19 18:04:44 -04:00

107 lines
3.1 KiB
TypeScript

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();