@@ -54,7 +54,7 @@ export function TerminalTab({ session }: Props): JSX.Element {
5454 // ── Context menu state ────────────────────────────────────────────────────────
5555 const [ ctxMenu , setCtxMenu ] = useState < { x : number ; y : number } | null > ( null )
5656
57- const { setSessionStatus, setSessionLogging } = useAppStore ( )
57+ const { setSessionStatus, setSessionLogging, setErrorAlert } = useAppStore ( )
5858
5959 // Derive logging state from the session's loggingPath (store is source of truth)
6060 const logging = ! ! session . loggingPath
@@ -254,6 +254,56 @@ export function TerminalTab({ session }: Props): JSX.Element {
254254 }
255255 } , [ session . id , session . connection . protocol ] )
256256
257+ // ── Error Alert — always-on detector (panel open or closed) ──────────────────
258+ useEffect ( ( ) => {
259+ const ERROR_PATTERNS = [
260+ / ^ % \s * ( E r r o r | I n v a l i d | I n c o m p l e t e | A m b i g u o u s | B a d | U n k n o w n ) / im, // Cisco IOS
261+ / \b c o m m a n d n o t f o u n d \b / i,
262+ / \b p e r m i s s i o n d e n i e d \b / i,
263+ / \b n o r o u t e t o h o s t \b / i,
264+ / \b c o n n e c t i o n r e f u s e d \b / i,
265+ / \b d e s t i n a t i o n h o s t u n r e a c h a b l e \b / i,
266+ / \b n e t w o r k u n r e a c h a b l e \b / i,
267+ / \b t i m e d ? o u t \b / i,
268+ / \b o p e r a t i o n t i m e d o u t \b / i,
269+ / \b f a t a l e r r o r \b / i,
270+ / \b s e g m e n t a t i o n f a u l t \b / i,
271+ / ^ e r r o r : / im,
272+ / \b F a i l e d t o \b / i,
273+ ]
274+ const stripAnsi = ( s : string ) => s . replace ( / \x1b \[ [ 0 - 9 ; ? ] * [ a - z A - Z ] / g, '' ) . trim ( )
275+
276+ let buf = ''
277+ let timer : ReturnType < typeof setTimeout > | null = null
278+
279+ const proto = session . connection . protocol
280+ const onData = ( _sid : string , data : string ) => {
281+ if ( _sid !== session . id ) return
282+ buf += data
283+ if ( timer ) clearTimeout ( timer )
284+ timer = setTimeout ( ( ) => {
285+ const plain = stripAnsi ( buf )
286+ buf = ''
287+ if ( ! plain || plain . length < 5 ) return
288+ const { aiPanelOpen, aiStreaming } = useAppStore . getState ( )
289+ if ( aiPanelOpen || aiStreaming ) return // panel already open, no need for badge
290+ if ( ERROR_PATTERNS . some ( ( p ) => p . test ( plain ) ) ) {
291+ setErrorAlert ( session . id )
292+ }
293+ } , 800 )
294+ }
295+
296+ let off : ( ( ) => void ) | undefined
297+ if ( proto === 'ssh' ) off = window . api . ssh . onData ( onData )
298+ else if ( proto === 'telnet' ) off = window . api . telnet . onData ( onData )
299+ else if ( proto === 'serial' ) off = window . api . serial . onData ( onData )
300+
301+ return ( ) => {
302+ if ( timer ) clearTimeout ( timer )
303+ off ?.( )
304+ }
305+ } , [ session . id , session . connection . protocol , setErrorAlert ] )
306+
257307 // ── 1. Init terminal ─────────────────────────────────────────────────────────
258308 useEffect ( ( ) => {
259309 mountedRef . current = true
0 commit comments