Files
paseo/packages/app/src/git/working-diff-comparison/state.ts
T
Mohamed Boudra 49784fac15 Keep Changes and working diff comparisons in sync (#4199)
* fix(app): keep Changes comparison and diff in sync

Own the working comparison at the workspace checkout so the Changes tree and adjacent working diff share it. Immutable commit diffs remain scoped to their commit SHA.

* docs(app): align working diff ownership notes
2026-09-02 12:38:16 +02:00

86 lines
2.7 KiB
TypeScript

export type WorkingDiffComparison = "uncommitted" | "base";
export interface WorkingDiffComparisonOverride {
serverId: string;
cwd: string;
comparison: WorkingDiffComparison;
isDirtyAtSelection: boolean;
}
export interface WorkingDiffComparisonState {
overrides: Record<string, WorkingDiffComparisonOverride>;
}
export interface WorkingDiffCheckoutIdentity {
serverId: string;
workspaceId?: string | null;
cwd: string;
}
function normalizeCwd(cwd: string): string {
const trimmed = cwd.trim();
return trimmed === "/" ? trimmed : trimmed.replace(/\/+$/, "");
}
export function workingDiffComparisonKey(input: WorkingDiffCheckoutIdentity): string {
const workspaceId = input.workspaceId?.trim();
const checkout = workspaceId
? `workspace=${encodeURIComponent(workspaceId)}`
: `cwd=${encodeURIComponent(normalizeCwd(input.cwd))}`;
return `working-diff:server=${encodeURIComponent(input.serverId.trim())}:${checkout}`;
}
export function selectWorkingDiffComparisonInState(
state: WorkingDiffComparisonState,
input: WorkingDiffCheckoutIdentity & {
comparison: WorkingDiffComparison;
isDirty: boolean;
},
): WorkingDiffComparisonState {
return {
overrides: {
...state.overrides,
[workingDiffComparisonKey(input)]: {
serverId: input.serverId.trim(),
cwd: normalizeCwd(input.cwd),
comparison: input.comparison,
isDirtyAtSelection: input.isDirty,
},
},
};
}
export function resolveWorkingDiffComparisonFromState(
state: WorkingDiffComparisonState,
input: WorkingDiffCheckoutIdentity & { isDirty: boolean },
): WorkingDiffComparison {
const override = state.overrides[workingDiffComparisonKey(input)];
// Status can render before boundary expiry runs, so resolution must also mask a stale
// selection under any ordering of the two updates.
if (override?.isDirtyAtSelection === input.isDirty) {
return override.comparison;
}
return input.isDirty ? "uncommitted" : "base";
}
export function expireWorkingDiffComparisonsInState(
state: WorkingDiffComparisonState,
input: { serverId: string; cwd: string; isDirty: boolean },
): WorkingDiffComparisonState {
// Expire at the status boundary so selections cannot return after an unmounted surface
// misses a dirty-state transition.
const staleKeys = Object.entries(state.overrides)
.filter(
([, override]) =>
override.serverId === input.serverId.trim() &&
override.cwd === normalizeCwd(input.cwd) &&
override.isDirtyAtSelection !== input.isDirty,
)
.map(([key]) => key);
if (staleKeys.length === 0) return state;
const overrides = { ...state.overrides };
for (const key of staleKeys) delete overrides[key];
return { overrides };
}