Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dfa3a14
fix(web): Changes row follows the base picked in the diff panel
whoisaldo Oct 14, 2026
5bef172
Merge remote-tracking branch 'origin/main' into fix/changes-row-base-ref
whoisaldo Oct 16, 2026
17d5d75
test(web): cover changes row preview lifecycle
juliusmarminge Oct 19, 2026
5d85016
Merge pinned main into fix/changes-row-base-ref
juliusmarminge Oct 19, 2026
6175642
fix(web): keep custom Changes totals fresh
juliusmarminge Oct 19, 2026
026294f
test(web): move Changes row coverage to browser suite
juliusmarminge Oct 19, 2026
d5c8fbe
fix(web): avoid redundant Changes preview refreshes
juliusmarminge Oct 19, 2026
97f5e64
test(web): cover status-identical changes row mutations
juliusmarminge Oct 19, 2026
06adeb4
fix(web): refresh Changes totals after workspace mutations
juliusmarminge Oct 19, 2026
afc252b
chore: merge main into fix/changes-row-base-ref
juliusmarminge Oct 19, 2026
11dc700
Merge remote-tracking branch 'origin/main' into L71-19553
juliusmarminge Oct 20, 2026
a169940
test(web): pass workspaceMutationId in ThreadDetailsPanel browser test
juliusmarminge Oct 20, 2026
d0774d3
Merge remote-tracking branch 'origin/main' into L71-19553
juliusmarminge Oct 20, 2026
2171c5a
Merge remote-tracking branch 'origin/main' into L71-19553
juliusmarminge Oct 20, 2026
6ac4c35
test(web): reproduce issue 19530 Changes row ignoring panel base and …
juliusmarminge Oct 24, 2026
367e9df
Merge PR #19553 head and complete issue 19530
juliusmarminge Oct 24, 2026
89b3933
fix(web): Changes row refreshes after edits during its first load
juliusmarminge Oct 24, 2026
750dbaf
fix(web): Changes row refreshes a comparison the panel cached
juliusmarminge Oct 24, 2026
d78da18
fix(web): Changes row refreshes after a panel load that predates an edit
juliusmarminge Oct 24, 2026
e8b7ebc
Merge origin/main into fix/changes-row-base-ref
juliusmarminge Nov 11, 2026
198fdc3
fix(web): Changes row refetches after edits made outside a turn
juliusmarminge Nov 11, 2026
c8e26b8
fix(web): Changes row refetches when branch totals change outside a turn
juliusmarminge Nov 11, 2026
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
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3852,6 +3852,7 @@ export default function ChatView(props: ChatViewProps) {
)
}
gitCwd={gitCwd}
workspaceMutationId={workspaceMutationId}
isGitRepo={isGitRepo}
envLocked={envLocked}
availableEnvironments={logicalProjectEnvironments}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ export default function DiffPanel({
}));
const diffLayout = settings.diffLayout;
const [wordWrap, setWordWrap] = useState(settings.wordWrap);
const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
const diffIgnoreWhitespace =
useDiffPanelSessionStore((state) => state.ignoreWhitespace) ?? settings.diffIgnoreWhitespace;
const [fileTreeOpen, setFileTreeOpen] = useLocalStorage(
DIFF_FILE_TREE_STORAGE_KEY,
false,
Expand Down Expand Up @@ -1222,7 +1223,7 @@ export default function DiffPanel({
size="sm"
pressed={diffIgnoreWhitespace}
onPressedChange={(pressed) => {
setDiffIgnoreWhitespace(Boolean(pressed));
useDiffPanelSessionStore.getState().setIgnoreWhitespace(Boolean(pressed));
}}
/>
}
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,37 @@ describe("resolveChangesTotals", () => {
);
});

it("uses the panel's branch comparison when it has file stats", () => {
const dirty = status({
branchChanges: { baseRef: "main", insertions: 20, deletions: 3 },
});
const source = {
id: "branch-range",
kind: "branch-range",
title: "Changes vs release",
baseRef: "release",
headRef: "feature/test",
diff: "",
diffHash: "hash",
truncated: false,
} as const;
assert.deepEqual(
resolveChangesTotals(dirty, {
...source,
files: [
{ path: "a.ts", previousPath: null, additions: 4, deletions: 1 },
{ path: "b.ts", previousPath: null, additions: 2, deletions: 0 },
],
}),
{ insertions: 6, deletions: 1, baseRef: "release" },
);
assert.deepEqual(resolveChangesTotals(dirty, source), {
insertions: 20,
deletions: 3,
baseRef: "main",
});
});

it("falls back to the working tree when branchChanges is omitted", () => {
assert.deepInclude(
resolveChangesTotals(
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/components/GitActionsControl.logic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
GitRunStackedActionResult,
GitStackedAction,
ReviewDiffPreviewSource,
VcsStatusResult,
} from "@t3tools/contracts";
import { isTemporaryWorktreeBranch } from "@t3tools/shared/git";
Expand Down Expand Up @@ -435,9 +436,21 @@ export type ChangesLineTotals = {

// `branchChanges ?? workingTree` treats `{0,0}` as present, so a zero branch total hid a dirty
// working tree and the Changes row read +0 while Diff still showed the work.
// The Diff panel's branch comparison wins when it has file stats, so a remembered base or
// hidden whitespace reads the same in the row as in the panel.
export function resolveChangesTotals(
status: VcsStatusResult | null | undefined,
branchPreview?: ReviewDiffPreviewSource | null,
): ChangesLineTotals | null {
if (branchPreview?.files) {
let insertions = 0;
let deletions = 0;
for (const file of branchPreview.files) {
insertions += file.additions;
deletions += file.deletions;
}
return { insertions, deletions, baseRef: branchPreview.baseRef };
}
if (!status) return null;
const branch = status.branchChanges;
const tree = { ...status.workingTree, baseRef: null };
Expand Down
59 changes: 58 additions & 1 deletion apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ import {
import { useQuietThreadProjection, useQuietThreadShell } from "~/state/entities";
import { readEnvironmentScope, useEnvironmentScope } from "~/state/session";
import { useEnvironmentQuery } from "~/state/query";
import { useFilesystemReadAccess } from "~/state/filesystem";
import { reviewEnvironment } from "~/state/review";
import { revisionedQuery } from "~/state/revisionedQuery";
import {
selectThreadBranchBaseRef,
useDiffPanelSessionStore,
useDiffPanelStore,
} from "~/diffPanelStore";
import { useClientSettings } from "~/hooks/useSettings";
import { serverEnvironment } from "~/state/server";
import { sourceControlEnvironment } from "~/state/sourceControl";
import { threadEnvironment } from "~/state/threads";
Expand Down Expand Up @@ -121,6 +130,7 @@ interface GitActionsControlProps {
showActions?: boolean;
showChanges?: boolean;
onOpenChanges?: () => void;
workspaceMutationId?: string | null;
}

interface PendingDefaultBranchAction {
Expand Down Expand Up @@ -932,6 +942,7 @@ export default function GitActionsControl({
showActions = true,
showChanges = true,
onOpenChanges,
workspaceMutationId = null,
}: GitActionsControlProps) {
const panelAnchorRef = useRef<HTMLDivElement | null>(null);
const updateThreadMetadata = useAtomCommand(
Expand Down Expand Up @@ -1049,7 +1060,53 @@ export default function GitActionsControl({
const isRepo = gitStatus?.isRepo ?? true;
const hasPrimaryRemote = gitStatus?.hasPrimaryRemote ?? false;
const gitStatusForActions = gitStatus;
const resolvedChangesTotals = resolveChangesTotals(gitStatusForActions);
const changesBaseRef = useDiffPanelStore((state) =>
selectThreadBranchBaseRef(state, activeThreadRef),
);
const ignoreWhitespaceSetting = useClientSettings((settings) => settings.diffIgnoreWhitespace);
const ignoreWhitespace =
useDiffPanelSessionStore((state) => state.ignoreWhitespace) ?? ignoreWhitespaceSetting;
const { canReadFiles } = useFilesystemReadAccess(activeEnvironmentId);
// Same input as the Diff panel's branch query, so both read one cached result. Status
// totals join the revision because edits, pulls and resets outside a turn leave the mutation
// id as is.
const changesRevision = gitStatusForActions
? JSON.stringify([
workspaceMutationId,
gitStatusForActions.refName,
gitStatusForActions.workingTree.insertions,
gitStatusForActions.workingTree.deletions,
gitStatusForActions.branchChanges?.baseRef,
gitStatusForActions.branchChanges?.insertions,
gitStatusForActions.branchChanges?.deletions,
])
: null;
const changesPreview = useEnvironmentQuery(
showChanges &&
isRepo &&
canReadFiles &&
changesRevision !== null &&
activeEnvironmentId !== null &&
gitCwd !== null
? revisionedQuery(
reviewEnvironment.diffPreview({
environmentId: activeEnvironmentId,
input: {
cwd: gitCwd,
...(changesBaseRef ? { baseRef: changesBaseRef } : {}),
ignoreWhitespace,
metadataOnly: true,
sourceKind: "branch-range",
},
}),
changesRevision,
)
: null,
);
const resolvedChangesTotals = resolveChangesTotals(
gitStatusForActions,
changesPreview.data?.sources.find((source) => source.kind === "branch-range"),
);
const changesTotals = resolvedChangesTotals ?? { insertions: 0, deletions: 0 };
const changesBaseLabel = resolvedChangesTotals
? resolvedChangesTotals.baseRef === null
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/chat/ThreadDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ interface ThreadDetailsPanelProps extends Pick<
showOpenInPicker: boolean;
openInShortcutEnabled: boolean;
gitCwd: string | null;
workspaceMutationId: string | null;
isGitRepo: boolean;
envLocked: boolean;
availableEnvironments: readonly EnvironmentOption[];
Expand Down Expand Up @@ -229,6 +230,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
showActions={visible.gitActions}
showChanges={visible.changes}
gitCwd={props.gitCwd}
workspaceMutationId={props.workspaceMutationId}
activeThreadRef={activeThreadRef}
{...(props.draftId ? { draftId: props.draftId } : {})}
{...(props.onOpenChanges ? { onOpenChanges: props.onOpenChanges } : {})}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/diffPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,13 @@ interface DiffPanelSessionStoreState {
defaults: ReadonlySet<string>,
update: (current: ReadonlySet<string>) => ReadonlySet<string>,
) => void;
// Null follows the diffIgnoreWhitespace setting until the panel toggle is used.
ignoreWhitespace: boolean | null;
setIgnoreWhitespace: (ignoreWhitespace: boolean) => void;
}

// Session-only so folds survive right-panel switches without changing reload defaults.
// Session-only so folds and the whitespace toggle survive right-panel switches without
// changing reload defaults.
export const useDiffPanelSessionStore = create<DiffPanelSessionStoreState>()((set) => ({
collapsedFileKeysByScopeKey: {},
updateCollapsedFileKeys: (scopeKey, defaults, update) =>
Expand All @@ -201,6 +205,8 @@ export const useDiffPanelSessionStore = create<DiffPanelSessionStoreState>()((se
collapsedFileKeysByScopeKey: { ...state.collapsedFileKeysByScopeKey, [scopeKey]: next },
};
}),
ignoreWhitespace: null,
setIgnoreWhitespace: (ignoreWhitespace) => set({ ignoreWhitespace }),
}));

export function selectThreadDiffPanelSelection(
Expand Down
Loading