Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner. * fix(chat): keep older history loading reliably Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { TFunction } from "i18next";
|
||||
import { SquarePen } from "lucide-react-native";
|
||||
@@ -11,7 +12,7 @@ import React, {
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, StyleSheet as RNStyleSheet, Text, View } from "react-native";
|
||||
import { StyleSheet as RNStyleSheet, Text, View } from "react-native";
|
||||
import ReanimatedAnimated from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
@@ -215,7 +216,7 @@ function renderChatAgentNonReadyView(args: {
|
||||
return (
|
||||
<View style={styles.container} testID="agent-loading">
|
||||
<View style={styles.errorContainer}>
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -716,7 +717,7 @@ function AgentPanelBody({
|
||||
return (
|
||||
<View style={styles.container} testID="agent-loading">
|
||||
<View style={styles.errorContainer}>
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -1246,7 +1247,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
|
||||
{showHistorySyncOverlay ? (
|
||||
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -1255,7 +1256,7 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({
|
||||
|
||||
{isArchivingCurrentAgent ? (
|
||||
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundColorMapping} />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundColorMapping} />
|
||||
<Text style={styles.archivingTitle}>{t("agentPanel.states.archivingTitle")}</Text>
|
||||
<Text style={styles.archivingSubtitle}>{t("agentPanel.states.archivingSubtitle")}</Text>
|
||||
</View>
|
||||
@@ -1600,7 +1601,7 @@ function AgentSessionUnavailableState({
|
||||
<View style={styles.centerState}>
|
||||
{isConnecting || isPreparingSession ? (
|
||||
<>
|
||||
<ActivityIndicator size="large" />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<Text style={styles.loadingText}>
|
||||
{isPreparingSession
|
||||
? t("agentPanel.unavailable.preparingSession", { serverLabel })
|
||||
@@ -1628,7 +1629,7 @@ function AgentSessionUnavailableState({
|
||||
);
|
||||
}
|
||||
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||
|
||||
const foregroundMutedColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.foregroundMuted,
|
||||
|
||||
@@ -130,6 +130,9 @@ function ProviderSubagentPanel() {
|
||||
target.subagentId,
|
||||
timeline,
|
||||
]);
|
||||
const firstTimelineSeq = timeline?.rows.size ? Math.min(...timeline.rows.keys()) : null;
|
||||
const progressKey =
|
||||
timeline?.epoch && firstTimelineSeq !== null ? `${timeline.epoch}:${firstTimelineSeq}` : null;
|
||||
|
||||
const streamContext = useMemo<AgentScreenAgent>(
|
||||
() => ({
|
||||
@@ -147,9 +150,10 @@ function ProviderSubagentPanel() {
|
||||
() => ({
|
||||
hasOlder: timeline?.hasOlder === true,
|
||||
isLoadingOlder,
|
||||
progressKey,
|
||||
onLoadOlder: loadOlder,
|
||||
}),
|
||||
[isLoadingOlder, loadOlder, timeline?.hasOlder],
|
||||
[isLoadingOlder, loadOlder, progressKey, timeline?.hasOlder],
|
||||
);
|
||||
|
||||
if (serverInfo && !supported) {
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CheckCircle2, ChevronRight, CircleAlert, SquareTerminal } from "lucide-react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Pressable, type PressableStateCallbackType, ScrollView, Text, View } from "react-native";
|
||||
import invariant from "tiny-invariant";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { usePaneContext } from "@/panels/pane-context";
|
||||
@@ -69,7 +63,7 @@ type CommandStatus = "running" | "completed" | "failed";
|
||||
|
||||
function CommandStatusIcon({ status }: { status: CommandStatus }) {
|
||||
if (status === "running") {
|
||||
return <ThemedActivityIndicator size={14} uniProps={foregroundColorMapping} />;
|
||||
return <ThemedLoadingSpinner size={14} uniProps={foregroundColorMapping} />;
|
||||
}
|
||||
if (status === "completed") {
|
||||
return <ThemedCheckCircle2 size={14} uniProps={greenColorMapping} />;
|
||||
@@ -247,7 +241,7 @@ function SetupPanel() {
|
||||
|
||||
{isWaiting ? (
|
||||
<View style={styles.waitingContainer}>
|
||||
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedLoadingSpinner size="large" uniProps={foregroundMutedColorMapping} />
|
||||
<Text style={styles.waitingText}>{t("workspace.setup.waiting")}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -449,7 +443,7 @@ function TopLevelSetupError({
|
||||
);
|
||||
}
|
||||
|
||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||
const ThemedCheckCircle2 = withUnistyles(CheckCircle2);
|
||||
const ThemedCircleAlert = withUnistyles(CircleAlert);
|
||||
const ThemedChevronRight = withUnistyles(ChevronRight);
|
||||
|
||||
Reference in New Issue
Block a user