diff --git a/packages/app/e2e/workspace-navigation-regression.spec.ts b/packages/app/e2e/workspace-navigation-regression.spec.ts index 55a4eaf0c..f275e3c92 100644 --- a/packages/app/e2e/workspace-navigation-regression.spec.ts +++ b/packages/app/e2e/workspace-navigation-regression.spec.ts @@ -157,7 +157,7 @@ test.describe("Workspace navigation regression", () => { }); await daemonGate.drop(); - await expect(page.getByTestId("workspace-reconnect-banner")).toBeVisible({ + await expect(page.getByTestId("agent-reconnecting-toast")).toBeVisible({ timeout: 30_000, }); await expectWorkspaceHeader(page, { @@ -172,7 +172,7 @@ test.describe("Workspace navigation regression", () => { label: "host reconnect", }); daemonGate.restore(); - await expect(page.getByTestId("workspace-reconnect-banner")).toHaveCount(0, { + await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, { timeout: 30_000, }); await monitorReconnect; diff --git a/packages/app/src/components/toast-host.tsx b/packages/app/src/components/toast-host.tsx index 7612bb216..e4c5d55f2 100644 --- a/packages/app/src/components/toast-host.tsx +++ b/packages/app/src/components/toast-host.tsx @@ -18,7 +18,7 @@ export type ToastVariant = "default" | "success" | "error"; export interface ToastShowOptions { icon?: ReactNode; variant?: ToastVariant; - durationMs?: number; + durationMs?: number | null; nativeAndroid?: boolean; testID?: string; } @@ -29,7 +29,7 @@ export interface ToastState { nativeMessage: string | null; icon?: ReactNode; variant: ToastVariant; - durationMs: number; + durationMs: number | null; testID?: string; } @@ -59,11 +59,12 @@ export function useToastHost(): { } const variant = options?.variant ?? "default"; - const durationMs = options?.durationMs ?? DEFAULT_DURATION_MS; + const durationMs = options?.durationMs === undefined ? DEFAULT_DURATION_MS : options.durationMs; const nativeAndroid = options?.nativeAndroid ?? false; if (Platform.OS === "android" && nativeAndroid && nativeMessage) { - const duration = durationMs <= 2500 ? ToastAndroid.SHORT : ToastAndroid.LONG; + const duration = + durationMs !== null && durationMs <= 2500 ? ToastAndroid.SHORT : ToastAndroid.LONG; ToastAndroid.showWithGravity(nativeMessage, duration, ToastAndroid.TOP); return; } @@ -148,8 +149,13 @@ export function ToastViewport({ }, [clearTimer, onDismiss, opacity, translateY]); const scheduleDismiss = useCallback( - (durationMs: number) => { + (durationMs: number | null) => { clearTimer(); + if (durationMs === null) { + remainingDurationRef.current = 0; + dismissDeadlineRef.current = null; + return; + } const nextDurationMs = Math.max(0, durationMs); remainingDurationRef.current = nextDurationMs; dismissDeadlineRef.current = Date.now() + nextDurationMs; @@ -169,7 +175,7 @@ export function ToastViewport({ }, [clearTimer]); const resumeDismiss = useCallback(() => { - if (!toast) { + if (!toast || toast.durationMs === null) { return; } scheduleDismiss(remainingDurationRef.current || toast.durationMs); @@ -204,7 +210,6 @@ export function ToastViewport({ }), ]).start(); - remainingDurationRef.current = toast.durationMs; scheduleDismiss(toast.durationMs); return () => { diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index 9ab7a2925..633f588d6 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -708,7 +708,10 @@ function ChatAgentContent({ useEffect(() => { if (connectionStatus === "online") { - reconnectToastArmedRef.current = false; + if (reconnectToastArmedRef.current) { + reconnectToastArmedRef.current = false; + panelToast.dismiss(); + } return; } if (connectionStatus === "idle") { @@ -717,11 +720,11 @@ function ChatAgentContent({ if (!reconnectToastArmedRef.current) { reconnectToastArmedRef.current = true; panelToast.api.show("Reconnecting...", { - durationMs: 2200, + durationMs: null, testID: "agent-reconnecting-toast", }); } - }, [connectionStatus, panelToast.api]); + }, [connectionStatus, panelToast]); useEffect(() => { if (!isPaneFocused || !agentId || !isConnected || !hasSession) { diff --git a/packages/app/src/screens/workspace/workspace-route-state-views.tsx b/packages/app/src/screens/workspace/workspace-route-state-views.tsx index dbb27c0c0..2ecf73972 100644 --- a/packages/app/src/screens/workspace/workspace-route-state-views.tsx +++ b/packages/app/src/screens/workspace/workspace-route-state-views.tsx @@ -41,16 +41,6 @@ export function renderWorkspaceRouteGate(input: { } } -export function renderWorkspaceReconnectIndicator(input: { - state: WorkspaceRouteState; - onRetryHost: () => void; -}): React.ReactNode { - if (input.state.kind !== "reconnecting") { - return null; - } - return ; -} - function getWorkspaceHostStateTitle( state: Extract, ): string { @@ -63,15 +53,6 @@ function getWorkspaceHostStateTitle( return `Cannot reach ${state.hostName}`; } -function getReconnectBannerCopy( - state: Extract, -): string { - if (state.connectionStatus === "connecting" || state.connectionStatus === "idle") { - return `Reconnecting to ${state.hostName}...`; - } - return `${state.hostName} is offline`; -} - function WorkspaceConnecting({ hostName }: { hostName: string }) { const { theme } = useUnistyles(); @@ -135,33 +116,6 @@ function WorkspaceUnreachable({ ); } -function WorkspaceReconnectBanner({ - state, - onRetry, -}: { - state: Extract; - onRetry: () => void; -}) { - const { theme } = useUnistyles(); - const canRetry = state.connectionStatus === "offline" || state.connectionStatus === "error"; - const showSpinner = state.connectionStatus === "connecting" || state.connectionStatus === "idle"; - const isErrorState = state.connectionStatus === "offline" || state.connectionStatus === "error"; - - return ( - - {showSpinner ? : null} - - {getReconnectBannerCopy(state)} - - {canRetry ? ( - - ) : null} - - ); -} - function WorkspaceMissing({ hostName, onDismiss }: { hostName: string; onDismiss: () => void }) { return ( @@ -220,28 +174,4 @@ const styles = StyleSheet.create((theme) => ({ flexWrap: "wrap", gap: theme.spacing[2], }, - reconnectBanner: { - minHeight: 32, - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: theme.spacing[2], - paddingHorizontal: theme.spacing[3], - paddingVertical: theme.spacing[1], - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: theme.colors.border, - backgroundColor: theme.colors.surface1, - }, - reconnectText: { - minWidth: 0, - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - flexShrink: 1, - }, - reconnectTextDestructive: { - minWidth: 0, - color: theme.colors.destructive, - fontSize: theme.fontSize.sm, - flexShrink: 1, - }, })); diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 842af421a..b1eb0893e 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -129,10 +129,7 @@ import { resolveWorkspaceRouteState, type WorkspaceRouteState, } from "@/screens/workspace/workspace-route-state"; -import { - renderWorkspaceReconnectIndicator, - renderWorkspaceRouteGate, -} from "@/screens/workspace/workspace-route-state-views"; +import { renderWorkspaceRouteGate } from "@/screens/workspace/workspace-route-state-views"; import { deriveWorkspaceAgentVisibility, workspaceAgentVisibilityEqual, @@ -2929,10 +2926,6 @@ function WorkspaceScreenContent({ gate: workspaceScreenGate, workspaceKey: persistenceKey, }); - const reconnectBanner = renderWorkspaceReconnectIndicator({ - state: workspaceRouteState, - onRetryHost: handleRetryHost, - }); const headerRight = useMemo( () => ( @@ -3175,7 +3168,6 @@ function WorkspaceScreenContent({ /> - {reconnectBanner} {showScreenHeader && (