diff --git a/app/session/[id].tsx b/app/session/[id].tsx
index 41a3c78..def808c 100644
--- a/app/session/[id].tsx
+++ b/app/session/[id].tsx
@@ -125,6 +125,11 @@ export default function SessionScreen() {
const shortDir = getShortDir(currentSession?.directory)
const [showScrollButton, setShowScrollButton] = useState(false)
+ // SSE reconnect banner
+ const reconnectAttempts = useEvents((s) => s.reconnectAttempts)
+ const [showConnectedFlash, setShowConnectedFlash] = useState(false)
+ const prevReconnecting = useRef(false)
+
// Voice input — transcript appends to the text input on completion
const speech = useSpeech(
useCallback((text: string) => {
@@ -363,6 +368,19 @@ export default function SessionScreen() {
if (!loadingMore) loadingTriggered.current = false
}, [loadingMore])
+ // Detect reconnecting → stable transition for the "Connected ✓" flash.
+ // reconnectAttempts and lastDisconnectAt reset in the same set() call, so we
+ // can't use lastDisconnectAt alone; a useRef tracks the prior reconnecting state.
+ useEffect(() => {
+ const isReconnecting = reconnectAttempts > 0
+ if (prevReconnecting.current && !isReconnecting) {
+ setShowConnectedFlash(true)
+ const t = setTimeout(() => setShowConnectedFlash(false), 2000)
+ return () => clearTimeout(t)
+ }
+ prevReconnecting.current = isReconnecting
+ }, [reconnectAttempts])
+
const handlePermissionReply = async (requestID: string, reply: "once" | "always" | "reject") => {
if (!sessionClient || !sessionID) return
// Snapshot for rollback
@@ -486,6 +504,18 @@ export default function SessionScreen() {
onClose={() => setShowInfo(false)}
/>
+ {/* SSE reconnect/connected banner */}
+ {reconnectAttempts > 0 && (
+
+ Reconnecting\u2026 (attempt {reconnectAttempts})
+
+ )}
+ {showConnectedFlash && reconnectAttempts === 0 && (
+
+ Connected ✓
+
+ )}
+
{isLoading ? (
@@ -821,4 +851,14 @@ const s = StyleSheet.create({
dirBadgeDark: { backgroundColor: "#1a1a1a" },
dirText: { fontSize: 12, color: "#666666", fontWeight: "500" },
dirTextDark: { color: "#888888" },
+
+ // SSE reconnect/connected banner
+ banner: {
+ paddingHorizontal: 16,
+ paddingVertical: 6,
+ alignItems: "center",
+ },
+ bannerReconnecting: { backgroundColor: "#92400e" },
+ bannerConnected: { backgroundColor: "#065f46" },
+ bannerText: { color: "#ffffff", fontSize: 13, fontWeight: "500" },
})
diff --git a/scripts/android-cua-smoke.py b/scripts/android-cua-smoke.py
index 5de42ad..88369cf 100755
--- a/scripts/android-cua-smoke.py
+++ b/scripts/android-cua-smoke.py
@@ -1029,7 +1029,12 @@ def run_scenario_sse_disconnect_banner(opencode_url: str, model: str, include_ui
_sleep(5.0) # wait for SSE timeout detection in app
# Phase 3: DETERMINISTIC — check UI XML for reconnect text
- has_banner = check_ui_text("Reconnecting") or check_ui_text("reconnect") or check_ui_text("offline")
+ has_banner = (
+ check_ui_text("Reconnecting") or
+ check_ui_text("Reconnecting\u2026") or
+ check_ui_text("reconnect") or
+ check_ui_text("offline")
+ )
results["banner_appeared"] = {"status": "success" if has_banner else "fail",
"detail": "uiautomator XML check"}
print(f" [DETERMINISTIC] banner_appeared={has_banner}")
@@ -1057,7 +1062,7 @@ def run_scenario_sse_disconnect_banner(opencode_url: str, model: str, include_ui
deadline = time.time() + 40
while time.time() < deadline:
_sleep(3.0)
- if not (check_ui_text("Reconnecting") or check_ui_text("reconnect") or check_ui_text("offline")):
+ if not (check_ui_text("Reconnecting") or check_ui_text("Reconnecting\u2026") or check_ui_text("reconnect") or check_ui_text("offline")):
banner_gone = True
break
results["banner_dismissed"] = {"status": "success" if banner_gone else "fail",
@@ -1144,7 +1149,7 @@ def run_scenario_backgrounded_permission_notification(opencode_url: str, model:
appeared = check_notification_drawer(APP_PACKAGE, timeout=15)
if not appeared:
# Fallback: match the real notification copy from events.ts / notifications.ts
- appeared = check_notification_drawer("Permission requested", timeout=5) or \
+ appeared = check_notification_drawer("Agent needs approval", timeout=5) or \
check_notification_drawer("A tool needs your approval", timeout=5)
results["notification_appeared"] = {