@@ -2,6 +2,12 @@ import { BrowserWindow, ipcMain } from "electron";
22import crypto from "crypto" ;
33import { log } from "../lib/logger" ;
44import { safeSend } from "../lib/safe-send" ;
5+ import {
6+ appendTerminalHistory ,
7+ EMPTY_TERMINAL_HISTORY ,
8+ readTerminalHistory ,
9+ } from "../lib/terminal-history" ;
10+ import type { TerminalHistoryState } from "../lib/terminal-history" ;
511
612interface TerminalEntry {
713 pty : {
@@ -14,6 +20,12 @@ interface TerminalEntry {
1420 cols : number ;
1521 rows : number ;
1622 spaceId : string ;
23+ createdAt : number ;
24+ history : TerminalHistoryState ;
25+ seq : number ;
26+ exited : boolean ;
27+ exitCode : number | null ;
28+ destroyed : boolean ;
1729}
1830
1931export const terminals = new Map < string , TerminalEntry > ( ) ;
@@ -46,15 +58,40 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
4658 env : { ...process . env , TERM : "xterm-256color" , COLORTERM : "truecolor" } ,
4759 } ) ;
4860
49- terminals . set ( terminalId , { pty : ptyProcess , cols : cols || 80 , rows : rows || 24 , spaceId : spaceId || "default" } ) ;
61+ const entry : TerminalEntry = {
62+ pty : ptyProcess ,
63+ cols : cols || 80 ,
64+ rows : rows || 24 ,
65+ spaceId : spaceId || "default" ,
66+ createdAt : Date . now ( ) ,
67+ history : EMPTY_TERMINAL_HISTORY ,
68+ seq : 0 ,
69+ exited : false ,
70+ exitCode : null ,
71+ destroyed : false ,
72+ } ;
73+ terminals . set ( terminalId , entry ) ;
5074
5175 ptyProcess . onData ( ( data : string ) => {
52- safeSend ( getMainWindow , "terminal:data" , { terminalId, data } ) ;
76+ if ( entry . destroyed ) return ;
77+ entry . history = appendTerminalHistory ( entry . history , data ) ;
78+ entry . seq += 1 ;
79+ safeSend ( getMainWindow , "terminal:data" , { terminalId, data, seq : entry . seq } ) ;
5380 } ) ;
5481
5582 ptyProcess . onExit ( ( { exitCode } : { exitCode : number } ) => {
83+ if ( entry . destroyed ) return ;
5684 log ( "TERMINAL" , `Terminal ${ terminalId . slice ( 0 , 8 ) } exited with code ${ exitCode } ` ) ;
57- terminals . delete ( terminalId ) ;
85+ entry . exited = true ;
86+ entry . exitCode = exitCode ;
87+ const exitNotice = "\r\n\x1b[2m[process exited]\x1b[0m\r\n" ;
88+ entry . history = appendTerminalHistory ( entry . history , exitNotice ) ;
89+ entry . seq += 1 ;
90+ safeSend ( getMainWindow , "terminal:data" , {
91+ terminalId,
92+ data : exitNotice ,
93+ seq : entry . seq ,
94+ } ) ;
5895 safeSend ( getMainWindow , "terminal:exit" , { terminalId, exitCode } ) ;
5996 } ) ;
6097
@@ -70,13 +107,19 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
70107 ipcMain . handle ( "terminal:write" , ( _event , { terminalId, data } : { terminalId : string ; data : string } ) => {
71108 const term = terminals . get ( terminalId ) ;
72109 if ( ! term ) return { error : "Terminal not found" } ;
110+ if ( term . exited ) return { error : "Terminal has exited" } ;
73111 term . pty . write ( data ) ;
74112 return { ok : true } ;
75113 } ) ;
76114
77115 ipcMain . handle ( "terminal:resize" , ( _event , { terminalId, cols, rows } : { terminalId : string ; cols : number ; rows : number } ) => {
78116 const term = terminals . get ( terminalId ) ;
79117 if ( ! term ) return { error : "Terminal not found" } ;
118+ if ( term . exited ) {
119+ term . cols = cols ;
120+ term . rows = rows ;
121+ return { ok : true } ;
122+ }
80123 try {
81124 term . pty . resize ( cols , rows ) ;
82125 term . cols = cols ;
@@ -87,10 +130,36 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
87130 return { ok : true } ;
88131 } ) ;
89132
133+ ipcMain . handle ( "terminal:snapshot" , ( _event , terminalId : string ) => {
134+ const term = terminals . get ( terminalId ) ;
135+ if ( ! term ) return { error : "Terminal not found" } ;
136+ return {
137+ output : readTerminalHistory ( term . history ) ,
138+ seq : term . seq ,
139+ exited : term . exited ,
140+ exitCode : term . exitCode ,
141+ } ;
142+ } ) ;
143+
144+ ipcMain . handle ( "terminal:list" , ( ) => {
145+ return {
146+ terminals : Array . from ( terminals . entries ( ) )
147+ . map ( ( [ terminalId , term ] ) => ( {
148+ terminalId,
149+ spaceId : term . spaceId ,
150+ createdAt : term . createdAt ,
151+ exited : term . exited ,
152+ exitCode : term . exitCode ,
153+ } ) )
154+ . sort ( ( a , b ) => a . createdAt - b . createdAt ) ,
155+ } ;
156+ } ) ;
157+
90158 ipcMain . handle ( "terminal:destroy" , ( _event , terminalId : string ) => {
91159 const term = terminals . get ( terminalId ) ;
92160 if ( term ) {
93- term . pty . kill ( ) ;
161+ term . destroyed = true ;
162+ if ( ! term . exited ) term . pty . kill ( ) ;
94163 terminals . delete ( terminalId ) ;
95164 log ( "TERMINAL" , `Destroyed terminal ${ terminalId . slice ( 0 , 8 ) } ` ) ;
96165 }
@@ -100,7 +169,8 @@ export function register(getMainWindow: () => BrowserWindow | null): void {
100169 ipcMain . handle ( "terminal:destroy-space" , ( _event , spaceId : string ) => {
101170 for ( const [ terminalId , term ] of terminals . entries ( ) ) {
102171 if ( term . spaceId !== spaceId ) continue ;
103- term . pty . kill ( ) ;
172+ term . destroyed = true ;
173+ if ( ! term . exited ) term . pty . kill ( ) ;
104174 terminals . delete ( terminalId ) ;
105175 log ( "TERMINAL" , `Destroyed terminal ${ terminalId . slice ( 0 , 8 ) } for space ${ spaceId } ` ) ;
106176 }
0 commit comments