Skip to content

Commit 55dd850

Browse files
feat: ARIA error alert badge — detects errors in terminal without panel open
1 parent f06855f commit 55dd850

3 files changed

Lines changed: 70 additions & 5 deletions

File tree

src/renderer/src/components/terminal/TabBar.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ export function TabBar(): JSX.Element {
1313
sessions, activeSessionId, splitSessionId,
1414
setActiveSession, closeSession, setQuickConnectOpen, setSplitSession,
1515
aiPanelOpen, setAiPanelOpen, activeForwardIds,
16-
licenseValid,
16+
licenseValid, errorAlertSessionId, setErrorAlert,
1717
} = useAppStore()
1818

19+
const hasErrorAlert = errorAlertSessionId === activeSessionId
20+
1921
const [splitMenuOpen, setSplitMenuOpen] = useState(false)
2022
const [forwardOpen, setForwardOpen] = useState(false)
2123
const splitMenuRef = useRef<HTMLDivElement>(null)
@@ -87,16 +89,22 @@ export function TabBar(): JSX.Element {
8789
toast.error('License key required', { description: 'Add your license key in Settings → ARIA to use the AI assistant.' })
8890
return
8991
}
92+
if (hasErrorAlert) setErrorAlert(null)
9093
setAiPanelOpen(!aiPanelOpen)
9194
}}
92-
title={aiPanelOpen ? 'Close ARIA' : 'Open ARIA'}
95+
title={hasErrorAlert ? 'ARIA detected an error — click to analyze' : aiPanelOpen ? 'Close ARIA' : 'Open ARIA'}
9396
className={cn(
94-
'shrink-0 self-center flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors ml-auto',
97+
'shrink-0 self-center relative flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors ml-auto',
9598
aiPanelOpen
9699
? 'text-primary bg-primary/15 hover:bg-primary/25'
97-
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
100+
: hasErrorAlert
101+
? 'text-red-400 bg-red-500/10 hover:bg-red-500/20'
102+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
98103
)}
99104
>
105+
{hasErrorAlert && (
106+
<span className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-red-500 animate-pulse" />
107+
)}
100108
<Sparkles className="w-3.5 h-3.5" />
101109
<span className="hidden sm:inline">ARIA AI</span>
102110
</button>

src/renderer/src/components/terminal/TerminalTab.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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*(Error|Invalid|Incomplete|Ambiguous|Bad|Unknown)/im, // Cisco IOS
261+
/\bcommand not found\b/i,
262+
/\bpermission denied\b/i,
263+
/\bno route to host\b/i,
264+
/\bconnection refused\b/i,
265+
/\bdestination host unreachable\b/i,
266+
/\bnetwork unreachable\b/i,
267+
/\btimed? out\b/i,
268+
/\boperation timed out\b/i,
269+
/\bfatal error\b/i,
270+
/\bsegmentation fault\b/i,
271+
/^error:/im,
272+
/\bFailed to\b/i,
273+
]
274+
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;?]*[a-zA-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

src/renderer/src/store/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ interface AppState {
136136
// Session logging (path stored per session so TabBar can show indicator)
137137
setSessionLogging: (sessionId: string, path: string | null) => void
138138

139+
// Error alert badge — set when ARIA detects an error in any session
140+
errorAlertSessionId: string | null
141+
setErrorAlert: (sessionId: string | null) => void
142+
139143
// AI Copilot
140144
aiPanelOpen: boolean
141145
// License state
@@ -193,6 +197,9 @@ export const useAppStore = create<AppState>((set, get) => ({
193197
setLicenseStatus: (s) => set({ licenseValid: s.valid, licensePlan: s.plan, licenseExpiry: s.expiresAt }),
194198
setDeviceId: (id) => set({ deviceId: id }),
195199

200+
errorAlertSessionId: null,
201+
setErrorAlert: (sessionId) => set({ errorAlertSessionId: sessionId }),
202+
196203
// AI Copilot initial state
197204
aiPanelOpen: false,
198205
aiPermission: 'troubleshoot',

0 commit comments

Comments
 (0)