Skip to content
Closed
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
16 changes: 16 additions & 0 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ export function ReviewSheet(props: ReviewSheetProps) {
selectSection,
isSelectedSectionPending,
diffPreviewRevision,
gitFileCount,
includeGitChanges,
toggleGitChanges,
} = useReviewSections({
enabled: isEnvironmentReady,
environmentId,
Expand Down Expand Up @@ -736,6 +739,19 @@ export function ReviewSheet(props: ReviewSheetProps) {

<MaterialScreenContent>
<View className="flex-1 bg-sheet android:bg-sheet-solid">
{gitFileCount > 0 && (
<View className="flex-row flex-wrap items-center gap-2 border-b border-border px-4 py-2">
<Text className="text-xs text-muted-foreground">
{gitFileCount} {gitFileCount === 1 ? "file" : "files"} updated via Git{" "}
{includeGitChanges ? "shown" : "hidden"}.
</Text>
<Pressable accessibilityRole="button" onPress={toggleGitChanges}>
<Text className="text-sm text-primary">
{includeGitChanges ? "Hide Git changes" : "Show Git changes"}
</Text>
</Pressable>
</View>
)}
{showConnectionNotice ? (
<View className="flex-1" style={{ paddingTop: topContentInset }}>
<EnvironmentConnectionNotice
Expand Down
10 changes: 8 additions & 2 deletions apps/mobile/src/features/review/reviewModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles";
import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types";
import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints";
import {
partitionCheckpointFiles,
type ThreadCheckpointSummary,
} from "@t3tools/client-runtime/state/thread-checkpoints";
import type { ReviewDiffPreviewSource } from "@t3tools/contracts";
import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath";
import * as Arr from "effect/Array";
Expand Down Expand Up @@ -104,7 +107,10 @@ function checkpointSubtitle(checkpoint: ThreadCheckpointSummary): string {
if (checkpoint.status !== "ready") {
return `Diff ${checkpoint.status}`;
}
return `${fileCount} file${fileCount === 1 ? "" : "s"} changed`;
const groups = partitionCheckpointFiles(checkpoint.files);
return groups.gitFiles.length > 0
? `${groups.workspaceFiles.length} changed file${groups.workspaceFiles.length === 1 ? "" : "s"} · ${groups.gitFiles.length} updated via Git`
: `${fileCount} file${fileCount === 1 ? "" : "s"} changed`;
}

function compareCheckpointTurnCountDescending(
Expand Down
27 changes: 23 additions & 4 deletions apps/mobile/src/features/review/useReviewSections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import * as DateTime from "effect/DateTime";

import {
Expand Down Expand Up @@ -130,13 +130,19 @@ export function useReviewSections(input: {
const activeSectionId = activeCheckpoint
? getReviewSectionIdForCheckpoint(activeCheckpoint)
: null;
const gitScopeKey = `${environmentId}:${threadId}:${activeSectionId}`;
const [gitVisibility, setGitVisibility] = useState<{ scope: string; visible: boolean } | null>(
null,
);
const includeGitChanges = gitVisibility?.scope === gitScopeKey && gitVisibility.visible;
const activeTurnDiff = useCheckpointDiff({
environmentId: enabled ? (environmentId ?? null) : null,
threadId: enabled ? (threadId ?? null) : null,
fromTurnCount:
enabled && activeCheckpoint ? Math.max(0, activeCheckpoint.checkpointTurnCount - 1) : null,
toTurnCount: enabled ? (activeCheckpoint?.checkpointTurnCount ?? null) : null,
ignoreWhitespace: false,
includeGitChanges,
});

useEffect(() => {
Expand All @@ -150,9 +156,12 @@ export function useReviewSections(input: {
if (!reviewCache.threadKey || !activeSectionId || !activeTurnDiff.data) {
return;
}
setReviewTurnDiff(reviewCache.threadKey, activeSectionId, activeTurnDiff.data.diff);
// This section cache holds only the default view. Full patches stay in the query's keyed cache.
if (!includeGitChanges) {
setReviewTurnDiff(reviewCache.threadKey, activeSectionId, activeTurnDiff.data.diff);
}
setReviewAsyncError(reviewCache.threadKey, null);
}, [activeSectionId, activeTurnDiff.data, reviewCache.threadKey]);
}, [activeSectionId, activeTurnDiff.data, includeGitChanges, reviewCache.threadKey]);

useEffect(() => {
if (reviewCache.threadKey && activeTurnDiff.error) {
Expand Down Expand Up @@ -181,6 +190,9 @@ export function useReviewSections(input: {
);

return {
gitFileCount: selectedSection?.kind === "turn" ? (activeTurnDiff.data?.gitFileCount ?? 0) : 0,
includeGitChanges,
toggleGitChanges: () => setGitVisibility({ scope: gitScopeKey, visible: !includeGitChanges }),
error: diffPreview.error ?? activeTurnDiff.error ?? reviewCache.asyncState.error,
isSelectedSectionPending:
selectedSection?.kind === "turn" ? activeTurnDiff.isPending : diffPreview.isPending,
Expand All @@ -190,7 +202,14 @@ export function useReviewSections(input: {
: undefined,
loadingTurnIds,
reviewSections,
selectedSection,
selectedSection:
selectedSection?.kind === "turn"
? {
...selectedSection,
diff: activeTurnDiff.data?.diff ?? (includeGitChanges ? null : selectedSection.diff),
isLoading: activeTurnDiff.isPending,
}
: selectedSection,
refreshSelectedSection,
selectSection,
};
Expand Down
35 changes: 34 additions & 1 deletion apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ describe("buildThreadFeed", () => {
expect(buildThreadFeed([projected(reply, 0)])[0]?.type).toBe("message");
});

it("does not create a work group for a message and checkpoint", () => {
it("does not create a work group for a message and empty checkpoint", () => {
const checkpoint: OrchestrationV2TurnItem = {
...base("checkpoint", "2026-06-20T00:00:04.000Z", 3),
type: "checkpoint",
Expand All @@ -236,6 +236,39 @@ describe("buildThreadFeed", () => {
expect(feed.map((entry) => entry.type)).toEqual(["message"]);
});

it("keeps workspace and Git counts visible in the completed turn feed", () => {
const checkpoint: OrchestrationV2TurnItem = {
...base("checkpoint", "2026-06-20T00:00:04.000Z", 3),
type: "checkpoint",
checkpointId: CheckpointId.make("checkpoint"),
scopeId: CheckpointScopeId.make("scope"),
files: [
{ path: "welcome.txt", kind: "modified", additions: 1, deletions: 1 },
{ path: "upstream.txt", kind: "added", additions: 2, deletions: 0, origin: "git" },
],
};
const feed = deriveThreadFeedPresentation(
buildThreadFeed([projected(assistantMessage(), 0), projected(checkpoint, 1)]),
{
runId,
status: "completed",
startedAt: null,
completedAt: DateTime.formatIso(checkpoint.updatedAt),
},
new Set(),
);
const activities = feed.flatMap((entry) =>
entry.type === "activity-group" ? entry.activities : [],
);
expect(activities).toMatchObject([
{ detail: "1 changed file · 1 updated via Git", canExpand: true, fetchesDetail: false },
]);
expect(activities.map((activity) => workEntryRowLabel(activity.workEntry))).toEqual([
"1 changed file · 1 updated via Git",
]);
expect(activities[0]?.getFullDetail()).toBe("welcome.txt\nupstream.txt (updated via Git)");
});

it("omits cached tool output and patch bodies from expanded and copied activity", () => {
const rawOutput = "RAW_TOOL_OUTPUT";
const items: OrchestrationV2TurnItem[] = [
Expand Down
18 changes: 15 additions & 3 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { partitionCheckpointFiles } from "@t3tools/client-runtime/state/thread-checkpoints";
import type {
ThreadPendingApproval,
ThreadPendingUserInput,
Expand Down Expand Up @@ -431,6 +432,7 @@ function itemIsProminent(item: OrchestrationV2TurnItem): boolean {
item.type === "fork" ||
item.type === "thread_created" ||
item.type === "system_notice" ||
item.type === "checkpoint" ||
// An answerable card: it must stand alone and never fold away with the run.
item.type === "secret_request"
);
Expand Down Expand Up @@ -648,10 +650,14 @@ function itemPreview(item: OrchestrationV2TurnItem): string | null {
return item.prompt ?? null;
case "user_input_request":
return item.questions.map((question) => question.question).join(" · ") || null;
case "checkpoint":
case "checkpoint": {
const groups = partitionCheckpointFiles(item.files);
if (groups.gitFiles.length > 0)
return `${groups.workspaceFiles.length} changed file${groups.workspaceFiles.length === 1 ? "" : "s"} · ${groups.gitFiles.length} updated via Git`;
return item.files.length === 1
? (item.files[0]?.path ?? null)
: `${item.files.length} changed files`;
}
case "run_interrupt_request":
case "run_interrupt_result":
case "system_notice":
Expand Down Expand Up @@ -736,7 +742,7 @@ function toWorkLogEntry(
toolData: item,
};
case "checkpoint":
return { ...common, changedFiles: item.files.map((file) => file.path), toolData: item };
return { ...common, ...(detail ? { detail } : {}), toolData: item };
case "approval_request":
return {
...common,
Expand All @@ -760,6 +766,11 @@ export function formatItemFullDetail(
row: OrchestrationV2ProjectedTurnItem,
item: OrchestrationV2TurnItem,
): string {
if (item.type === "checkpoint") {
return item.files
.map((file) => `${file.path}${file.origin === "git" ? " (updated via Git)" : ""}`)
.join("\n");
}
return JSON.stringify(
{
visibility: row.visibility,
Expand Down Expand Up @@ -1715,7 +1726,8 @@ export function buildThreadFeed(
for (const row of visibleTurnItems) {
const item = row.item;
if (turnItemIsWorkspacePreparation(item)) continue;
if (item.type === "todo_list" || item.type === "checkpoint") continue;
if (item.type === "todo_list" || (item.type === "checkpoint" && item.files.length === 0))
continue;
if (item.type === "user_message" && foldedAnswerMessageIds.has(item.messageId)) continue;
// Match the web timeline: only the terminal interrupt result is useful to
// users; the preceding request is transient bookkeeping.
Expand Down
26 changes: 18 additions & 8 deletions apps/mobile/src/state/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,25 @@ export function useComposerPathSearch(target: ComposerPathSearchTarget) {
}

export function useCheckpointDiff(target: CheckpointDiffTarget) {
const {
environmentId,
fromTurnCount,
ignoreWhitespace,
includeGitChanges,
threadId,
toTurnCount,
} = target;
const targets = useMemo(
() => buildCheckpointDiffTargets(target),
[
target.environmentId,
target.fromTurnCount,
target.ignoreWhitespace,
target.threadId,
target.toTurnCount,
],
() =>
buildCheckpointDiffTargets({
environmentId,
fromTurnCount,
ignoreWhitespace,
includeGitChanges,
threadId,
toTurnCount,
}),
[environmentId, fromTurnCount, ignoreWhitespace, includeGitChanges, threadId, toTurnCount],
);
const fullThread = useEnvironmentQuery(
targets.fullThread === null
Expand Down
7 changes: 7 additions & 0 deletions apps/mobile/src/state/queryTargets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface CheckpointDiffTarget {
readonly fromTurnCount: number | null;
readonly toTurnCount: number | null;
readonly ignoreWhitespace: boolean;
readonly includeGitChanges?: boolean;
}

export function normalizeComposerPathSearchQuery(query: string | null): string {
Expand All @@ -30,6 +31,9 @@ export function buildCheckpointDiffTargets(target: CheckpointDiffTarget) {
threadId: target.threadId,
toTurnCount: target.toTurnCount,
ignoreWhitespace: target.ignoreWhitespace,
...(target.includeGitChanges === undefined
? {}
: { includeGitChanges: target.includeGitChanges }),
},
},
turn: null,
Expand All @@ -45,6 +49,9 @@ export function buildCheckpointDiffTargets(target: CheckpointDiffTarget) {
fromTurnCount: target.fromTurnCount,
toTurnCount: target.toTurnCount,
ignoreWhitespace: target.ignoreWhitespace,
...(target.includeGitChanges === undefined
? {}
: { includeGitChanges: target.includeGitChanges }),
},
},
} as const;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function layerFor(input: {
Layer.mergeAll(
Layer.mock(ThreadManagement.ThreadManagementService)({
getCheckpointContext: () => input.projection,
getThreadRecords: () => Effect.fail(new OrchestratorProjectionError({ threadId })),
}),
Layer.mock(CheckpointStore.CheckpointStore)({
diffCheckpoints: input.diffCheckpoints ?? (() => Effect.succeed("diff")),
Expand Down
Loading
Loading