c61712d8c4
- 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>
107 lines
3.1 KiB
TypeScript
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();
|