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
5 changes: 4 additions & 1 deletion apps/mobile/src/connection/environment-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import * as Schema from "effect/Schema";
import * as MobileDatabase from "../persistence/mobile-database";

const SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION = 1;
const THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION = 2;
// v3 adds windowed (paginated) snapshots carrying `page` metadata; the bump
// makes pre-pagination clients discard the record instead of decoding a
// partial thread as complete (rollback safety).
const THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION = 3;
const SERVER_CONFIG_CACHE_SCHEMA_VERSION = 1;
const VCS_REFS_CACHE_SCHEMA_VERSION = 1;

Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface ThreadDetailScreenProps {
readonly connectionStateLabel: EnvironmentConnectionPhase;
/** Message sync status for the selected thread (drives the composer status pill). */
readonly threadSyncStatus?: EnvironmentThreadStatus;
/** Non-null when older turns exist beyond the loaded window. */
readonly loadEarlier?: { readonly loading: boolean; readonly onLoadEarlier: () => void } | null;
readonly activeThreadBusy: boolean;
readonly environmentId: EnvironmentId;
readonly projectWorkspaceRoot: string | null;
Expand Down Expand Up @@ -371,6 +373,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
usesAutomaticContentInsets={props.usesAutomaticContentInsets}
onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange}
skills={selectedProviderSkills}
loadEarlier={props.loadEarlier ?? null}
/>
</View>
) : (
Expand Down
20 changes: 19 additions & 1 deletion apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ export interface ThreadFeedProps {
readonly usesAutomaticContentInsets?: boolean;
readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void;
readonly skills?: ReadonlyArray<SelectableMarkdownSkill>;
/** Non-null when older turns exist beyond the loaded window. */
readonly loadEarlier?: {
readonly loading: boolean;
readonly onLoadEarlier: () => void;
} | null;
}

function MessageAttachmentImage(props: {
Expand Down Expand Up @@ -1893,7 +1898,20 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
onScroll={handleScroll}
scrollEventThrottle={16}
ListHeaderComponent={
usesNativeAutomaticInsets ? null : <View style={{ height: topContentInset }} />
<>
{usesNativeAutomaticInsets ? null : <View style={{ height: topContentInset }} />}
{props.loadEarlier != null ? (
<Pressable
onPress={props.loadEarlier.onLoadEarlier}
disabled={props.loadEarlier.loading}
className="items-center py-2"
>
<Text className="text-xs text-foreground-secondary">
{props.loadEarlier.loading ? "Loading earlier turns…" : "Load earlier turns"}
</Text>
</Pressable>
) : null}
</>
}
contentContainerStyle={{
paddingTop: 12,
Expand Down
19 changes: 19 additions & 0 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import * as Option from "effect/Option";
import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts";
import {
requestOlderThreadTurns,
threadHasOlderTurns,
} from "@t3tools/client-runtime/state/threads";
import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";
import { Platform, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
Expand Down Expand Up @@ -190,6 +194,20 @@ function ThreadRouteContent(
useThreadSelection();
const selectedThreadDetailState = props.selectedThreadDetailState;
const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data);
// "Load earlier turns" header state for windowed (paginated) thread loads.
const loadEarlierTurns = useMemo(() => {
if (selectedThread === null || !threadHasOlderTurns(selectedThreadDetailState)) {
return null;
}
return {
loading:
selectedThreadDetailState.page._tag === "Some" &&
selectedThreadDetailState.page.value.loadingOlder,
onLoadEarlier: () => {
requestOlderThreadTurns(selectedThread.environmentId, selectedThread.id);
},
};
}, [selectedThread, selectedThreadDetailState]);
const { selectedThreadCwd } = useSelectedThreadWorktree();
const composer = useThreadComposerState();
const gitState = useSelectedThreadGitState();
Expand Down Expand Up @@ -766,6 +784,7 @@ function ThreadRouteContent(
draftAttachments={composer.draftAttachments}
connectionStateLabel={routeConnectionState}
threadSyncStatus={selectedThreadDetailState.status}
loadEarlier={loadEarlierTurns}
activeThreadBusy={composer.activeThreadBusy}
environmentId={selectedThread.environmentId}
projectWorkspaceRoot={selectedThreadProject?.workspaceRoot ?? null}
Expand Down
Loading
Loading