Add Ghostty terminal, session recovery, voice asset caching, and gitignore large assets
Integrates ghostty-web and node-pty packages, adds tmux session recovery on startup, refactors WebSocket transport to noServer mode, caches Sherpa voice model assets via service worker. Large assets (sherpa, ghostty-vt.wasm, fonts) are now gitignored as they are copied from the webterm project. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,7 @@ export class TmuxAdapter extends EventEmitter {
|
||||
this._clientChecker = null;
|
||||
this._cleanupInterval = null;
|
||||
this._startSessionCleanup();
|
||||
this._recoverSessions().catch((err) => console.error('[tmux] Recovery error:', err));
|
||||
}
|
||||
|
||||
/** Set a function that checks if WS clients are connected for a session */
|
||||
@@ -806,6 +807,48 @@ export class TmuxAdapter extends EventEmitter {
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
private async _recoverSessions(): Promise<void> {
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
let windows: TmuxWindow[];
|
||||
try {
|
||||
windows = await tmuxManager.listWindows();
|
||||
} catch (err) {
|
||||
console.error('[tmux] Recovery: listWindows failed:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[tmux] Recovery: found ${windows.length} window(s):`, windows.map(w => `${w.name}(${w.command})`).join(', '));
|
||||
const candidates = windows.filter(w => UUID_RE.test(w.name));
|
||||
if (candidates.length === 0) {
|
||||
console.log('[tmux] Recovery: no UUID-named windows to recover');
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(candidates.map(async (win) => {
|
||||
const sessionId = win.name;
|
||||
if (this.sessions.has(sessionId)) return;
|
||||
|
||||
// Find the JSONL file to recover real cwd and firstPrompt
|
||||
let cwd = win.cwd;
|
||||
let firstPrompt: string | null = null;
|
||||
const jsonlPath = await this._findJsonlPath(sessionId);
|
||||
if (jsonlPath) {
|
||||
try {
|
||||
const header = await parseSessionHeader(jsonlPath, sessionId);
|
||||
if (header.cwd) cwd = header.cwd;
|
||||
firstPrompt = header.firstPrompt;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const state = this._createSession(win.id, cwd, sessionId, 'bypassPermissions');
|
||||
state.firstPrompt = firstPrompt;
|
||||
this.sessions.set(sessionId, state);
|
||||
this._startMonitor(sessionId, win.id);
|
||||
await this._ensureWatcher(sessionId);
|
||||
console.log(`[tmux] Recovered session ${sessionId} (${cwd})`);
|
||||
}));
|
||||
}
|
||||
|
||||
private _createSession(windowId: string, cwd: string, cliSessionId: string, permissionMode: string): SessionState {
|
||||
return {
|
||||
windowId,
|
||||
|
||||
@@ -22,11 +22,10 @@ export class WebSocketTransport extends EventEmitter {
|
||||
// connection is never terminated before it has a chance to respond.
|
||||
private alive = new WeakMap<WebSocket, boolean>();
|
||||
|
||||
/** Create WebSocketServer on /ws path with JWT verification and ping/pong keepalive. */
|
||||
setup(server: HttpServer | HttpsServer): void {
|
||||
/** Create WebSocketServer in noServer mode. Call handleUpgrade() from your server upgrade handler. */
|
||||
setup(_server: HttpServer | HttpsServer): void {
|
||||
this.wss = new WebSocketServer({
|
||||
server,
|
||||
path: '/ws',
|
||||
noServer: true,
|
||||
verifyClient: ({ req }, cb) => {
|
||||
const url = new URL(req.url!, `http://${req.headers.host}`);
|
||||
const token = url.searchParams.get('token');
|
||||
@@ -73,6 +72,13 @@ export class WebSocketTransport extends EventEmitter {
|
||||
this.pingInterval.unref();
|
||||
}
|
||||
|
||||
/** Route an HTTP upgrade request into this WSS (called from server's upgrade handler). */
|
||||
handleUpgrade(req: import('http').IncomingMessage, socket: import('stream').Duplex, head: Buffer): void {
|
||||
this.wss!.handleUpgrade(req, socket, head, (ws) => {
|
||||
this.wss!.emit('connection', ws, req);
|
||||
});
|
||||
}
|
||||
|
||||
/** Shut down the WebSocket server and stop keepalive. */
|
||||
destroy(): void {
|
||||
if (this.pingInterval) {
|
||||
|
||||
Reference in New Issue
Block a user