Skip to content

Commit de63a82

Browse files
feat: Enhance terminal API with command result tracking and UI integration
1 parent 1f1c572 commit de63a82

File tree

11 files changed

+462
-163
lines changed

11 files changed

+462
-163
lines changed

src/helpers/ipc/terminal/terminal-channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export const TERMINAL_LIST_CHANNEL = 'terminal:list';
1010
export const TERMINAL_DATA_EVENT = 'terminal:data';
1111
export const TERMINAL_EXIT_EVENT = 'terminal:exit';
1212
export const TERMINAL_ERROR_EVENT = 'terminal:error';
13+
export const TERMINAL_COMMAND_RESULT_EVENT = 'terminal:command-result';

src/helpers/ipc/terminal/terminal-context.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
TERMINAL_LIST_CHANNEL,
88
TERMINAL_DATA_EVENT,
99
TERMINAL_EXIT_EVENT,
10-
TERMINAL_ERROR_EVENT
10+
TERMINAL_ERROR_EVENT,
11+
TERMINAL_COMMAND_RESULT_EVENT
1112
} from './terminal-channels';
1213

1314
export interface TerminalCreateOptions {
@@ -37,15 +38,35 @@ export interface TerminalErrorEvent {
3738
error: string;
3839
}
3940

41+
export interface TerminalCommandResultEvent {
42+
terminalId: string;
43+
commandId: string;
44+
result: string;
45+
exitCode: number;
46+
}
47+
48+
export type CommandResultCallback = (result: string, exitCode: number) => void;
49+
4050
export function exposeTerminalContext() {
4151
const { contextBridge, ipcRenderer } = window.require("electron");
4252

53+
// Store command callbacks
54+
const commandCallbacks = new Map<string, CommandResultCallback>();
55+
4356
contextBridge.exposeInMainWorld("terminalApi", {
4457
// Terminal management
4558
create: (options?: TerminalCreateOptions) =>
4659
ipcRenderer.invoke(TERMINAL_CREATE_CHANNEL, options),
47-
write: (terminalId: string, data: string) =>
48-
ipcRenderer.invoke(TERMINAL_WRITE_CHANNEL, terminalId, data),
60+
write: (terminalId: string, data: string, callback?: CommandResultCallback) => {
61+
if (callback) {
62+
// Generate a unique command ID
63+
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
64+
commandCallbacks.set(commandId, callback);
65+
return ipcRenderer.invoke(TERMINAL_WRITE_CHANNEL, terminalId, data, commandId);
66+
} else {
67+
return ipcRenderer.invoke(TERMINAL_WRITE_CHANNEL, terminalId, data);
68+
}
69+
},
4970
resize: (terminalId: string, cols: number, rows: number) =>
5071
ipcRenderer.invoke(TERMINAL_RESIZE_CHANNEL, terminalId, cols, rows),
5172
kill: (terminalId: string) =>
@@ -74,11 +95,28 @@ export function exposeTerminalContext() {
7495
return () => ipcRenderer.removeListener(TERMINAL_ERROR_EVENT, handler);
7596
},
7697

98+
onCommandResult: (callback: (data: TerminalCommandResultEvent) => void) => {
99+
const handler = (_: any, data: TerminalCommandResultEvent) => {
100+
// Execute the stored callback
101+
const commandCallback = commandCallbacks.get(data.commandId);
102+
if (commandCallback) {
103+
commandCallback(data.result, data.exitCode);
104+
commandCallbacks.delete(data.commandId);
105+
}
106+
// Also call the general callback if provided
107+
callback(data);
108+
};
109+
ipcRenderer.on(TERMINAL_COMMAND_RESULT_EVENT, handler);
110+
return () => ipcRenderer.removeListener(TERMINAL_COMMAND_RESULT_EVENT, handler);
111+
},
112+
77113
// Cleanup
78114
removeAllListeners: () => {
79115
ipcRenderer.removeAllListeners(TERMINAL_DATA_EVENT);
80116
ipcRenderer.removeAllListeners(TERMINAL_EXIT_EVENT);
81117
ipcRenderer.removeAllListeners(TERMINAL_ERROR_EVENT);
118+
ipcRenderer.removeAllListeners(TERMINAL_COMMAND_RESULT_EVENT);
119+
commandCallbacks.clear();
82120
}
83121
});
84122
}

src/helpers/ipc/terminal/terminal-listeners.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,29 @@ import {
1010
TERMINAL_LIST_CHANNEL,
1111
TERMINAL_DATA_EVENT,
1212
TERMINAL_EXIT_EVENT,
13-
TERMINAL_ERROR_EVENT
13+
TERMINAL_ERROR_EVENT,
14+
TERMINAL_COMMAND_RESULT_EVENT
1415
} from './terminal-channels';
1516

1617
const spawn = nodePty.spawn;
1718
type IPty = typeof nodePty.spawn extends (...args: any[]) => infer R ? R : never;
1819

20+
interface PendingCommand {
21+
id: string;
22+
output: string;
23+
startTime: number;
24+
isActive: boolean;
25+
}
26+
1927
interface TerminalInstance {
2028
id: string;
2129
pty: IPty;
2230
cwd: string;
2331
title: string;
2432
pid: number;
2533
windowId: number;
34+
pendingCommands: Map<string, PendingCommand>;
35+
currentCommand: PendingCommand | null;
2636
}
2737

2838
const terminals = new Map<string, TerminalInstance>();
@@ -46,6 +56,26 @@ function generateTerminalId(): string {
4656
return `terminal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
4757
}
4858

59+
// Helper function to detect when a command has finished
60+
function isCommandFinished(latestData: string, fullOutput: string): boolean {
61+
// Look for common shell prompt patterns that indicate command completion
62+
const promptPatterns = [
63+
/\$\s*$/, // Basic $ prompt
64+
/%\s*$/, // Zsh % prompt
65+
/>\s*$/, // Windows > prompt
66+
/[#$%>]\s*$/, // Any of the common prompt endings
67+
/~\s*[#$%>]\s*$/, // Prompt with ~ (home directory)
68+
/.*[#$%>]\s*$/ // Any path ending with prompt symbol
69+
];
70+
71+
// Split into lines and check the last non-empty line
72+
const lines = latestData.split(/\r?\n/);
73+
const lastLine = lines[lines.length - 1] || '';
74+
75+
// Check if the last line contains a prompt pattern
76+
return promptPatterns.some(pattern => pattern.test(lastLine));
77+
}
78+
4979
export function addTerminalEventListeners(window: BrowserWindow): void {
5080
mainWindow = window;
5181

@@ -76,7 +106,9 @@ export function addTerminalEventListeners(window: BrowserWindow): void {
76106
cwd,
77107
title,
78108
pid: pty.pid,
79-
windowId: window.id
109+
windowId: window.id,
110+
pendingCommands: new Map(),
111+
currentCommand: null
80112
};
81113

82114
terminals.set(terminalId, terminal);
@@ -89,6 +121,31 @@ export function addTerminalEventListeners(window: BrowserWindow): void {
89121
data
90122
});
91123
}
124+
125+
// Track command output if there's a current command
126+
if (terminal.currentCommand && terminal.currentCommand.isActive) {
127+
terminal.currentCommand.output += data;
128+
129+
// Check if command has finished (look for shell prompt patterns)
130+
if (isCommandFinished(data, terminal.currentCommand.output)) {
131+
const command = terminal.currentCommand;
132+
terminal.currentCommand.isActive = false;
133+
134+
// Send command result
135+
if (mainWindow && !mainWindow.isDestroyed()) {
136+
mainWindow.webContents.send(TERMINAL_COMMAND_RESULT_EVENT, {
137+
terminalId,
138+
commandId: command.id,
139+
result: command.output,
140+
exitCode: 0 // We'll get the real exit code from the command parsing
141+
});
142+
}
143+
144+
// Clean up
145+
terminal.pendingCommands.delete(command.id);
146+
terminal.currentCommand = null;
147+
}
148+
}
92149
});
93150

94151
// Handle terminal exit
@@ -115,13 +172,26 @@ export function addTerminalEventListeners(window: BrowserWindow): void {
115172
});
116173

117174
// Write to terminal
118-
ipcMain.handle(TERMINAL_WRITE_CHANNEL, async (_, terminalId: string, data: string) => {
175+
ipcMain.handle(TERMINAL_WRITE_CHANNEL, async (_, terminalId: string, data: string, commandId?: string) => {
119176
try {
120177
const terminal = terminals.get(terminalId);
121178
if (!terminal) {
122179
throw new Error(`Terminal ${terminalId} not found`);
123180
}
124181

182+
// If a commandId is provided, start tracking this command
183+
if (commandId) {
184+
const pendingCommand: PendingCommand = {
185+
id: commandId,
186+
output: '',
187+
startTime: Date.now(),
188+
isActive: true
189+
};
190+
191+
terminal.pendingCommands.set(commandId, pendingCommand);
192+
terminal.currentCommand = pendingCommand;
193+
}
194+
125195
terminal.pty.write(data);
126196
return true;
127197
} catch (error) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Usage example for the enhanced terminal API with command result tracking
3+
*/
4+
5+
// Example usage of the new terminal API with command result callbacks
6+
export function exampleTerminalUsage() {
7+
// Create a new terminal
8+
window.terminalApi.create({ cwd: process.cwd(), title: 'Example Terminal' })
9+
.then((terminal) => {
10+
console.log('Terminal created:', terminal);
11+
12+
// Example 1: Run a command with callback to get results
13+
window.terminalApi.write(terminal.id, 'cat package.json\n', (result: string, exitCode: number) => {
14+
console.log('Command finished with exit code:', exitCode);
15+
console.log('Command output:', result);
16+
17+
// Parse the package.json content from the result
18+
try {
19+
// Extract just the JSON part from the terminal output
20+
const jsonMatch = result.match(/\{[\s\S]*\}/);
21+
if (jsonMatch) {
22+
const packageJson = JSON.parse(jsonMatch[0]);
23+
console.log('Package name:', packageJson.name);
24+
console.log('Package version:', packageJson.version);
25+
}
26+
} catch (error) {
27+
console.error('Failed to parse package.json:', error);
28+
}
29+
});
30+
31+
// Example 2: Run multiple commands in sequence
32+
setTimeout(() => {
33+
window.terminalApi.write(terminal.id, 'ls -la\n', (result: string, exitCode: number) => {
34+
console.log('Directory listing result:', result);
35+
36+
// Count files in the directory
37+
const lines = result.split('\n').filter((line: string) => line.trim() && !line.startsWith('total'));
38+
console.log(`Found ${lines.length} files/directories`);
39+
});
40+
}, 2000);
41+
42+
// Example 3: Run a command that might fail
43+
setTimeout(() => {
44+
window.terminalApi.write(terminal.id, 'nonexistent-command\n', (result: string, exitCode: number) => {
45+
if (exitCode !== 0) {
46+
console.error('Command failed with exit code:', exitCode);
47+
console.error('Error output:', result);
48+
} else {
49+
console.log('Command succeeded:', result);
50+
}
51+
});
52+
}, 4000);
53+
54+
// Example 4: Regular write without callback (existing behavior)
55+
setTimeout(() => {
56+
window.terminalApi.write(terminal.id, 'echo "This is a regular write without callback"\n');
57+
}, 6000);
58+
})
59+
.catch((error) => {
60+
console.error('Failed to create terminal:', error);
61+
});
62+
}
63+
64+
// Example of setting up global command result listener
65+
export function setupCommandResultListener() {
66+
window.terminalApi.onCommandResult((data: { terminalId: string; commandId: string; result: string; exitCode: number }) => {
67+
console.log(`Command ${data.commandId} in terminal ${data.terminalId} completed:`, {
68+
result: data.result,
69+
exitCode: data.exitCode
70+
});
71+
});
72+
}

0 commit comments

Comments
 (0)