Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@
"@tanstack/react-virtual": "^3.13.13",
"@types/d3-force": "^3.0.10",
"@types/dompurify": "^3.0.5",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-unicode11": "^0.9.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.19.0",
"@xterm/xterm": "^6.0.0",
"adm-zip": "^0.5.16",
"ansi-to-html": "^0.7.2",
"archiver": "^7.0.1",
Expand Down
54 changes: 54 additions & 0 deletions src/main/ipc/handlers/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,60 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
})
);

// Spawn a terminal PTY for a specific tab (xterm.js integration)
// This creates a persistent PTY shell for terminal tab emulation
ipcMain.handle(
'process:spawnTerminalTab',
withIpcErrorLogging(handlerOpts('spawnTerminalTab'), async (config: {
sessionId: string;
cwd: string;
shell?: string;
shellArgs?: string;
shellEnvVars?: Record<string, string>;
cols?: number;
rows?: number;
}) => {
const processManager = requireProcessManager(getProcessManager);

// If no shell specified, use defaults from settings
let shellToUse = config.shell || settingsStore.get('defaultShell', 'zsh');

// Custom shell path overrides the detected/selected shell path
const customShellPath = settingsStore.get('customShellPath', '');
if (customShellPath && customShellPath.trim()) {
shellToUse = customShellPath.trim();
}

// Get shell args and env vars from settings if not provided
const shellArgs = config.shellArgs || settingsStore.get('shellArgs', '');
const shellEnvVars = config.shellEnvVars || settingsStore.get('shellEnvVars', {}) as Record<string, string>;

logger.info('Spawning terminal tab', LOG_CONTEXT, {
sessionId: config.sessionId,
cwd: config.cwd,
shell: shellToUse,
cols: config.cols,
rows: config.rows,
});

try {
const result = processManager.spawnTerminalTab({
sessionId: config.sessionId,
cwd: config.cwd,
shell: shellToUse,
shellArgs,
shellEnvVars,
cols: config.cols,
rows: config.rows,
});
return result;
} catch (error) {
logger.error('Failed to spawn terminal tab', LOG_CONTEXT, { error: String(error) });
throw error;
}
})
);

// Run a single command and capture only stdout/stderr (no PTY echo/prompts)
// Supports SSH remote execution when sessionSshRemoteConfig is provided
ipcMain.handle(
Expand Down
21 changes: 21 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ contextBridge.exposeInMainWorld('maestro', {
};
}) => ipcRenderer.invoke('process:runCommand', config),

// Spawn a terminal PTY for a specific tab (xterm.js integration)
// Creates a persistent PTY shell for terminal tab emulation
spawnTerminalTab: (config: {
sessionId: string;
cwd: string;
shell?: string;
shellArgs?: string;
shellEnvVars?: Record<string, string>;
cols?: number;
rows?: number;
}) => ipcRenderer.invoke('process:spawnTerminalTab', config),

// Get all active processes from ProcessManager
getActiveProcesses: () => ipcRenderer.invoke('process:getActiveProcesses'),

Expand Down Expand Up @@ -1785,6 +1797,15 @@ export interface MaestroAPI {
workingDirOverride?: string;
};
}) => Promise<{ exitCode: number }>;
spawnTerminalTab: (config: {
sessionId: string;
cwd: string;
shell?: string;
shellArgs?: string;
shellEnvVars?: Record<string, string>;
cols?: number;
rows?: number;
}) => Promise<{ pid: number; success: boolean }>;
getActiveProcesses: () => Promise<Array<{
sessionId: string;
toolType: string;
Expand Down
58 changes: 57 additions & 1 deletion src/main/process-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,19 @@ export class ProcessManager extends EventEmitter {

// Handle output
ptyProcess.onData((data) => {
// Strip terminal control sequences and filter prompts/echoes
// For terminal mode with xterm.js, send RAW data - xterm handles all rendering
// This includes control sequences, colors, cursor movement, etc.
if (isTerminal) {
logger.debug('[ProcessManager] PTY onData (raw terminal)', 'ProcessManager', {
sessionId,
pid: ptyProcess.pid,
dataLength: data.length,
});
this.emit('data', sessionId, data);
return;
}

// For AI agents using PTY, apply filtering to clean up output
const managedProc = this.processes.get(sessionId);
const cleanedData = stripControlSequences(data, managedProc?.lastCommand, isTerminal);
logger.debug('[ProcessManager] PTY onData', 'ProcessManager', { sessionId, pid: ptyProcess.pid, dataPreview: cleanedData.substring(0, 100) });
Expand Down Expand Up @@ -1366,6 +1378,50 @@ export class ProcessManager extends EventEmitter {
}
}

/**
* Spawn a terminal PTY for a specific terminal tab.
* This is a convenience wrapper around spawn() that enforces terminal mode.
*
* @param config.sessionId - Full session ID in format {sessionId}-terminal-{tabId}
* @param config.cwd - Working directory for the shell
* @param config.shell - Shell to use (e.g., 'zsh', 'bash', '/usr/local/bin/zsh')
* @param config.shellArgs - Additional shell arguments
* @param config.shellEnvVars - Custom environment variables
* @param config.cols - Initial column count (default: 80)
* @param config.rows - Initial row count (default: 24)
*/
spawnTerminalTab(config: {
sessionId: string;
cwd: string;
shell?: string;
shellArgs?: string;
shellEnvVars?: Record<string, string>;
cols?: number;
rows?: number;
}): { pid: number; success: boolean } {
const { sessionId, cwd, shell, shellArgs, shellEnvVars, cols = 80, rows = 24 } = config;

logger.debug('[ProcessManager] spawnTerminalTab()', 'ProcessManager', {
sessionId,
cwd,
shell,
cols,
rows,
});

// Use the existing spawn logic but force terminal mode
return this.spawn({
sessionId,
toolType: 'terminal',
cwd,
command: shell || (process.platform === 'win32' ? 'powershell.exe' : 'zsh'),
args: [],
shell,
shellArgs,
shellEnvVars,
});
}

/**
* Write data to a process's stdin
*/
Expand Down
Loading