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
73 changes: 73 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,41 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
assert.deepStrictEqual(paths, ["complete.txt", "final.txt"]);
}),
);

it.effect("honors whitespace filtering for worktree and branch previews", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["checkout", "-b", "feature/whitespace"]);
yield* writeTextFile(cwd, "README.md", "# test\n");
yield* git(cwd, ["add", "README.md"]);
yield* git(cwd, ["commit", "-m", "change whitespace"]);
yield* writeTextFile(cwd, "README.md", "# test\n");

const included = yield* driver.getReviewDiffPreview({
cwd,
baseRef: initialBranch,
ignoreWhitespace: false,
});
const ignored = yield* driver.getReviewDiffPreview({
cwd,
baseRef: initialBranch,
ignoreWhitespace: true,
});

assert.isNotEmpty(included.sources.find((source) => source.kind === "working-tree")?.diff);
assert.isNotEmpty(included.sources.find((source) => source.kind === "branch-range")?.diff);
assert.strictEqual(
ignored.sources.find((source) => source.kind === "working-tree")?.diff,
"",
);
assert.strictEqual(
ignored.sources.find((source) => source.kind === "branch-range")?.diff,
"",
);
}),
);
});

describe("repository status", () => {
Expand Down Expand Up @@ -342,6 +377,44 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
});

describe("refName operations", () => {
it.effect("optionally includes remote refs that match local branches", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-vcs-driver-remote-");
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "-u", "origin", initialBranch]);
const driver = yield* GitVcsDriver.GitVcsDriver;

const deduplicated = yield* driver.listRefs({ cwd });
assert.equal(
deduplicated.refs.some((ref) => ref.name === `origin/${initialBranch}`),
false,
);

const complete = yield* driver.listRefs({ cwd, includeMatchingRemoteRefs: true });
assert.equal(
complete.refs.some((ref) => ref.name === initialBranch),
true,
);
assert.equal(
complete.refs.some((ref) => ref.name === `origin/${initialBranch}`),
true,
);

const remoteOnly = yield* driver.listRefs({
cwd,
includeMatchingRemoteRefs: true,
refKind: "remote",
limit: 1,
});
assert.equal(remoteOnly.refs.length, 1);
assert.equal(remoteOnly.refs[0]?.name, `origin/${initialBranch}`);
assert.equal(remoteOnly.refs[0]?.isRemote, true);
}),
);

it.effect("creates, checks out, renames, and lists refs", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
31 changes: 25 additions & 6 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1817,7 +1817,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const dirtyTrackedResult = yield* executeGit(
"GitVcsDriver.getReviewDiffPreview.dirtyTracked",
input.cwd,
["diff", "--patch", "--minimal", "HEAD", "--"],
[
"diff",
"--patch",
"--minimal",
...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
"HEAD",
"--",
],
{
maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
Expand All @@ -1843,7 +1850,13 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
? yield* executeGit(
"GitVcsDriver.getReviewDiffPreview.base",
input.cwd,
["diff", "--patch", "--minimal", `${baseRef}...HEAD`],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace toggle skips untracked

Medium Severity

ignoreWhitespace adds --ignore-all-space only to tracked git diff calls. Unstaged worktree previews still merge readUntrackedReviewDiffs output without that flag, so enabling ignore-whitespace can leave untracked (or mixed) unstaged diffs unchanged.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 104bb7a. Configure here.

[
"diff",
"--patch",
"--minimal",
...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
`${baseRef}...HEAD`,
],
{
maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES,
appendTruncationMarker: true,
Expand Down Expand Up @@ -2127,11 +2140,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
})
: [];

const allBranches = input.includeMatchingRemoteRefs
? [...localBranches, ...remoteBranches]
: dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]);
const branchesForKind =
input.refKind === "local"
? allBranches.filter((ref) => !ref.isRemote)
: input.refKind === "remote"
? allBranches.filter((ref) => ref.isRemote)
: allBranches;
const refs = paginateBranches({
refs: filterBranchesForListQuery(
dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),
input.query,
),
refs: filterBranchesForListQuery(branchesForKind, input.query),
cursor: input.cursor,
limit: input.limit,
});
Expand Down
133 changes: 13 additions & 120 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { nextTerminalId, resolveTerminalSessionLabel } from "@t3tools/shared/ter
import { Debouncer } from "@tanstack/react-pacer";
import { useAtomValue } from "@effect/atom-react";
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useNavigate } from "@tanstack/react-router";
import { useShallow } from "zustand/react/shallow";
import {
isAtomCommandInterrupted,
Expand All @@ -55,7 +55,7 @@ import * as Cause from "effect/Cause";
import { AsyncResult } from "effect/unstable/reactivity";
import { isElectron } from "../env";
import { readLocalApi } from "../localApi";
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
import { useDiffPanelStore } from "../diffPanelStore";
import {
collapseExpandedComposerCursor,
parseStandaloneComposerSlashCommand,
Expand Down Expand Up @@ -104,7 +104,7 @@ import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";
import {
selectActiveRightPanelKindWithUrl,
selectActiveRightPanel,
selectActiveRightPanelSurface,
selectThreadRightPanelState,
type RightPanelSurface,
Expand Down Expand Up @@ -1034,10 +1034,6 @@ function ChatViewContent(props: ChatViewProps) {
const timestampFormat = settings.timestampFormat;
const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;
const navigate = useNavigate();
const rawSearch = useSearch({
strict: false,
select: (params) => parseDiffRouteSearch(params),
});
const { resolvedTheme } = useTheme();
// Granular store selectors — avoid subscribing to prompt changes.
const composerRuntimeMode = useComposerDraftStore(
Expand Down Expand Up @@ -1217,7 +1213,6 @@ function ChatViewContent(props: ChatViewProps) {
composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
const canCheckoutPullRequestIntoThread = isLocalDraftThread;
const diffOpen = rawSearch.diff === "1";
const activeThreadId = activeThread?.id ?? null;
const runningTerminalIds = useThreadRunningTerminalIds({
environmentId: activeThread?.environmentId ?? null,
Expand Down Expand Up @@ -1259,8 +1254,9 @@ function ChatViewContent(props: ChatViewProps) {
);
const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;
const activeRightPanelKind = useRightPanelStore((state) =>
selectActiveRightPanelKindWithUrl(state.byThreadKey, activeThreadRef, diffOpen),
selectActiveRightPanel(state.byThreadKey, activeThreadRef),
);
const diffOpen = activeRightPanelKind === "diff";
const rightPanelState = useRightPanelStore((state) =>
selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
);
Expand Down Expand Up @@ -1295,11 +1291,6 @@ function ChatViewContent(props: ChatViewProps) {

const planSidebarOpen = activeRightPanelKind === "plan";

useEffect(() => {
if (!activeThreadRef || !diffOpen) return;
useRightPanelStore.getState().open(activeThreadRef, "diff");
}, [activeThreadRef, diffOpen]);

const existingOpenTerminalThreadKeys = useMemo(() => {
const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));
Expand Down Expand Up @@ -2151,27 +2142,7 @@ function ChatViewContent(props: ChatViewProps) {
if (activeThreadRef) {
useRightPanelStore.getState().toggle(activeThreadRef, "diff");
}
void navigate({
to: "/$environmentId/$threadId",
params: {
environmentId,
threadId,
},
replace: true,
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" };
},
});
}, [
activeThreadRef,
diffOpen,
environmentId,
isServerThread,
navigate,
onDiffPanelOpen,
threadId,
]);
}, [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen]);

const envLocked = Boolean(
activeThread &&
Expand Down Expand Up @@ -2757,21 +2728,7 @@ function ChatViewContent(props: ChatViewProps) {
if (!activeThreadRef || !isServerThread || !isGitRepo) return;
useRightPanelStore.getState().open(activeThreadRef, "diff");
onDiffPanelOpen?.();
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => ({ ...stripDiffSearchParams(previous), diff: "1" }),
});
}, [
activeThreadRef,
environmentId,
isGitRepo,
isServerThread,
navigate,
onDiffPanelOpen,
threadId,
]);
}, [activeThreadRef, isGitRepo, isServerThread, onDiffPanelOpen]);
const addFilesSurface = useCallback(() => {
if (!activeThreadRef || !activeProject) return;
useRightPanelStore.getState().open(activeThreadRef, "files");
Expand All @@ -2789,30 +2746,13 @@ function ChatViewContent(props: ChatViewProps) {
useRightPanelStore.getState().close(activeThreadRef);
return;
}
if (diffOpen) {
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
});
}
const activeTabId = activePreviewState.activeTabId;
if (activeTabId) {
useRightPanelStore.getState().openBrowser(activeThreadRef, activeTabId);
} else {
createBrowserSurface();
}
}, [
activePreviewState.activeTabId,
activeThreadRef,
createBrowserSurface,
diffOpen,
environmentId,
navigate,
previewPanelOpen,
threadId,
]);
}, [activePreviewState.activeTabId, activeThreadRef, createBrowserSurface, previewPanelOpen]);
const closePreviewPanel = useCallback(() => {
if (activeThreadRef) {
setMaximizedRightPanelThreadKey(null);
Expand Down Expand Up @@ -2936,31 +2876,9 @@ function ChatViewContent(props: ChatViewProps) {
}
if (surface.kind === "diff" && !diffOpen) {
onDiffPanelOpen?.();
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => ({ ...stripDiffSearchParams(previous), diff: "1" }),
});
} else if (surface.kind !== "diff" && diffOpen) {
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
});
}
},
[
activeThreadRef,
diffOpen,
dismissPlanSidebarForCurrentTurn,
environmentId,
navigate,
onDiffPanelOpen,
planSidebarOpen,
threadId,
],
[activeThreadRef, diffOpen, dismissPlanSidebarForCurrentTurn, onDiffPanelOpen, planSidebarOpen],
);
const toggleRightPanel = useCallback(() => {
if (!activeThreadRef) return;
Expand Down Expand Up @@ -3006,26 +2924,14 @@ function ChatViewContent(props: ChatViewProps) {
}
}
}
if (diffOpen && surfaces.some((surface) => surface.kind === "diff")) {
void navigate({
to: "/$environmentId/$threadId",
params: { environmentId, threadId },
replace: true,
search: (previous) => ({ ...stripDiffSearchParams(previous), diff: undefined }),
});
}
},
[
activeThreadRef,
activePreviewState.sessions,
closePreview,
closeTerminalMutation,
diffOpen,
dismissPlanSidebarForCurrentTurn,
environmentId,
navigate,
storeCloseTerminal,
threadId,
],
);
const syncActivePreviewSurface = useCallback(() => {
Expand Down Expand Up @@ -4631,25 +4537,12 @@ function ChatViewContent(props: ChatViewProps) {
}, []);
const onOpenTurnDiff = useCallback(
(turnId: TurnId, filePath?: string) => {
if (!isServerThread) {
return;
}
if (!isServerThread || !activeThreadRef) return;
useDiffPanelStore.getState().selectTurn(activeThreadRef, turnId, filePath);
useRightPanelStore.getState().open(activeThreadRef, "diff");
onDiffPanelOpen?.();
void navigate({
to: "/$environmentId/$threadId",
params: {
environmentId,
threadId,
},
search: (previous) => {
const rest = stripDiffSearchParams(previous);
return filePath
? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
: { ...rest, diff: "1", diffTurnId: turnId };
},
});
},
[environmentId, isServerThread, navigate, onDiffPanelOpen, threadId],
[activeThreadRef, isServerThread, onDiffPanelOpen],
);
// Both the Map and the revert handler are read from refs at call-time so
// the callback reference is fully stable and never busts context identity.
Expand Down
Loading
Loading