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 && (