diff --git a/README.md b/README.md index cfe19aa2c4..3458805dd5 100644 --- a/README.md +++ b/README.md @@ -2960,6 +2960,11 @@ The workspace Browser Live client negotiates binary JPEG WebSocket frames, keeps only the newest pending frame per display refresh, and revokes replaced object URLs. Gateway control messages remain JSON, and clients that do not request the binary capability retain the legacy JSON/base64 frame protocol. +Opening a Live connection has a 10-second handshake deadline. A stalled +handshake enters the existing capped backoff, with up to six reconnects; +successful connections reset that retry budget. The panel distinguishes connection +timeouts from other connection failures and shows recovery guidance, including +when retries are exhausted. Turn Live off and on to start a fresh retry budget. ### Context Engineering diff --git a/backend/tests/test_agent_guidance_check.py b/backend/tests/test_agent_guidance_check.py index a1c07f6c70..d645856868 100644 --- a/backend/tests/test_agent_guidance_check.py +++ b/backend/tests/test_agent_guidance_check.py @@ -53,6 +53,7 @@ EXPECTED_GUIDANCE_PATHS = { "backend/packages/harness/deerflow/uploads/AGENTS.md", "backend/packages/harness/deerflow/utils/AGENTS.md", "frontend/src/AGENTS.md", + "frontend/src/components/workspace/browser-view/AGENTS.md", "frontend/src/core/memory/AGENTS.md", "frontend/tests/e2e/AGENTS.md", "scripts/AGENTS.md", diff --git a/frontend/src/components/workspace/browser-view/AGENTS.md b/frontend/src/components/workspace/browser-view/AGENTS.md new file mode 100644 index 0000000000..78421a394c --- /dev/null +++ b/frontend/src/components/workspace/browser-view/AGENTS.md @@ -0,0 +1,18 @@ +# Browser Live + +`useBrowserStream` owns a 10-second opening-handshake deadline per socket +attempt. Timeout retires that attempt and schedules the existing bounded +backoff without depending on a later socket event. Open/error/close and effect +cleanup clear the deadline; retired callbacks cannot reset the retry budget, +update frames, or accept input. Keep the deadline limited to CONNECTING; an +established quiet stream has no client idle timeout. + +Expose `closedReason` as `handshake_timeout` or `connection_failed`; WebSocket +error/close events cannot reliably identify auth, origin, or network failures. +Keep the latest failure reason visible during retries; fence retired callbacks; +clear it on open, disable, or thread change. The panel localizes the reason and +recovery hint even over a retained frame; exhausted retries must not look pending. + +Lifecycle regressions live in +`frontend/tests/unit/components/workspace/browser-view/use-browser-stream.dom.test.tsx`; +panel feedback is covered by the adjacent `browser-view-panel.dom.test.tsx`. diff --git a/frontend/src/components/workspace/browser-view/browser-view-panel.tsx b/frontend/src/components/workspace/browser-view/browser-view-panel.tsx index e1a073bba7..5f086c0bc2 100644 --- a/frontend/src/components/workspace/browser-view/browser-view-panel.tsx +++ b/frontend/src/components/workspace/browser-view/browser-view-panel.tsx @@ -16,6 +16,7 @@ import { ConversationEmptyState } from "@/components/ai-elements/conversation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { resolveArtifactURL } from "@/core/artifacts/utils"; +import { useI18n } from "@/core/i18n/hooks"; import { isIMEComposing } from "@/lib/ime"; import { cn } from "@/lib/utils"; @@ -31,6 +32,7 @@ export function BrowserViewPanel({ threadId: string; className?: string; }) { + const { t } = useI18n(); const browserView = useMaybeBrowserView(); const frame = browserView?.latestFrame ?? null; const imageUrl = frame @@ -57,12 +59,8 @@ export function BrowserViewPanel({ }, [], ); - const { status, frameUrl, liveUrl, sendInput } = useBrowserStream( - threadId, - live, - streamSeedUrl, - handleNavRejected, - ); + const { status, closedReason, frameUrl, liveUrl, sendInput } = + useBrowserStream(threadId, live, streamSeedUrl, handleNavRejected); const panelRef = useRef(null); const surfaceRef = useRef(null); const stageRef = useRef(null); @@ -240,6 +238,12 @@ export function BrowserViewPanel({ } }; + const liveFailureMessage = + live && closedReason + ? closedReason === "handshake_timeout" + ? t.browserStream.handshakeTimeout + : t.browserStream.connectionFailed + : null; const liveActive = live && status === "open"; const liveConnecting = live && status === "connecting"; const displayUrl = live @@ -407,7 +411,11 @@ export function BrowserViewPanel({ title={live ? "Stop live control" : "Take live control"} > - {live ? (status === "open" ? "Live" : "…") : "Live"} + {live && status !== "open" + ? status === "closed" + ? t.browserStream.disconnected + : t.browserStream.connecting + : "Live"}