diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts
index 71b6b7df4e8..a3cfec5ed33 100644
--- a/apps/desktop/src/clientPersistence.test.ts
+++ b/apps/desktop/src/clientPersistence.test.ts
@@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
+ diffIgnoreWhitespace: true,
diffWordWrap: true,
favorites: [],
providerModelPreferences: {},
diff --git a/apps/server/integration/orchestrationEngine.integration.test.ts b/apps/server/integration/orchestrationEngine.integration.test.ts
index 6ea4d849086..6bd94819839 100644
--- a/apps/server/integration/orchestrationEngine.integration.test.ts
+++ b/apps/server/integration/orchestrationEngine.integration.test.ts
@@ -506,6 +506,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () =>
fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 1),
toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2),
fallbackFromToHead: false,
+ ignoreWhitespace: false,
});
assert.equal(incrementalDiff.includes("README.md"), true);
@@ -514,6 +515,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () =>
fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 0),
toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2),
fallbackFromToHead: false,
+ ignoreWhitespace: false,
});
assert.equal(fullDiff.includes("README.md"), true);
diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts
index e6c53dd06b0..645568d734d 100644
--- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts
+++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts
@@ -48,6 +48,7 @@ describe("CheckpointDiffQueryLive", () => {
readonly fromCheckpointRef: CheckpointRef;
readonly toCheckpointRef: CheckpointRef;
readonly cwd: string;
+ readonly ignoreWhitespace: boolean;
}> = [];
const threadCheckpointContext = makeThreadCheckpointContext({
@@ -68,9 +69,14 @@ describe("CheckpointDiffQueryLive", () => {
return true;
}),
restoreCheckpoint: () => Effect.succeed(true),
- diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd }) =>
+ diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) =>
Effect.sync(() => {
- diffCheckpointsCalls.push({ fromCheckpointRef, toCheckpointRef, cwd });
+ diffCheckpointsCalls.push({
+ fromCheckpointRef,
+ toCheckpointRef,
+ cwd,
+ ignoreWhitespace,
+ });
return "diff patch";
}),
deleteCheckpointRefs: () => Effect.void,
@@ -102,6 +108,7 @@ describe("CheckpointDiffQueryLive", () => {
threadId,
fromTurnCount: 0,
toTurnCount: 1,
+ ignoreWhitespace: true,
});
}).pipe(Effect.provide(layer)),
);
@@ -113,6 +120,7 @@ describe("CheckpointDiffQueryLive", () => {
cwd: "/tmp/workspace",
fromCheckpointRef: expectedFromRef,
toCheckpointRef,
+ ignoreWhitespace: true,
},
]);
expect(result).toEqual({
@@ -123,6 +131,67 @@ describe("CheckpointDiffQueryLive", () => {
});
});
+ it("defaults to hide whitespace changes", async () => {
+ const projectId = ProjectId.make("project-default-whitespace");
+ const threadId = ThreadId.make("thread-default-whitespace");
+ const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
+ const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = [];
+
+ const threadCheckpointContext = makeThreadCheckpointContext({
+ projectId,
+ threadId,
+ workspaceRoot: "/tmp/workspace",
+ worktreePath: null,
+ checkpointTurnCount: 1,
+ checkpointRef: toCheckpointRef,
+ });
+
+ const checkpointStore: CheckpointStoreShape = {
+ isGitRepository: () => Effect.succeed(true),
+ captureCheckpoint: () => Effect.void,
+ hasCheckpointRef: () => Effect.succeed(true),
+ restoreCheckpoint: () => Effect.succeed(true),
+ diffCheckpoints: ({ ignoreWhitespace }) =>
+ Effect.sync(() => {
+ diffCheckpointsCalls.push({ ignoreWhitespace });
+ return "diff patch";
+ }),
+ deleteCheckpointRefs: () => Effect.void,
+ };
+
+ const layer = CheckpointDiffQueryLive.pipe(
+ Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)),
+ Layer.provideMerge(
+ Layer.succeed(ProjectionSnapshotQuery, {
+ getSnapshot: () =>
+ Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
+ getShellSnapshot: () =>
+ Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
+ getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
+ getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
+ getProjectShellById: () => Effect.succeed(Option.none()),
+ getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
+ getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
+ getThreadShellById: () => Effect.succeed(Option.none()),
+ getThreadDetailById: () => Effect.succeed(Option.none()),
+ }),
+ ),
+ );
+
+ await Effect.runPromise(
+ Effect.gen(function* () {
+ const query = yield* CheckpointDiffQuery;
+ return yield* query.getTurnDiff({
+ threadId,
+ fromTurnCount: 0,
+ toTurnCount: 1,
+ });
+ }).pipe(Effect.provide(layer)),
+ );
+
+ expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]);
+ });
+
it("fails when the thread is missing from the snapshot", async () => {
const threadId = ThreadId.make("thread-missing");
diff --git a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts
index 1c2edee469e..c6b15134711 100644
--- a/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts
+++ b/apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts
@@ -24,6 +24,7 @@ const make = Effect.gen(function* () {
const getTurnDiff: CheckpointDiffQueryShape["getTurnDiff"] = Effect.fn("getTurnDiff")(
function* (input) {
const operation = "CheckpointDiffQuery.getTurnDiff";
+ const ignoreWhitespace = input.ignoreWhitespace ?? true;
if (input.fromTurnCount === input.toTurnCount) {
const emptyDiff: OrchestrationGetTurnDiffResultType = {
@@ -131,6 +132,7 @@ const make = Effect.gen(function* () {
fromCheckpointRef,
toCheckpointRef,
fallbackFromToHead: false,
+ ignoreWhitespace,
});
const turnDiff: OrchestrationGetTurnDiffResultType = {
@@ -157,6 +159,7 @@ const make = Effect.gen(function* () {
threadId: input.threadId,
fromTurnCount: 0,
toTurnCount: input.toTurnCount,
+ ignoreWhitespace: input.ignoreWhitespace ?? true,
}).pipe(Effect.map((result): OrchestrationGetFullThreadDiffResult => result));
return {
diff --git a/apps/server/src/checkpointing/Layers/CheckpointStore.test.ts b/apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
index 36739594842..204fd565740 100644
--- a/apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
+++ b/apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
@@ -114,6 +114,7 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => {
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
+ ignoreWhitespace: true,
});
expect(diff).toContain("diff --git");
@@ -121,5 +122,80 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => {
expect(diff).toContain("+line 04999");
}),
);
+
+ it.effect("can hide indentation churn when changes wrap existing lines", () =>
+ Effect.gen(function* () {
+ const tmp = yield* makeTmpDir();
+ yield* initRepoWithCommit(tmp);
+ const checkpointStore = yield* CheckpointStore;
+ const threadId = ThreadId.make("thread-checkpoint-store-whitespace");
+ const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
+ const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
+
+ const componentPath = path.join(tmp, "Component.tsx");
+ yield* writeTextFile(
+ componentPath,
+ [
+ "export function View() {",
+ " return (",
+ " ",
+ " Title
",
+ " Body
",
+ " ",
+ " );",
+ "}",
+ "",
+ ].join("\n"),
+ );
+ yield* checkpointStore.captureCheckpoint({
+ cwd: tmp,
+ checkpointRef: fromCheckpointRef,
+ });
+ yield* writeTextFile(
+ componentPath,
+ [
+ "export function View() {",
+ " return (",
+ " ",
+ " {isReady ? (",
+ " ",
+ "
Title
",
+ "
Body
",
+ "
",
+ " ) : null}",
+ " ",
+ " );",
+ "}",
+ "",
+ ].join("\n"),
+ );
+ yield* checkpointStore.captureCheckpoint({
+ cwd: tmp,
+ checkpointRef: toCheckpointRef,
+ });
+
+ const normalDiff = yield* checkpointStore.diffCheckpoints({
+ cwd: tmp,
+ fromCheckpointRef,
+ toCheckpointRef,
+ ignoreWhitespace: false,
+ });
+ const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({
+ cwd: tmp,
+ fromCheckpointRef,
+ toCheckpointRef,
+ ignoreWhitespace: true,
+ });
+
+ expect(normalDiff).toContain("diff --git");
+ expect(normalDiff).toContain("-
Title
");
+ expect(normalDiff).toContain("+ Title
");
+ expect(whitespaceIgnoredDiff).toContain("diff --git");
+ expect(whitespaceIgnoredDiff).toContain("+ {isReady ? (");
+ expect(whitespaceIgnoredDiff).toContain("+ ");
+ expect(whitespaceIgnoredDiff).not.toContain("-
Title
");
+ expect(whitespaceIgnoredDiff).not.toContain("+
Title
");
+ }),
+ );
});
});
diff --git a/apps/server/src/checkpointing/Layers/CheckpointStore.ts b/apps/server/src/checkpointing/Layers/CheckpointStore.ts
index c8b59564604..7efe4cc0a56 100644
--- a/apps/server/src/checkpointing/Layers/CheckpointStore.ts
+++ b/apps/server/src/checkpointing/Layers/CheckpointStore.ts
@@ -259,10 +259,20 @@ const makeCheckpointStore = Effect.gen(function* () {
});
}
+ const diffArgs = [
+ "diff",
+ "--patch",
+ "--minimal",
+ "--no-color",
+ ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []),
+ fromCommitOid,
+ toCommitOid,
+ ];
+
const result = yield* vcs.execute({
operation,
cwd: input.cwd,
- args: ["diff", "--patch", "--minimal", "--no-color", fromCommitOid, toCommitOid],
+ args: diffArgs,
maxOutputBytes: CHECKPOINT_DIFF_MAX_OUTPUT_BYTES,
});
diff --git a/apps/server/src/checkpointing/Services/CheckpointStore.ts b/apps/server/src/checkpointing/Services/CheckpointStore.ts
index d9a43fa4e95..be6bdbccfaa 100644
--- a/apps/server/src/checkpointing/Services/CheckpointStore.ts
+++ b/apps/server/src/checkpointing/Services/CheckpointStore.ts
@@ -32,6 +32,7 @@ export interface DiffCheckpointsInput {
readonly fromCheckpointRef: CheckpointRef;
readonly toCheckpointRef: CheckpointRef;
readonly fallbackFromToHead?: boolean;
+ readonly ignoreWhitespace: boolean;
}
export interface DeleteCheckpointRefsInput {
diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts
index 15677feaaf7..e534f6851ae 100644
--- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts
+++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts
@@ -237,6 +237,7 @@ const make = Effect.gen(function* () {
fromCheckpointRef,
toCheckpointRef: targetCheckpointRef,
fallbackFromToHead: false,
+ ignoreWhitespace: false,
})
.pipe(
Effect.map((diff) =>
diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx
index e6dbb57cc7d..28db53e917e 100644
--- a/apps/web/src/components/DiffPanel.tsx
+++ b/apps/web/src/components/DiffPanel.tsx
@@ -8,6 +8,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
Columns2Icon,
+ PilcrowIcon,
Rows3Icon,
TextWrapIcon,
} from "lucide-react";
@@ -172,6 +173,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
const settings = useSettings();
const [diffRenderMode, setDiffRenderMode] = useState
("stacked");
const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap);
+ const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace);
const patchViewportRef = useRef(null);
const turnStripRef = useRef(null);
const previousDiffOpenRef = useRef(false);
@@ -277,6 +279,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
threadId: activeThreadId,
fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null,
toTurnCount: activeCheckpointRange?.toTurnCount ?? null,
+ ignoreWhitespace: diffIgnoreWhitespace,
cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope,
enabled: isGitRepo,
}),
@@ -317,9 +320,10 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
useEffect(() => {
if (diffOpen && !previousDiffOpenRef.current) {
setDiffWordWrap(settings.diffWordWrap);
+ setDiffIgnoreWhitespace(settings.diffIgnoreWhitespace);
}
previousDiffOpenRef.current = diffOpen;
- }, [diffOpen, settings.diffWordWrap]);
+ }, [diffOpen, settings.diffIgnoreWhitespace, settings.diffWordWrap]);
useEffect(() => {
if (!selectedFilePath || !patchViewportRef.current) {
@@ -551,6 +555,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
>
+ {
+ setDiffIgnoreWhitespace(Boolean(pressed));
+ }}
+ >
+
+
>
);
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index af904f562d9..8fc36d4a32b 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -386,6 +386,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
? ["Diff line wrapping"]
: []),
+ ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
+ ? ["Diff whitespace changes"]
+ : []),
...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar
? ["Auto-open task panel"]
: []),
@@ -415,6 +418,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadDelete,
settings.addProjectBaseDirectory,
settings.defaultThreadEnvMode,
+ settings.diffIgnoreWhitespace,
settings.diffWordWrap,
settings.enableAssistantStreaming,
settings.timestampFormat,
@@ -889,6 +893,32 @@ export function GeneralSettingsPanel() {
}
/>
+
+ updateSettings({
+ diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ updateSettings({ diffIgnoreWhitespace: Boolean(checked) })
+ }
+ aria-label="Hide whitespace changes by default"
+ />
+ }
+ />
+
{
threadId,
fromTurnCount: 1,
toTurnCount: 2,
+ ignoreWhitespace: false,
} as const;
expect(
@@ -44,10 +45,58 @@ describe("providerQueryKeys.checkpointDiff", () => {
}),
);
});
+
+ it("includes ignoreWhitespace so normal and whitespace-hidden diffs do not collide", () => {
+ const baseInput = {
+ environmentId,
+ threadId,
+ fromTurnCount: 1,
+ toTurnCount: 2,
+ cacheScope: "turn:abc",
+ } as const;
+
+ expect(
+ providerQueryKeys.checkpointDiff({
+ ...baseInput,
+ ignoreWhitespace: false,
+ }),
+ ).not.toEqual(
+ providerQueryKeys.checkpointDiff({
+ ...baseInput,
+ ignoreWhitespace: true,
+ }),
+ );
+ });
});
describe("checkpointDiffQueryOptions", () => {
- it("forwards checkpoint range to the provider API", async () => {
+ it("forwards checkpoint range to the provider API by default", async () => {
+ const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" });
+ const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" });
+ mockNativeApi({ getTurnDiff, getFullThreadDiff });
+
+ const options = checkpointDiffQueryOptions({
+ environmentId,
+ threadId,
+ fromTurnCount: 3,
+ toTurnCount: 4,
+ ignoreWhitespace: false,
+ cacheScope: "turn:abc",
+ });
+
+ const queryClient = new QueryClient();
+ await queryClient.fetchQuery(options);
+
+ expect(getTurnDiff).toHaveBeenCalledWith({
+ threadId,
+ fromTurnCount: 3,
+ toTurnCount: 4,
+ ignoreWhitespace: false,
+ });
+ expect(getFullThreadDiff).not.toHaveBeenCalled();
+ });
+
+ it("forwards whitespace-hidden checkpoint range to the provider API", async () => {
const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" });
const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" });
mockNativeApi({ getTurnDiff, getFullThreadDiff });
@@ -57,6 +106,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 3,
toTurnCount: 4,
+ ignoreWhitespace: true,
cacheScope: "turn:abc",
});
@@ -67,6 +117,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 3,
toTurnCount: 4,
+ ignoreWhitespace: true,
});
expect(getFullThreadDiff).not.toHaveBeenCalled();
});
@@ -81,6 +132,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 0,
toTurnCount: 2,
+ ignoreWhitespace: true,
cacheScope: "thread:all",
});
@@ -90,6 +142,7 @@ describe("checkpointDiffQueryOptions", () => {
expect(getFullThreadDiff).toHaveBeenCalledWith({
threadId,
toTurnCount: 2,
+ ignoreWhitespace: true,
});
expect(getTurnDiff).not.toHaveBeenCalled();
});
@@ -104,6 +157,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 4,
toTurnCount: 3,
+ ignoreWhitespace: false,
cacheScope: "turn:invalid",
});
@@ -122,6 +176,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 1,
toTurnCount: 2,
+ ignoreWhitespace: false,
cacheScope: "turn:abc",
});
const retry = options.retry;
@@ -147,6 +202,7 @@ describe("checkpointDiffQueryOptions", () => {
threadId,
fromTurnCount: 1,
toTurnCount: 2,
+ ignoreWhitespace: false,
cacheScope: "turn:abc",
});
const retryDelay = options.retryDelay;
diff --git a/apps/web/src/lib/providerReactQuery.ts b/apps/web/src/lib/providerReactQuery.ts
index 20007fc8fb6..663d618b92e 100644
--- a/apps/web/src/lib/providerReactQuery.ts
+++ b/apps/web/src/lib/providerReactQuery.ts
@@ -13,6 +13,7 @@ interface CheckpointDiffQueryInput {
threadId: ThreadId | null;
fromTurnCount: number | null;
toTurnCount: number | null;
+ ignoreWhitespace: boolean;
cacheScope?: string | null;
enabled?: boolean;
}
@@ -27,6 +28,7 @@ export const providerQueryKeys = {
input.threadId,
input.fromTurnCount,
input.toTurnCount,
+ input.ignoreWhitespace,
input.cacheScope ?? null,
] as const,
};
@@ -36,6 +38,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) {
return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({
threadId: input.threadId,
toTurnCount: input.toTurnCount,
+ ignoreWhitespace: input.ignoreWhitespace,
}).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields })));
}
@@ -43,6 +46,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) {
threadId: input.threadId,
fromTurnCount: input.fromTurnCount,
toTurnCount: input.toTurnCount,
+ ignoreWhitespace: input.ignoreWhitespace,
}).pipe(Option.map((fields) => ({ kind: "turnDiff" as const, input: fields })));
}
diff --git a/apps/web/src/localApi.test.ts b/apps/web/src/localApi.test.ts
index b627286199c..47a791806a0 100644
--- a/apps/web/src/localApi.test.ts
+++ b/apps/web/src/localApi.test.ts
@@ -537,6 +537,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
+ diffIgnoreWhitespace: true,
diffWordWrap: true,
favorites: [],
providerModelPreferences: {},
@@ -597,6 +598,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
+ diffIgnoreWhitespace: true,
diffWordWrap: true,
favorites: [],
providerModelPreferences: {},
diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts
index 559f3016e0f..605d0375342 100644
--- a/packages/contracts/src/orchestration.test.ts
+++ b/packages/contracts/src/orchestration.test.ts
@@ -8,6 +8,7 @@ import {
ModelSelection,
OrchestrationCommand,
OrchestrationEvent,
+ OrchestrationGetFullThreadDiffInput,
OrchestrationGetTurnDiffInput,
OrchestrationLatestTurn,
ProjectCreatedPayload,
@@ -24,6 +25,7 @@ import {
import { ProviderInstanceId } from "./providerInstance.ts";
const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput);
+const decodeFullThreadDiffInput = Schema.decodeUnknownEffect(OrchestrationGetFullThreadDiffInput);
const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff);
const decodeProjectCreateCommand = Schema.decodeUnknownEffect(ProjectCreateCommand);
const decodeProjectCreatedPayload = Schema.decodeUnknownEffect(ProjectCreatedPayload);
@@ -59,6 +61,29 @@ it.effect("parses turn diff input when fromTurnCount <= toTurnCount", () =>
}),
);
+it.effect("parses turn diff input with whitespace ignoring enabled", () =>
+ Effect.gen(function* () {
+ const parsed = yield* decodeTurnDiffInput({
+ threadId: "thread-1",
+ fromTurnCount: 1,
+ toTurnCount: 2,
+ ignoreWhitespace: true,
+ });
+ assert.strictEqual(parsed.ignoreWhitespace, true);
+ }),
+);
+
+it.effect("parses full thread diff input with whitespace ignoring enabled", () =>
+ Effect.gen(function* () {
+ const parsed = yield* decodeFullThreadDiffInput({
+ threadId: "thread-1",
+ toTurnCount: 2,
+ ignoreWhitespace: true,
+ });
+ assert.strictEqual(parsed.ignoreWhitespace, true);
+ }),
+);
+
it.effect("rejects turn diff input when fromTurnCount > toTurnCount", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
index 19160c0b604..047ef9e5cd9 100644
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -1171,7 +1171,10 @@ export const DispatchResult = Schema.Struct({
export type DispatchResult = typeof DispatchResult.Type;
export const OrchestrationGetTurnDiffInput = TurnCountRange.mapFields(
- Struct.assign({ threadId: ThreadId }),
+ Struct.assign({
+ threadId: ThreadId,
+ ignoreWhitespace: Schema.optionalKey(Schema.Boolean),
+ }),
{ unsafePreserveChecks: true },
);
export type OrchestrationGetTurnDiffInput = typeof OrchestrationGetTurnDiffInput.Type;
@@ -1182,6 +1185,7 @@ export type OrchestrationGetTurnDiffResult = typeof OrchestrationGetTurnDiffResu
export const OrchestrationGetFullThreadDiffInput = Schema.Struct({
threadId: ThreadId,
toTurnCount: NonNegativeInt,
+ ignoreWhitespace: Schema.optionalKey(Schema.Boolean),
});
export type OrchestrationGetFullThreadDiffInput = typeof OrchestrationGetFullThreadDiffInput.Type;
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 087b031402c..90b9099d177 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -32,6 +32,7 @@ export const ClientSettingsSchema = Schema.Struct({
autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
+ diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
// Model favorites. Historically keyed by provider kind, now
// widened to `ProviderInstanceId` so users can favorite a specific model
@@ -451,6 +452,7 @@ export const ClientSettingsPatch = Schema.Struct({
autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean),
confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
+ diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
diffWordWrap: Schema.optionalKey(Schema.Boolean),
favorites: Schema.optionalKey(
Schema.Array(