Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.0 kB · 191 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192/** * Built-in terminal panel toggled with Meta+J. * * Uses tmux for shell persistence: a separate tmux server with a per-instance * socket (e.g., "claude-panel-a1b2c3d4") holds the shell session. Each Claude * Code instance gets its own isolated terminal panel that persists within the * session but is destroyed when the instance exits. * * Meta+J is bound to detach-client inside tmux, so pressing it returns to * Claude Code while the shell keeps running. Next toggle re-attaches to the * same session. * * When tmux is not available, falls back to a non-persistent shell via spawnSync. * * Uses the same suspend-Ink pattern as the external editor (promptEditor.ts). */
import { spawn, spawnSync } from 'child_process'import { getSessionId } from '../bootstrap/state.js'import instances from '../ink/instances.js'import { registerCleanup } from './cleanupRegistry.js'import { pwd } from './cwd.js'import { logForDebugging } from './debug.js'
const TMUX_SESSION = 'panel'
/** * Get the tmux socket name for the terminal panel. * Uses a unique socket per Claude Code instance (based on session ID) * so that each instance has its own isolated terminal panel. */export function getTerminalPanelSocket(): string { // Use first 8 chars of session UUID for uniqueness while keeping name short const sessionId = getSessionId() return `claude-panel-${sessionId.slice(0, 8)}`}
let instance: TerminalPanel | undefined
/** * Return the singleton TerminalPanel, creating it lazily on first use. */export function getTerminalPanel(): TerminalPanel { if (!instance) { instance = new TerminalPanel() } return instance}
class TerminalPanel { private hasTmux: boolean | undefined private cleanupRegistered = false
// ── public API ────────────────────────────────────────────────────
toggle(): void { this.showShell() }
// ── tmux helpers ──────────────────────────────────────────────────
private checkTmux(): boolean { if (this.hasTmux !== undefined) return this.hasTmux const result = spawnSync('tmux', ['-V'], { encoding: 'utf-8' }) this.hasTmux = result.status === 0 if (!this.hasTmux) { logForDebugging( 'Terminal panel: tmux not found, falling back to non-persistent shell', ) } return this.hasTmux }
private hasSession(): boolean { const result = spawnSync( 'tmux', ['-L', getTerminalPanelSocket(), 'has-session', '-t', TMUX_SESSION], { encoding: 'utf-8' }, ) return result.status === 0 }
private createSession(): boolean { const shell = process.env.SHELL || '/bin/bash' const cwd = pwd() const socket = getTerminalPanelSocket()
const result = spawnSync( 'tmux', [ '-L', socket, 'new-session', '-d', '-s', TMUX_SESSION, '-c', cwd, shell, '-l', ], { encoding: 'utf-8' }, )
if (result.status !== 0) { logForDebugging( `Terminal panel: failed to create tmux session: ${result.stderr}`, ) return false }
// Bind Meta+J (toggles back to Claude Code from inside the terminal) // and configure the status bar hint. Chained with ';' to collapse // 5 spawnSync calls into 1. // biome-ignore format: one tmux command per line spawnSync('tmux', [ '-L', socket, 'bind-key', '-n', 'M-j', 'detach-client', ';', 'set-option', '-g', 'status-style', 'bg=default', ';', 'set-option', '-g', 'status-left', '', ';', 'set-option', '-g', 'status-right', ' Alt+J to return to Claude ', ';', 'set-option', '-g', 'status-right-style', 'fg=brightblack', ])
if (!this.cleanupRegistered) { this.cleanupRegistered = true registerCleanup(async () => { // Detached async spawn — spawnSync here would block the event loop // and serialize the entire cleanup Promise.all in gracefulShutdown. // .on('error') swallows ENOENT if tmux disappears between session // creation and cleanup — prevents spurious uncaughtException noise. spawn('tmux', ['-L', socket, 'kill-server'], { detached: true, stdio: 'ignore', }) .on('error', () => {}) .unref() }) }
return true }
private attachSession(): void { spawnSync( 'tmux', ['-L', getTerminalPanelSocket(), 'attach-session', '-t', TMUX_SESSION], { stdio: 'inherit' }, ) }
// ── show shell ────────────────────────────────────────────────────
private showShell(): void { const inkInstance = instances.get(process.stdout) if (!inkInstance) { logForDebugging('Terminal panel: no Ink instance found, aborting') return }
inkInstance.enterAlternateScreen() try { if (this.checkTmux() && this.ensureSession()) { this.attachSession() } else { this.runShellDirect() } } finally { inkInstance.exitAlternateScreen() } }
// ── helpers ───────────────────────────────────────────────────────
/** Ensure a tmux session exists, creating one if needed. */ private ensureSession(): boolean { if (this.hasSession()) return true return this.createSession() }
/** Fallback when tmux is not available — runs a non-persistent shell. */ private runShellDirect(): void { const shell = process.env.SHELL || '/bin/bash' const cwd = pwd() spawnSync(shell, ['-i', '-l'], { stdio: 'inherit', cwd, env: process.env, }) }}