Enhance CLI terminal integration and output handling in InkApp. Introduce terminalService for managing PTY backend, including resizing and updating terminal output. Implement ANSI stripping and tab expansion for accurate line rendering. Improve state management for terminal, logs, and LLM output, ensuring responsive UI updates and error handling during input submission.

This commit is contained in:
sebseb7
2025-08-12 18:34:07 +00:00
parent ac09e4ed08
commit b515275407
4 changed files with 4935 additions and 16 deletions

123
src/terminalService.js Normal file
View File

@@ -0,0 +1,123 @@
import EventEmitter from 'events';
import pty from 'node-pty';
class TerminalService extends EventEmitter {
constructor() {
super();
this.lines = [];
this.partial = '';
this.ptyProcess = null;
this.started = false;
this.maxLines = 1000;
}
start() {
if (this.started) return;
const shell = process.platform === 'win32' ? 'powershell.exe' : 'bash';
const cols = process.stdout && process.stdout.columns ? process.stdout.columns : 120;
const rows = process.stdout && process.stdout.rows ? process.stdout.rows : 30;
const isWindows = process.platform === 'win32';
const userShell = process.env.SHELL && !isWindows ? process.env.SHELL : null;
const shellPath = userShell || (isWindows ? 'powershell.exe' : '/bin/bash');
const args = ['--rcfile','rc'];
this.ptyProcess = pty.spawn(shellPath, args, {
name: 'xterm-256color',
cols,
rows,
cwd: process.cwd(),
env: {
...process.env,
TERM: 'xterm-256color',
PS1: '> '
},
});
this.ptyProcess.onData((data) => {
// Normalize line endings
const normalized = String(data).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const parts = normalized.split('\n');
// First part joins the existing partial
if (parts.length > 0) {
this.partial += parts[0];
}
// For each subsequent part before the last, we have completed lines
for (let i = 1; i < parts.length; i += 1) {
this.lines.push(this.partial);
this.partial = parts[i];
}
// Enforce max lines buffer
if (this.lines.length > this.maxLines) {
this.lines.splice(0, this.lines.length - this.maxLines);
}
// Emit lines including current partial to ensure prompts (no trailing newline) are visible
const display = this.partial ? [...this.lines, this.partial] : this.lines.slice();
this.emit('update', display);
});
// Resize with the host TTY
const onResize = () => {
try {
const newCols = process.stdout.columns || cols;
const newRows = process.stdout.rows || rows;
this.ptyProcess.resize(newCols, newRows);
} catch {
// ignore
}
};
if (process.stdout && process.stdout.on) {
process.stdout.on('resize', onResize);
}
this.ptyProcess.onExit(({ exitCode, signal }) => {
this.emit('exit', { exitCode, signal });
});
this.started = true;
}
getLines() {
return this.lines.slice();
}
write(input) {
if (!this.ptyProcess) return;
this.ptyProcess.write(input);
}
resize(columns, rows) {
if (!this.ptyProcess) return;
try {
const cols = Math.max(1, Number(columns) || 1);
const r = rows ? Math.max(1, Number(rows) || 1) : undefined;
if (r !== undefined) {
this.ptyProcess.resize(cols, r);
} else {
this.ptyProcess.resize(cols, this.ptyProcess.rows || 24);
}
} catch {
// ignore
}
}
dispose() {
try {
if (this.ptyProcess) {
this.ptyProcess.kill();
this.ptyProcess = null;
}
} catch {
// ignore
}
}
}
const terminalService = new TerminalService();
export default terminalService;