@@ -64,6 +64,9 @@ const elements = {
6464 terminalContainer : document . getElementById ( 'terminal-container' ) ,
6565 terminalTitleText : document . getElementById ( 'terminal-title-text' ) ,
6666 terminalStatus : document . getElementById ( 'terminal-status' ) ,
67+ terminalPresence : document . getElementById ( 'terminal-presence' ) ,
68+ terminalPresenceLabel : document . getElementById ( 'terminal-presence-label' ) ,
69+ terminalPresenceHint : document . getElementById ( 'terminal-presence-hint' ) ,
6770 btnTerminalModeAdb : document . getElementById ( 'btn-terminal-mode-adb' ) ,
6871 btnTerminalModeSsh : document . getElementById ( 'btn-terminal-mode-ssh' ) ,
6972 btnCloseTerminal : document . getElementById ( 'btn-close-terminal' ) ,
@@ -126,6 +129,9 @@ class TerminalManager {
126129 this . mode = this . loadMode ( ) ;
127130 this . credentials = null ;
128131 this . rememberCredentials = false ;
132+ this . localAdbInput = '' ;
133+ this . adbPromptVisible = false ;
134+ this . adbPromptTimer = null ;
129135 }
130136
131137 normalizeMode ( mode ) {
@@ -157,6 +163,163 @@ class TerminalManager {
157163 return this . normalizeMode ( mode ) === 'ssh' ? 'SSH Terminal' : 'ADB Terminal' ;
158164 }
159165
166+ clearAdbPromptTimer ( ) {
167+ if ( this . adbPromptTimer ) {
168+ clearTimeout ( this . adbPromptTimer ) ;
169+ this . adbPromptTimer = null ;
170+ }
171+ }
172+
173+ resetAdbFlowState ( ) {
174+ this . clearAdbPromptTimer ( ) ;
175+ this . localAdbInput = '' ;
176+ this . adbPromptVisible = false ;
177+ }
178+
179+ updateInteractionState ( stage , hint = '' ) {
180+ const presenceEl = elements . terminalPresence ;
181+ const labelEl = elements . terminalPresenceLabel ;
182+ const hintEl = elements . terminalPresenceHint ;
183+ if ( ! presenceEl || ! labelEl || ! hintEl ) return ;
184+
185+ const defaults = {
186+ disconnected : {
187+ label : 'Disconnected' ,
188+ hint : 'Open the terminal to start a shell session.'
189+ } ,
190+ connecting : {
191+ label : `Opening ${ this . getModeLabel ( ) } ` ,
192+ hint : `Preparing the ${ this . getModeLabel ( ) . toLowerCase ( ) } session...`
193+ } ,
194+ credentials : {
195+ label : 'Waiting For Credentials' ,
196+ hint : 'Enter your SSH username and password to continue.'
197+ } ,
198+ ready : {
199+ label : `${ this . getModeLabel ( this . connectedMode || this . mode ) } Ready` ,
200+ hint : this . normalizeMode ( this . connectedMode || this . mode ) === 'adb'
201+ ? 'Blue ">" means you can type. Press Enter to run the command.'
202+ : 'Use the remote shell prompt in the terminal to keep typing.'
203+ } ,
204+ typing : {
205+ label : 'Typing' ,
206+ hint : 'Your command is still being edited. Press Enter to run it.'
207+ } ,
208+ running : {
209+ label : 'Running' ,
210+ hint : 'The command was sent to the device. Waiting for response...'
211+ } ,
212+ output : {
213+ label : 'Streaming Output' ,
214+ hint : 'Device output is still arriving. A new prompt appears when it settles.'
215+ } ,
216+ error : {
217+ label : 'Needs Attention' ,
218+ hint : 'Check the latest terminal message for details.'
219+ }
220+ } ;
221+
222+ const next = defaults [ stage ] || defaults . disconnected ;
223+ presenceEl . className = `terminal-presence is-${ stage } ` ;
224+ labelEl . textContent = next . label ;
225+ hintEl . textContent = hint || next . hint ;
226+ }
227+
228+ renderAdbPrompt ( ) {
229+ if ( ! this . terminal || this . connectedMode !== 'adb' || this . adbPromptVisible ) {
230+ return ;
231+ }
232+
233+ this . terminal . write ( '\x1b[38;2;96;165;250m>\x1b[0m ' ) ;
234+ this . adbPromptVisible = true ;
235+ this . updateInteractionState ( 'ready' ) ;
236+ }
237+
238+ scheduleAdbPrompt ( delay = 220 ) {
239+ if ( this . connectedMode !== 'adb' ) return ;
240+
241+ this . clearAdbPromptTimer ( ) ;
242+ this . adbPromptTimer = setTimeout ( ( ) => {
243+ if ( ! this . connected || this . connectedMode !== 'adb' || this . localAdbInput ) {
244+ return ;
245+ }
246+ this . renderAdbPrompt ( ) ;
247+ } , delay ) ;
248+ }
249+
250+ handleAdbLocalInput ( data ) {
251+ for ( const char of data ) {
252+ if ( char === '\r' ) {
253+ this . terminal . write ( '\r\n' ) ;
254+ const hadCommand = this . localAdbInput . trim ( ) . length > 0 ;
255+ this . localAdbInput = '' ;
256+ this . adbPromptVisible = false ;
257+ this . updateInteractionState ( hadCommand ? 'running' : 'ready' ) ;
258+ if ( ! hadCommand ) {
259+ this . scheduleAdbPrompt ( 120 ) ;
260+ }
261+ continue ;
262+ }
263+
264+ if ( char === '\u007f' ) {
265+ if ( this . localAdbInput . length > 0 ) {
266+ this . localAdbInput = this . localAdbInput . slice ( 0 , - 1 ) ;
267+ this . terminal . write ( '\b \b' ) ;
268+ }
269+ this . updateInteractionState ( this . localAdbInput ? 'typing' : 'ready' ) ;
270+ continue ;
271+ }
272+
273+ if ( char === '\t' ) {
274+ if ( ! this . adbPromptVisible ) {
275+ this . renderAdbPrompt ( ) ;
276+ }
277+ this . localAdbInput += ' ' ;
278+ this . terminal . write ( ' ' ) ;
279+ this . updateInteractionState ( 'typing' ) ;
280+ continue ;
281+ }
282+
283+ if ( char === '\u0003' ) {
284+ this . localAdbInput = '' ;
285+ this . adbPromptVisible = false ;
286+ this . terminal . write ( '^C\r\n' ) ;
287+ this . updateInteractionState ( 'ready' , 'Command interrupted. You can type the next one.' ) ;
288+ this . scheduleAdbPrompt ( 120 ) ;
289+ continue ;
290+ }
291+
292+ if ( char < ' ' || char === '\x7f' ) {
293+ continue ;
294+ }
295+
296+ if ( ! this . adbPromptVisible ) {
297+ this . renderAdbPrompt ( ) ;
298+ }
299+
300+ this . localAdbInput += char ;
301+ this . terminal . write ( char ) ;
302+ this . updateInteractionState ( 'typing' ) ;
303+ }
304+ }
305+
306+ handleTerminalData ( data ) {
307+ if ( ! this . terminal ) return ;
308+
309+ if ( this . connectedMode === 'adb' ) {
310+ this . clearAdbPromptTimer ( ) ;
311+ this . adbPromptVisible = false ;
312+ if ( data && data . trim ( ) ) {
313+ this . updateInteractionState ( 'output' ) ;
314+ }
315+ this . terminal . write ( data ) ;
316+ this . scheduleAdbPrompt ( ) ;
317+ return ;
318+ }
319+
320+ this . terminal . write ( data ) ;
321+ }
322+
160323 updateModeUI ( ) {
161324 const isAdb = this . mode === 'adb' ;
162325
@@ -302,6 +465,9 @@ class TerminalManager {
302465 // Handle input
303466 this . terminal . onData ( ( data ) => {
304467 if ( this . connected ) {
468+ if ( this . connectedMode === 'adb' ) {
469+ this . handleAdbLocalInput ( data ) ;
470+ }
305471 window . electronAPI . terminalWrite ( data ) ;
306472 }
307473 } ) ;
@@ -315,18 +481,18 @@ class TerminalManager {
315481
316482 // Listen for data from main process
317483 window . electronAPI . onTerminalData ( ( data ) => {
318- if ( this . terminal ) {
319- this . terminal . write ( data ) ;
320- }
484+ this . handleTerminalData ( data ) ;
321485 } ) ;
322486
323487 // Listen for close event
324488 window . electronAPI . onTerminalClose ( ( data ) => {
325489 const closedMode = this . normalizeMode ( data && data . mode ? data . mode : this . connectedMode ) ;
490+ this . resetAdbFlowState ( ) ;
326491 this . connected = false ;
327492 this . connecting = false ;
328493 this . connectedMode = null ;
329494 this . updateStatus ( 'disconnected' ) ;
495+ this . updateInteractionState ( 'disconnected' , `${ this . getModeTitle ( closedMode ) } closed${ data && data . reason ? `: ${ data . reason } ` : '.' } ` ) ;
330496 this . updateModeUI ( ) ;
331497 this . terminal . write ( `\r\n\x1b[33m[${ this . getModeTitle ( closedMode ) } closed${ data && data . reason ? `: ${ data . reason } ` : '' } ]\x1b[0m\r\n` ) ;
332498 } ) ;
@@ -337,6 +503,7 @@ class TerminalManager {
337503 } ) ;
338504
339505 this . updateModeUI ( ) ;
506+ this . updateInteractionState ( 'disconnected' ) ;
340507 }
341508
342509 /**
@@ -379,6 +546,7 @@ class TerminalManager {
379546 case 'connecting' :
380547 statusEl . classList . add ( 'connecting' ) ;
381548 statusEl . textContent = `Connecting ${ this . getModeLabel ( ) } ...` ;
549+ this . updateInteractionState ( 'connecting' ) ;
382550 break ;
383551 case 'connected' :
384552 statusEl . classList . add ( 'connected' ) ;
@@ -387,6 +555,7 @@ class TerminalManager {
387555 case 'error' :
388556 statusEl . classList . add ( 'error' ) ;
389557 statusEl . textContent = message || 'Error' ;
558+ this . updateInteractionState ( 'error' , message || '' ) ;
390559 break ;
391560 default :
392561 statusEl . textContent = '' ;
@@ -598,13 +767,15 @@ class TerminalManager {
598767
599768 writeBanner ( mode ) {
600769 const title = this . getModeTitle ( mode ) ;
770+ this . resetAdbFlowState ( ) ;
601771 this . terminal . clear ( ) ;
602772 this . terminal . write ( `\x1b[36m=== ${ title } ===\x1b[0m\r\n` ) ;
603773 if ( mode === 'adb' ) {
604774 this . terminal . write ( '\x1b[90mDirect interactive shell over adb.\x1b[0m\r\n' ) ;
605- this . terminal . write ( '\x1b[90mType directly in the terminal area after it opens .\x1b[0m\r\n\r\n' ) ;
775+ this . terminal . write ( '\x1b[90mBlue ">" means ready for input. Status strip shows typing, running and output .\x1b[0m\r\n\r\n' ) ;
606776 } else {
607- this . terminal . write ( '\x1b[90mTermux SSH session over the adb tunnel.\x1b[0m\r\n\r\n' ) ;
777+ this . terminal . write ( '\x1b[90mTermux SSH session over the adb tunnel.\x1b[0m\r\n' ) ;
778+ this . terminal . write ( '\x1b[90mFollow the remote shell prompt shown by the device once connected.\x1b[0m\r\n\r\n' ) ;
608779 }
609780 }
610781
@@ -735,6 +906,7 @@ class TerminalManager {
735906 */
736907 async connectSshMode ( ) {
737908 console . log ( '[Terminal] Starting SSH connection process' ) ;
909+ this . updateInteractionState ( 'credentials' ) ;
738910 this . terminal . write ( '\x1b[90mStep 1/3: Waiting for SSH credentials...\x1b[0m\r\n' ) ;
739911 const credentials = await this . promptCredentials ( ) ;
740912
@@ -770,6 +942,7 @@ class TerminalManager {
770942 this . connectedMode = 'ssh' ;
771943 this . updateModeUI ( ) ;
772944 this . updateStatus ( 'connected' ) ;
945+ this . updateInteractionState ( 'ready' ) ;
773946 this . terminal . write ( `\r\n\x1b[32mSSH connected in ${ elapsed } ms.\x1b[0m\r\n\r\n` ) ;
774947 this . fit ( ) ;
775948 this . focusTerminal ( ) ;
@@ -808,6 +981,7 @@ class TerminalManager {
808981 this . updateModeUI ( ) ;
809982 this . updateStatus ( 'connected' ) ;
810983 this . terminal . write ( `\r\n\x1b[32mADB shell connected in ${ elapsed } ms.\x1b[0m\r\n\r\n` ) ;
984+ this . renderAdbPrompt ( ) ;
811985 this . fit ( ) ;
812986 this . focusTerminal ( ) ;
813987 return true ;
@@ -842,8 +1016,10 @@ class TerminalManager {
8421016 this . connected = false ;
8431017 this . connecting = false ;
8441018 this . connectedMode = null ;
1019+ this . resetAdbFlowState ( ) ;
8451020 state . terminalLastError = '' ;
8461021 this . updateStatus ( 'disconnected' ) ;
1022+ this . updateInteractionState ( 'disconnected' ) ;
8471023 if ( this . terminal && ! silent ) {
8481024 this . terminal . write ( '\r\n\x1b[33mDisconnected.\x1b[0m\r\n' ) ;
8491025 }
0 commit comments