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
21 changes: 2 additions & 19 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ import {
} from "../../state/composer-attachment-uploads";
import Animated, {
FadeIn,
FadeInDown,
FadeOut,
FadeOutDown,
LinearTransition,
ReduceMotion,
useAnimatedStyle,
Expand Down Expand Up @@ -272,12 +270,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
}) {
const isReconnecting = props.status.kind === "reconnecting";
return (
<Animated.View
className="absolute inset-x-0 bottom-full items-center pb-2"
entering={FadeInDown.duration(180)}
exiting={FadeOutDown.duration(140)}
pointerEvents="box-none"
>
<View className="absolute inset-x-0 bottom-full items-center pb-2" pointerEvents="box-none">
<Pressable
accessibilityRole="button"
onPress={props.onPress}
Expand All @@ -295,7 +288,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
{props.status.label}
</Text>
</Pressable>
</Animated.View>
</View>
);
});

Expand Down Expand Up @@ -891,16 +884,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
</ComposerDictationToolbar>
</Animated.View>
</ComposerSurface>

{/* Queue count */}
{props.queueCount > 0 ? (
<Animated.View entering={FadeIn.duration(180)} exiting={FadeOut.duration(120)}>
<Text className="pt-2 text-xs text-foreground-muted">
{props.queueCount} queued message{props.queueCount === 1 ? "" : "s"} will send
automatically.
</Text>
</Animated.View>
) : null}
</Animated.View>

<VideoPreviewModal source={previewVideo} onRequestClose={closePreview} />
Expand Down
57 changes: 47 additions & 10 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
useState,
} from "react";
import {
Alert,
AppState,
Keyboard,
Platform,
Expand Down Expand Up @@ -67,6 +68,8 @@ import type { StatusTone } from "../../components/StatusPill";
import type { DraftComposerAttachment } from "../../lib/composerImages";
import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout";
import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
import { editPendingThreadMessage } from "../../state/edit-pending-thread-message";
import type { QueuedThreadMessage } from "../../state/thread-outbox-model";
import { scopedThreadKey } from "../../lib/scopedEntities";
import type {
PendingApproval,
Expand Down Expand Up @@ -127,6 +130,8 @@ export interface ThreadDetailScreenProps {
readonly projectWorkspaceRoot: string | null;
readonly threadCwd: string | null;
readonly selectedThreadQueueCount: number;
readonly queuedMessages: ReadonlyArray<QueuedThreadMessage>;
readonly dispatchingMessageId: MessageId | null;
readonly serverConfig: T3ServerConfig | null;
readonly layoutVariant?: LayoutVariant;
readonly usesAutomaticContentInsets?: boolean;
Expand Down Expand Up @@ -347,6 +352,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
return null;
})();
const showWorkingControl = floatingStatus !== null;
// Connection and working status occupy the same space. Keep the feed inset
// stable when reconnecting hands off to syncing and then to a running turn.
const showFloatingStatus =
showWorkingControl ||
props.connectionStateLabel !== "connected" ||
props.queuedMessages.length > 0 ||
props.selectedThreadFeed.some(
(entry) => "acknowledged" in entry && entry.acknowledged === true,
);
const selectedThreadFeed = props.selectedThreadFeed;
const hasCompactableConversation =
selectedThreadFeed.some(
Expand Down Expand Up @@ -486,14 +500,14 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
const userInputInsetProgress = useSharedValue(1);
const userInputCardCoverage = useSharedValue(0);
const floatingControlCoverage = useSharedValue(
showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0,
showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0,
);
useEffect(() => {
floatingControlCoverage.value = withTiming(
showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0,
showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0,
{ duration: 180, reduceMotion: ReduceMotion.System },
);
}, [floatingControlCoverage, showWorkingControl]);
}, [floatingControlCoverage, showFloatingStatus]);
// Android renders the expanded card in-flow (it cannot hit-test the iOS
// overlay outside the bar's bounds), so its measured overlay height already
// includes the card — the coverage extra is iOS-only.
Expand Down Expand Up @@ -553,20 +567,20 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
useEffect(() => {
const previous = previousWorkingControlStateRef.current;
const threadChanged = previous.threadKey !== selectedThreadKey;
const visibilityChanged = previous.visible !== showWorkingControl;
const visibilityChanged = previous.visible !== showFloatingStatus;
previousWorkingControlStateRef.current = {
threadKey: selectedThreadKey,
visible: showWorkingControl,
visible: showFloatingStatus,
};
if ((!threadChanged && !visibilityChanged) || (threadChanged && !showWorkingControl)) {
if ((!threadChanged && !visibilityChanged) || (threadChanged && !showFloatingStatus)) {
return;
}
// LegendList applies the larger inset but does not re-anchor short
// followed conversations when this floating coverage changes after the
// initial load. Re-pin after the finite inset transition; the callback
// checks follow state again so a user who scrolled up stays put.
scheduleOverlayRepin(230);
}, [scheduleOverlayRepin, selectedThreadKey, showWorkingControl]);
}, [scheduleOverlayRepin, selectedThreadKey, showFloatingStatus]);
const handleToggleUserInputCollapsed = useCallback(() => {
if (activeUserInputRequestId === null) {
return;
Expand Down Expand Up @@ -633,11 +647,13 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
useEffect(() => {
if (
submittedMessageId === null ||
anchorMessageId !== submittedMessageId ||
lastScrolledSubmittedMessageIdRef.current === submittedMessageId ||
contentPresentationKind !== "ready" ||
!selectedThreadFeed.some(
(!selectedThreadFeed.some(
(entry) => entry.type === "message" && entry.id === submittedMessageId,
)
) &&
!props.queuedMessages.some((message) => message.messageId === submittedMessageId))
) {
return;
}
Expand Down Expand Up @@ -676,9 +692,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
});
return () => cancelAnimationFrame(frame);
}, [
anchorMessageId,
submittedMessageId,
freeze,
contentPresentationKind,
props.queuedMessages,
selectedThreadFeed,
scrollMessageToEnd,
selectedThreadKey,
Expand Down Expand Up @@ -719,6 +737,22 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
selectedThreadKey,
]);

const handleEditPendingMessage = useCallback(async (message: QueuedThreadMessage) => {
try {
if (
(await editPendingThreadMessage(message)) &&
selectedThreadKeyRef.current === scopedThreadKey(message.environmentId, message.threadId)
) {
composerEditorRef.current?.focus();
}
} catch (error) {
Alert.alert(
"Could not edit message",
error instanceof Error ? error.message : "Please try again.",
);
}
}, []);

const collapseComposer = useCallback(() => {
composerEditorRef.current?.blur();
}, []);
Expand Down Expand Up @@ -796,6 +830,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
threadId={props.selectedThread.id}
workspaceRoot={props.threadCwd}
feed={props.selectedThreadFeed}
queuedMessages={props.queuedMessages}
dispatchingMessageId={props.dispatchingMessageId}
onEditPendingMessage={handleEditPendingMessage}
contentPresentation={props.contentPresentation}
agentLabel={agentLabel}
latestTurn={props.selectedThread.latestTurn}
Expand All @@ -807,7 +844,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
contentInsetEndAdjustment={combinedContentInsetEndAdjustment}
contentTopInset={0}
contentBottomInset={
estimatedOverlayHeight + (showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0)
estimatedOverlayHeight + (showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0)
}
contentMaxWidth={contentMaxWidth}
layoutVariant={layoutVariant}
Expand Down
Loading
Loading