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"] = {