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(