Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 1 addition & 81 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
useState,
type RefObject,
} from "react";
import { ActivityIndicator, Alert, Platform, Pressable, View, type ViewStyle } from "react-native";
import { Alert, Platform, Pressable, View, type ViewStyle } from "react-native";
import { FilePreviewModal, type FilePreviewSource } from "../../components/FilePreviewModal";
import {
composerAttachmentUploadBlockReason,
Expand Down Expand Up @@ -113,7 +113,6 @@ export interface ThreadComposerProps {
readonly contentMaxWidth?: number;
readonly bottomInset?: number;
readonly connectionState: RemoteClientConnectionState;
readonly connectionError: string | null;
readonly environmentLabel: string | null;
readonly selectedThread: OrchestrationThreadShell;
readonly hasCompactableConversation: boolean;
Expand All @@ -134,7 +133,6 @@ export interface ThreadComposerProps {
readonly onUpdateModelSelection: (modelSelection: ModelSelection) => void;
readonly onUpdateRuntimeMode: (runtimeMode: RuntimeMode) => void;
readonly onUpdateInteractionMode: (interactionMode: ProviderInteractionMode) => void;
readonly onReconnectEnvironment: () => void;
readonly onExpandedChange?: (expanded: boolean) => void;
/** Fires on editor focus/blur; hosts use it to vet stale keyboard state. */
readonly onEditorFocusChange?: (focused: boolean) => void;
Expand Down Expand Up @@ -226,72 +224,6 @@ export function ComposerSurface(props: {
);
}

type ComposerStatusPillState = {
readonly kind: "unavailable" | "reconnecting";
readonly label: string;
};

function composerConnectionStatus(input: {
readonly connectionError: string | null;
readonly connectionState: RemoteClientConnectionState;
readonly environmentLabel: string | null;
}): ComposerStatusPillState | null {
const environmentLabel = input.environmentLabel ?? "Environment";

switch (input.connectionState) {
case "connecting":
case "reconnecting":
return {
kind: "reconnecting",
label:
input.connectionError === null
? `Reconnecting to ${environmentLabel}...`
: `Failed to connect. Retrying ${environmentLabel}...`,
};
case "offline":
return { kind: "unavailable", label: "You are offline" };
case "error":
return {
kind: "unavailable",
label: input.connectionError
? `Failed to connect to ${environmentLabel}: ${input.connectionError}`
: `Failed to connect to ${environmentLabel}`,
};
case "available":
return { kind: "unavailable", label: `${environmentLabel} is not connected` };
case "connected":
return null;
}
}

const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(props: {
readonly onPress: () => void;
readonly status: ComposerStatusPillState;
}) {
const isReconnecting = props.status.kind === "reconnecting";
return (
<View className="absolute inset-x-0 bottom-full items-center pb-2" pointerEvents="box-none">
<Pressable
accessibilityRole="button"
onPress={props.onPress}
className="max-w-full flex-row items-center gap-2 rounded-full bg-card px-3 py-2 shadow-sm active:opacity-70"
>
{isReconnecting ? (
<ActivityIndicator size="small" colorClassName={"accent-icon-muted"} />
) : (
<View className="h-2 w-2 rounded-full bg-red-500" />
)}
<Text
className="max-w-[260px] text-sm font-t3-bold leading-snug text-foreground"
numberOfLines={1}
>
{props.status.label}
</Text>
</Pressable>
</View>
);
});

export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
const navigation = useNavigation();
const foregroundColor = useUniwindTheme()["--color-foreground"];
Expand Down Expand Up @@ -337,11 +269,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
const modelUnavailable =
props.connectionState === "connected" &&
isModelSelectionUnavailable(props.serverConfig, currentModelSelection);
const connectionStatus = composerConnectionStatus({
connectionError: props.connectionError,
connectionState: props.connectionState,
environmentLabel: props.environmentLabel,
});
const selectedProviderStatus = useMemo(() => {
if (!props.serverConfig) return null;
return (
Expand Down Expand Up @@ -640,13 +567,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
</View>
) : null}

{connectionStatus ? (
<ComposerConnectionStatusPill
status={connectionStatus}
onPress={props.onReconnectEnvironment}
/>
) : null}

{modelUnavailable ? (
<Pressable accessibilityRole="button" className="px-3 py-2" onPress={openSettings}>
<Text className="text-xs text-foreground">Model unavailable. Open model settings.</Text>
Expand Down
24 changes: 14 additions & 10 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ import { PendingUserInputCard } from "./PendingUserInputCard";
import {
FLOATING_WORKING_CONTROL_COVERAGE,
FloatingWorkingControl,
type FloatingWorkingStatus,
} from "./floating-working-control";
import { connectionFloatingStatus, type FloatingWorkingStatus } from "./floating-working-status";
import {
derivePendingUserInputMaxHeight,
ESTIMATED_KEYBOARD_HEIGHT,
Expand Down Expand Up @@ -331,14 +331,20 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
return null;
}
})();
// One floating pill above the composer: it reads the sync state while
// messages load, then the working timer once the feed is settled.
// One floating pill above the composer: it reads the connection phase while
// disconnected, the sync state while messages load, then the working timer
// once the feed is settled.
const floatingStatus = ((): FloatingWorkingStatus | null => {
if (
props.connectionStateLabel !== "connected" ||
props.activePendingApproval !== null ||
props.activePendingUserInput !== null
) {
const connectionStatus = connectionFloatingStatus({
connectionError: props.connectionError,
connectionState: props.connectionStateLabel,
environmentLabel: props.environmentLabel,
onReconnect: props.onReconnectEnvironment,
});
if (connectionStatus !== null) {
return connectionStatus;
}
if (props.activePendingApproval !== null || props.activePendingUserInput !== null) {
return null;
}
if (threadSyncLabel !== null) {
Expand Down Expand Up @@ -964,7 +970,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
placeholder="Ask the repo agent, or run a command…"
contentMaxWidth={contentMaxWidth}
connectionState={props.connectionStateLabel}
connectionError={props.connectionError}
environmentLabel={props.environmentLabel}
selectedThread={props.selectedThread}
hasCompactableConversation={hasCompactableConversation && !props.isCompacting}
Expand All @@ -981,7 +986,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
onStopThread={props.onStopThread}
onSendMessage={handleSendMessage}
onShowUsageLimits={showUsageLimits}
onReconnectEnvironment={props.onReconnectEnvironment}
onUpdateModelSelection={props.onUpdateThreadModelSelection}
onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}
onUpdateInteractionMode={props.onUpdateThreadInteractionMode}
Expand Down
Loading
Loading