| author | |
| committer | |
| log | f7748a0d656f918080828130aceda039457897e0 |
| tree | 7d3d8a25cc6e724863d4e1bc3c75de65b692e182 |
| parent | 0ce7e56e436ba31e2f0b51f3203a09758c866cd1 |
| signature | Signed by PGP key B5690EEEBB952194 |
16 files changed, 308 insertions(+), 6 deletions(-)
apps/desktop/src/clientPersistence.test.ts+1| ... | @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = { | ... | @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = { |
| 52 | autoOpenPlanSidebar: false, | 52 | autoOpenPlanSidebar: false, |
| 53 | confirmThreadArchive: true, | 53 | confirmThreadArchive: true, |
| 54 | confirmThreadDelete: false, | 54 | confirmThreadDelete: false, |
| 55 | diffIgnoreWhitespace: true, | ||
| 55 | diffWordWrap: true, | 56 | diffWordWrap: true, |
| 56 | favorites: [], | 57 | favorites: [], |
| 57 | providerModelPreferences: {}, | 58 | providerModelPreferences: {}, |
apps/server/integration/orchestrationEngine.integration.test.ts+2| ... | @@ -506,6 +506,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -506,6 +506,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 506 | fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 1), | 506 | fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 1), |
| 507 | toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), | 507 | toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), |
| 508 | fallbackFromToHead: false, | 508 | fallbackFromToHead: false, |
| 509 | ignoreWhitespace: false, | ||
| 509 | }); | 510 | }); |
| 510 | assert.equal(incrementalDiff.includes("README.md"), true); | 511 | assert.equal(incrementalDiff.includes("README.md"), true); |
| 511 | 512 | ||
| ... | @@ -514,6 +515,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => | ... | @@ -514,6 +515,7 @@ it.live("runs multi-turn file edits and persists checkpoint diffs", () => |
| 514 | fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 0), | 515 | fromCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 0), |
| 515 | toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), | 516 | toCheckpointRef: checkpointRefForThreadTurn(THREAD_ID, 2), |
| 516 | fallbackFromToHead: false, | 517 | fallbackFromToHead: false, |
| 518 | ignoreWhitespace: false, | ||
| 517 | }); | 519 | }); |
| 518 | assert.equal(fullDiff.includes("README.md"), true); | 520 | assert.equal(fullDiff.includes("README.md"), true); |
| 519 | 521 |
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts+71-2| ... | @@ -48,6 +48,7 @@ describe("CheckpointDiffQueryLive", () => { | ... | @@ -48,6 +48,7 @@ describe("CheckpointDiffQueryLive", () => { |
| 48 | readonly fromCheckpointRef: CheckpointRef; | 48 | readonly fromCheckpointRef: CheckpointRef; |
| 49 | readonly toCheckpointRef: CheckpointRef; | 49 | readonly toCheckpointRef: CheckpointRef; |
| 50 | readonly cwd: string; | 50 | readonly cwd: string; |
| 51 | readonly ignoreWhitespace: boolean; | ||
| 51 | }> = []; | 52 | }> = []; |
| 52 | 53 | ||
| 53 | const threadCheckpointContext = makeThreadCheckpointContext({ | 54 | const threadCheckpointContext = makeThreadCheckpointContext({ |
| ... | @@ -68,9 +69,14 @@ describe("CheckpointDiffQueryLive", () => { | ... | @@ -68,9 +69,14 @@ describe("CheckpointDiffQueryLive", () => { |
| 68 | return true; | 69 | return true; |
| 69 | }), | 70 | }), |
| 70 | restoreCheckpoint: () => Effect.succeed(true), | 71 | restoreCheckpoint: () => Effect.succeed(true), |
| 71 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd }) => | 72 | diffCheckpoints: ({ fromCheckpointRef, toCheckpointRef, cwd, ignoreWhitespace }) => |
| 72 | Effect.sync(() => { | 73 | Effect.sync(() => { |
| 73 | diffCheckpointsCalls.push({ fromCheckpointRef, toCheckpointRef, cwd }); | 74 | diffCheckpointsCalls.push({ |
| 75 | fromCheckpointRef, | ||
| 76 | toCheckpointRef, | ||
| 77 | cwd, | ||
| 78 | ignoreWhitespace, | ||
| 79 | }); | ||
| 74 | return "diff patch"; | 80 | return "diff patch"; |
| 75 | }), | 81 | }), |
| 76 | deleteCheckpointRefs: () => Effect.void, | 82 | deleteCheckpointRefs: () => Effect.void, |
| ... | @@ -102,6 +108,7 @@ describe("CheckpointDiffQueryLive", () => { | ... | @@ -102,6 +108,7 @@ describe("CheckpointDiffQueryLive", () => { |
| 102 | threadId, | 108 | threadId, |
| 103 | fromTurnCount: 0, | 109 | fromTurnCount: 0, |
| 104 | toTurnCount: 1, | 110 | toTurnCount: 1, |
| 111 | ignoreWhitespace: true, | ||
| 105 | }); | 112 | }); |
| 106 | }).pipe(Effect.provide(layer)), | 113 | }).pipe(Effect.provide(layer)), |
| 107 | ); | 114 | ); |
| ... | @@ -113,6 +120,7 @@ describe("CheckpointDiffQueryLive", () => { | ... | @@ -113,6 +120,7 @@ describe("CheckpointDiffQueryLive", () => { |
| 113 | cwd: "/tmp/workspace", | 120 | cwd: "/tmp/workspace", |
| 114 | fromCheckpointRef: expectedFromRef, | 121 | fromCheckpointRef: expectedFromRef, |
| 115 | toCheckpointRef, | 122 | toCheckpointRef, |
| 123 | ignoreWhitespace: true, | ||
| 116 | }, | 124 | }, |
| 117 | ]); | 125 | ]); |
| 118 | expect(result).toEqual({ | 126 | expect(result).toEqual({ |
| ... | @@ -123,6 +131,67 @@ describe("CheckpointDiffQueryLive", () => { | ... | @@ -123,6 +131,67 @@ describe("CheckpointDiffQueryLive", () => { |
| 123 | }); | 131 | }); |
| 124 | }); | 132 | }); |
| 125 | 133 | ||
| 134 | it("defaults to hide whitespace changes", async () => { | ||
| 135 | const projectId = ProjectId.make("project-default-whitespace"); | ||
| 136 | const threadId = ThreadId.make("thread-default-whitespace"); | ||
| 137 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 138 | const diffCheckpointsCalls: Array<{ readonly ignoreWhitespace: boolean }> = []; | ||
| 139 | |||
| 140 | const threadCheckpointContext = makeThreadCheckpointContext({ | ||
| 141 | projectId, | ||
| 142 | threadId, | ||
| 143 | workspaceRoot: "/tmp/workspace", | ||
| 144 | worktreePath: null, | ||
| 145 | checkpointTurnCount: 1, | ||
| 146 | checkpointRef: toCheckpointRef, | ||
| 147 | }); | ||
| 148 | |||
| 149 | const checkpointStore: CheckpointStoreShape = { | ||
| 150 | isGitRepository: () => Effect.succeed(true), | ||
| 151 | captureCheckpoint: () => Effect.void, | ||
| 152 | hasCheckpointRef: () => Effect.succeed(true), | ||
| 153 | restoreCheckpoint: () => Effect.succeed(true), | ||
| 154 | diffCheckpoints: ({ ignoreWhitespace }) => | ||
| 155 | Effect.sync(() => { | ||
| 156 | diffCheckpointsCalls.push({ ignoreWhitespace }); | ||
| 157 | return "diff patch"; | ||
| 158 | }), | ||
| 159 | deleteCheckpointRefs: () => Effect.void, | ||
| 160 | }; | ||
| 161 | |||
| 162 | const layer = CheckpointDiffQueryLive.pipe( | ||
| 163 | Layer.provideMerge(Layer.succeed(CheckpointStore, checkpointStore)), | ||
| 164 | Layer.provideMerge( | ||
| 165 | Layer.succeed(ProjectionSnapshotQuery, { | ||
| 166 | getSnapshot: () => | ||
| 167 | Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"), | ||
| 168 | getShellSnapshot: () => | ||
| 169 | Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"), | ||
| 170 | getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), | ||
| 171 | getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), | ||
| 172 | getProjectShellById: () => Effect.succeed(Option.none()), | ||
| 173 | getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), | ||
| 174 | getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)), | ||
| 175 | getThreadShellById: () => Effect.succeed(Option.none()), | ||
| 176 | getThreadDetailById: () => Effect.succeed(Option.none()), | ||
| 177 | }), | ||
| 178 | ), | ||
| 179 | ); | ||
| 180 | |||
| 181 | await Effect.runPromise( | ||
| 182 | Effect.gen(function* () { | ||
| 183 | const query = yield* CheckpointDiffQuery; | ||
| 184 | return yield* query.getTurnDiff({ | ||
| 185 | threadId, | ||
| 186 | fromTurnCount: 0, | ||
| 187 | toTurnCount: 1, | ||
| 188 | }); | ||
| 189 | }).pipe(Effect.provide(layer)), | ||
| 190 | ); | ||
| 191 | |||
| 192 | expect(diffCheckpointsCalls).toEqual([{ ignoreWhitespace: true }]); | ||
| 193 | }); | ||
| 194 | |||
| 126 | it("fails when the thread is missing from the snapshot", async () => { | 195 | it("fails when the thread is missing from the snapshot", async () => { |
| 127 | const threadId = ThreadId.make("thread-missing"); | 196 | const threadId = ThreadId.make("thread-missing"); |
| 128 | 197 |
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts+3| ... | @@ -24,6 +24,7 @@ const make = Effect.gen(function* () { | ... | @@ -24,6 +24,7 @@ const make = Effect.gen(function* () { |
| 24 | const getTurnDiff: CheckpointDiffQueryShape["getTurnDiff"] = Effect.fn("getTurnDiff")( | 24 | const getTurnDiff: CheckpointDiffQueryShape["getTurnDiff"] = Effect.fn("getTurnDiff")( |
| 25 | function* (input) { | 25 | function* (input) { |
| 26 | const operation = "CheckpointDiffQuery.getTurnDiff"; | 26 | const operation = "CheckpointDiffQuery.getTurnDiff"; |
| 27 | const ignoreWhitespace = input.ignoreWhitespace ?? true; | ||
| 27 | 28 | ||
| 28 | if (input.fromTurnCount === input.toTurnCount) { | 29 | if (input.fromTurnCount === input.toTurnCount) { |
| 29 | const emptyDiff: OrchestrationGetTurnDiffResultType = { | 30 | const emptyDiff: OrchestrationGetTurnDiffResultType = { |
| ... | @@ -131,6 +132,7 @@ const make = Effect.gen(function* () { | ... | @@ -131,6 +132,7 @@ const make = Effect.gen(function* () { |
| 131 | fromCheckpointRef, | 132 | fromCheckpointRef, |
| 132 | toCheckpointRef, | 133 | toCheckpointRef, |
| 133 | fallbackFromToHead: false, | 134 | fallbackFromToHead: false, |
| 135 | ignoreWhitespace, | ||
| 134 | }); | 136 | }); |
| 135 | 137 | ||
| 136 | const turnDiff: OrchestrationGetTurnDiffResultType = { | 138 | const turnDiff: OrchestrationGetTurnDiffResultType = { |
| ... | @@ -157,6 +159,7 @@ const make = Effect.gen(function* () { | ... | @@ -157,6 +159,7 @@ const make = Effect.gen(function* () { |
| 157 | threadId: input.threadId, | 159 | threadId: input.threadId, |
| 158 | fromTurnCount: 0, | 160 | fromTurnCount: 0, |
| 159 | toTurnCount: input.toTurnCount, | 161 | toTurnCount: input.toTurnCount, |
| 162 | ignoreWhitespace: input.ignoreWhitespace ?? true, | ||
| 160 | }).pipe(Effect.map((result): OrchestrationGetFullThreadDiffResult => result)); | 163 | }).pipe(Effect.map((result): OrchestrationGetFullThreadDiffResult => result)); |
| 161 | 164 | ||
| 162 | return { | 165 | return { |
apps/server/src/checkpointing/Layers/CheckpointStore.test.ts+76| ... | @@ -114,6 +114,7 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => { | ... | @@ -114,6 +114,7 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => { |
| 114 | cwd: tmp, | 114 | cwd: tmp, |
| 115 | fromCheckpointRef, | 115 | fromCheckpointRef, |
| 116 | toCheckpointRef, | 116 | toCheckpointRef, |
| 117 | ignoreWhitespace: true, | ||
| 117 | }); | 118 | }); |
| 118 | 119 | ||
| 119 | expect(diff).toContain("diff --git"); | 120 | expect(diff).toContain("diff --git"); |
| ... | @@ -121,5 +122,80 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => { | ... | @@ -121,5 +122,80 @@ it.layer(TestLayer)("CheckpointStoreLive", (it) => { |
| 121 | expect(diff).toContain("+line 04999"); | 122 | expect(diff).toContain("+line 04999"); |
| 122 | }), | 123 | }), |
| 123 | ); | 124 | ); |
| 125 | |||
| 126 | it.effect("can hide indentation churn when changes wrap existing lines", () => | ||
| 127 | Effect.gen(function* () { | ||
| 128 | const tmp = yield* makeTmpDir(); | ||
| 129 | yield* initRepoWithCommit(tmp); | ||
| 130 | const checkpointStore = yield* CheckpointStore; | ||
| 131 | const threadId = ThreadId.make("thread-checkpoint-store-whitespace"); | ||
| 132 | const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); | ||
| 133 | const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); | ||
| 134 | |||
| 135 | const componentPath = path.join(tmp, "Component.tsx"); | ||
| 136 | yield* writeTextFile( | ||
| 137 | componentPath, | ||
| 138 | [ | ||
| 139 | "export function View() {", | ||
| 140 | " return (", | ||
| 141 | " <section>", | ||
| 142 | " <h1>Title</h1>", | ||
| 143 | " <p>Body</p>", | ||
| 144 | " </section>", | ||
| 145 | " );", | ||
| 146 | "}", | ||
| 147 | "", | ||
| 148 | ].join("\n"), | ||
| 149 | ); | ||
| 150 | yield* checkpointStore.captureCheckpoint({ | ||
| 151 | cwd: tmp, | ||
| 152 | checkpointRef: fromCheckpointRef, | ||
| 153 | }); | ||
| 154 | yield* writeTextFile( | ||
| 155 | componentPath, | ||
| 156 | [ | ||
| 157 | "export function View() {", | ||
| 158 | " return (", | ||
| 159 | " <section>", | ||
| 160 | " {isReady ? (", | ||
| 161 | " <div>", | ||
| 162 | " <h1>Title</h1>", | ||
| 163 | " <p>Body</p>", | ||
| 164 | " </div>", | ||
| 165 | " ) : null}", | ||
| 166 | " </section>", | ||
| 167 | " );", | ||
| 168 | "}", | ||
| 169 | "", | ||
| 170 | ].join("\n"), | ||
| 171 | ); | ||
| 172 | yield* checkpointStore.captureCheckpoint({ | ||
| 173 | cwd: tmp, | ||
| 174 | checkpointRef: toCheckpointRef, | ||
| 175 | }); | ||
| 176 | |||
| 177 | const normalDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 178 | cwd: tmp, | ||
| 179 | fromCheckpointRef, | ||
| 180 | toCheckpointRef, | ||
| 181 | ignoreWhitespace: false, | ||
| 182 | }); | ||
| 183 | const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({ | ||
| 184 | cwd: tmp, | ||
| 185 | fromCheckpointRef, | ||
| 186 | toCheckpointRef, | ||
| 187 | ignoreWhitespace: true, | ||
| 188 | }); | ||
| 189 | |||
| 190 | expect(normalDiff).toContain("diff --git"); | ||
| 191 | expect(normalDiff).toContain("- <h1>Title</h1>"); | ||
| 192 | expect(normalDiff).toContain("+ <h1>Title</h1>"); | ||
| 193 | expect(whitespaceIgnoredDiff).toContain("diff --git"); | ||
| 194 | expect(whitespaceIgnoredDiff).toContain("+ {isReady ? ("); | ||
| 195 | expect(whitespaceIgnoredDiff).toContain("+ <div>"); | ||
| 196 | expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>"); | ||
| 197 | expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>"); | ||
| 198 | }), | ||
| 199 | ); | ||
| 124 | }); | 200 | }); |
| 125 | }); | 201 | }); |
apps/server/src/checkpointing/Layers/CheckpointStore.ts+11-1| ... | @@ -259,10 +259,20 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -259,10 +259,20 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 259 | }); | 259 | }); |
| 260 | } | 260 | } |
| 261 | 261 | ||
| 262 | const diffArgs = [ | ||
| 263 | "diff", | ||
| 264 | "--patch", | ||
| 265 | "--minimal", | ||
| 266 | "--no-color", | ||
| 267 | ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []), | ||
| 268 | fromCommitOid, | ||
| 269 | toCommitOid, | ||
| 270 | ]; | ||
| 271 | |||
| 262 | const result = yield* vcs.execute({ | 272 | const result = yield* vcs.execute({ |
| 263 | operation, | 273 | operation, |
| 264 | cwd: input.cwd, | 274 | cwd: input.cwd, |
| 265 | args: ["diff", "--patch", "--minimal", "--no-color", fromCommitOid, toCommitOid], | 275 | args: diffArgs, |
| 266 | maxOutputBytes: CHECKPOINT_DIFF_MAX_OUTPUT_BYTES, | 276 | maxOutputBytes: CHECKPOINT_DIFF_MAX_OUTPUT_BYTES, |
| 267 | }); | 277 | }); |
| 268 | 278 |
apps/server/src/checkpointing/Services/CheckpointStore.ts+1| ... | @@ -32,6 +32,7 @@ export interface DiffCheckpointsInput { | ... | @@ -32,6 +32,7 @@ export interface DiffCheckpointsInput { |
| 32 | readonly fromCheckpointRef: CheckpointRef; | 32 | readonly fromCheckpointRef: CheckpointRef; |
| 33 | readonly toCheckpointRef: CheckpointRef; | 33 | readonly toCheckpointRef: CheckpointRef; |
| 34 | readonly fallbackFromToHead?: boolean; | 34 | readonly fallbackFromToHead?: boolean; |
| 35 | readonly ignoreWhitespace: boolean; | ||
| 35 | } | 36 | } |
| 36 | 37 | ||
| 37 | export interface DeleteCheckpointRefsInput { | 38 | export interface DeleteCheckpointRefsInput { |
apps/server/src/orchestration/Layers/CheckpointReactor.ts+1| ... | @@ -237,6 +237,7 @@ const make = Effect.gen(function* () { | ... | @@ -237,6 +237,7 @@ const make = Effect.gen(function* () { |
| 237 | fromCheckpointRef, | 237 | fromCheckpointRef, |
| 238 | toCheckpointRef: targetCheckpointRef, | 238 | toCheckpointRef: targetCheckpointRef, |
| 239 | fallbackFromToHead: false, | 239 | fallbackFromToHead: false, |
| 240 | ignoreWhitespace: false, | ||
| 240 | }) | 241 | }) |
| 241 | .pipe( | 242 | .pipe( |
| 242 | Effect.map((diff) => | 243 | Effect.map((diff) => |
apps/web/src/components/DiffPanel.tsx+17-1| ... | @@ -8,6 +8,7 @@ import { | ... | @@ -8,6 +8,7 @@ import { |
| 8 | ChevronLeftIcon, | 8 | ChevronLeftIcon, |
| 9 | ChevronRightIcon, | 9 | ChevronRightIcon, |
| 10 | Columns2Icon, | 10 | Columns2Icon, |
| 11 | PilcrowIcon, | ||
| 11 | Rows3Icon, | 12 | Rows3Icon, |
| 12 | TextWrapIcon, | 13 | TextWrapIcon, |
| 13 | } from "lucide-react"; | 14 | } from "lucide-react"; |
| ... | @@ -172,6 +173,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -172,6 +173,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 172 | const settings = useSettings(); | 173 | const settings = useSettings(); |
| 173 | const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); | 174 | const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); |
| 174 | const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); | 175 | const [diffWordWrap, setDiffWordWrap] = useState(settings.diffWordWrap); |
| 176 | const [diffIgnoreWhitespace, setDiffIgnoreWhitespace] = useState(settings.diffIgnoreWhitespace); | ||
| 175 | const patchViewportRef = useRef<HTMLDivElement>(null); | 177 | const patchViewportRef = useRef<HTMLDivElement>(null); |
| 176 | const turnStripRef = useRef<HTMLDivElement>(null); | 178 | const turnStripRef = useRef<HTMLDivElement>(null); |
| 177 | const previousDiffOpenRef = useRef(false); | 179 | const previousDiffOpenRef = useRef(false); |
| ... | @@ -277,6 +279,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -277,6 +279,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 277 | threadId: activeThreadId, | 279 | threadId: activeThreadId, |
| 278 | fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, | 280 | fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, |
| 279 | toTurnCount: activeCheckpointRange?.toTurnCount ?? null, | 281 | toTurnCount: activeCheckpointRange?.toTurnCount ?? null, |
| 282 | ignoreWhitespace: diffIgnoreWhitespace, | ||
| 280 | cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, | 283 | cacheScope: selectedTurn ? `turn:${selectedTurn.turnId}` : conversationCacheScope, |
| 281 | enabled: isGitRepo, | 284 | enabled: isGitRepo, |
| 282 | }), | 285 | }), |
| ... | @@ -317,9 +320,10 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -317,9 +320,10 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 317 | useEffect(() => { | 320 | useEffect(() => { |
| 318 | if (diffOpen && !previousDiffOpenRef.current) { | 321 | if (diffOpen && !previousDiffOpenRef.current) { |
| 319 | setDiffWordWrap(settings.diffWordWrap); | 322 | setDiffWordWrap(settings.diffWordWrap); |
| 323 | setDiffIgnoreWhitespace(settings.diffIgnoreWhitespace); | ||
| 320 | } | 324 | } |
| 321 | previousDiffOpenRef.current = diffOpen; | 325 | previousDiffOpenRef.current = diffOpen; |
| 322 | }, [diffOpen, settings.diffWordWrap]); | 326 | }, [diffOpen, settings.diffIgnoreWhitespace, settings.diffWordWrap]); |
| 323 | 327 | ||
| 324 | useEffect(() => { | 328 | useEffect(() => { |
| 325 | if (!selectedFilePath || !patchViewportRef.current) { | 329 | if (!selectedFilePath || !patchViewportRef.current) { |
| ... | @@ -551,6 +555,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -551,6 +555,18 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 551 | > | 555 | > |
| 552 | <TextWrapIcon className="size-3" /> | 556 | <TextWrapIcon className="size-3" /> |
| 553 | </Toggle> | 557 | </Toggle> |
| 558 | <Toggle | ||
| 559 | aria-label={diffIgnoreWhitespace ? "Show whitespace changes" : "Hide whitespace changes"} | ||
| 560 | title={diffIgnoreWhitespace ? "Show whitespace changes" : "Hide whitespace changes"} | ||
| 561 | variant="outline" | ||
| 562 | size="xs" | ||
| 563 | pressed={diffIgnoreWhitespace} | ||
| 564 | onPressedChange={(pressed) => { | ||
| 565 | setDiffIgnoreWhitespace(Boolean(pressed)); | ||
| 566 | }} | ||
| 567 | > | ||
| 568 | <PilcrowIcon className="size-3" /> | ||
| 569 | </Toggle> | ||
| 554 | </div> | 570 | </div> |
| 555 | </> | 571 | </> |
| 556 | ); | 572 | ); |
apps/web/src/components/settings/SettingsPanels.tsx+30| ... | @@ -386,6 +386,9 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -386,6 +386,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 386 | ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap | 386 | ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap |
| 387 | ? ["Diff line wrapping"] | 387 | ? ["Diff line wrapping"] |
| 388 | : []), | 388 | : []), |
| 389 | ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace | ||
| 390 | ? ["Diff whitespace changes"] | ||
| 391 | : []), | ||
| 389 | ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar | 392 | ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar |
| 390 | ? ["Auto-open task panel"] | 393 | ? ["Auto-open task panel"] |
| 391 | : []), | 394 | : []), |
| ... | @@ -415,6 +418,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -415,6 +418,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 415 | settings.confirmThreadDelete, | 418 | settings.confirmThreadDelete, |
| 416 | settings.addProjectBaseDirectory, | 419 | settings.addProjectBaseDirectory, |
| 417 | settings.defaultThreadEnvMode, | 420 | settings.defaultThreadEnvMode, |
| 421 | settings.diffIgnoreWhitespace, | ||
| 418 | settings.diffWordWrap, | 422 | settings.diffWordWrap, |
| 419 | settings.enableAssistantStreaming, | 423 | settings.enableAssistantStreaming, |
| 420 | settings.timestampFormat, | 424 | settings.timestampFormat, |
| ... | @@ -889,6 +893,32 @@ export function GeneralSettingsPanel() { | ... | @@ -889,6 +893,32 @@ export function GeneralSettingsPanel() { |
| 889 | } | 893 | } |
| 890 | /> | 894 | /> |
| 891 | 895 | ||
| 896 | <SettingsRow | ||
| 897 | title="Hide whitespace changes" | ||
| 898 | description="Set whether the diff panel ignores whitespace-only edits by default." | ||
| 899 | resetAction={ | ||
| 900 | settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ( | ||
| 901 | <SettingResetButton | ||
| 902 | label="diff whitespace changes" | ||
| 903 | onClick={() => | ||
| 904 | updateSettings({ | ||
| 905 | diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace, | ||
| 906 | }) | ||
| 907 | } | ||
| 908 | /> | ||
| 909 | ) : null | ||
| 910 | } | ||
| 911 | control={ | ||
| 912 | <Switch | ||
| 913 | checked={settings.diffIgnoreWhitespace} | ||
| 914 | onCheckedChange={(checked) => | ||
| 915 | updateSettings({ diffIgnoreWhitespace: Boolean(checked) }) | ||
| 916 | } | ||
| 917 | aria-label="Hide whitespace changes by default" | ||
| 918 | /> | ||
| 919 | } | ||
| 920 | /> | ||
| 921 | |||
| 892 | <SettingsRow | 922 | <SettingsRow |
| 893 | title="Assistant output" | 923 | title="Assistant output" |
| 894 | description="Show token-by-token output while a response is in progress." | 924 | description="Show token-by-token output while a response is in progress." |
apps/web/src/lib/providerReactQuery.test.ts+57-1| ... | @@ -30,6 +30,7 @@ describe("providerQueryKeys.checkpointDiff", () => { | ... | @@ -30,6 +30,7 @@ describe("providerQueryKeys.checkpointDiff", () => { |
| 30 | threadId, | 30 | threadId, |
| 31 | fromTurnCount: 1, | 31 | fromTurnCount: 1, |
| 32 | toTurnCount: 2, | 32 | toTurnCount: 2, |
| 33 | ignoreWhitespace: false, | ||
| 33 | } as const; | 34 | } as const; |
| 34 | 35 | ||
| 35 | expect( | 36 | expect( |
| ... | @@ -44,10 +45,58 @@ describe("providerQueryKeys.checkpointDiff", () => { | ... | @@ -44,10 +45,58 @@ describe("providerQueryKeys.checkpointDiff", () => { |
| 44 | }), | 45 | }), |
| 45 | ); | 46 | ); |
| 46 | }); | 47 | }); |
| 48 | |||
| 49 | it("includes ignoreWhitespace so normal and whitespace-hidden diffs do not collide", () => { | ||
| 50 | const baseInput = { | ||
| 51 | environmentId, | ||
| 52 | threadId, | ||
| 53 | fromTurnCount: 1, | ||
| 54 | toTurnCount: 2, | ||
| 55 | cacheScope: "turn:abc", | ||
| 56 | } as const; | ||
| 57 | |||
| 58 | expect( | ||
| 59 | providerQueryKeys.checkpointDiff({ | ||
| 60 | ...baseInput, | ||
| 61 | ignoreWhitespace: false, | ||
| 62 | }), | ||
| 63 | ).not.toEqual( | ||
| 64 | providerQueryKeys.checkpointDiff({ | ||
| 65 | ...baseInput, | ||
| 66 | ignoreWhitespace: true, | ||
| 67 | }), | ||
| 68 | ); | ||
| 69 | }); | ||
| 47 | }); | 70 | }); |
| 48 | 71 | ||
| 49 | describe("checkpointDiffQueryOptions", () => { | 72 | describe("checkpointDiffQueryOptions", () => { |
| 50 | it("forwards checkpoint range to the provider API", async () => { | 73 | it("forwards checkpoint range to the provider API by default", async () => { |
| 74 | const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); | ||
| 75 | const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); | ||
| 76 | mockNativeApi({ getTurnDiff, getFullThreadDiff }); | ||
| 77 | |||
| 78 | const options = checkpointDiffQueryOptions({ | ||
| 79 | environmentId, | ||
| 80 | threadId, | ||
| 81 | fromTurnCount: 3, | ||
| 82 | toTurnCount: 4, | ||
| 83 | ignoreWhitespace: false, | ||
| 84 | cacheScope: "turn:abc", | ||
| 85 | }); | ||
| 86 | |||
| 87 | const queryClient = new QueryClient(); | ||
| 88 | await queryClient.fetchQuery(options); | ||
| 89 | |||
| 90 | expect(getTurnDiff).toHaveBeenCalledWith({ | ||
| 91 | threadId, | ||
| 92 | fromTurnCount: 3, | ||
| 93 | toTurnCount: 4, | ||
| 94 | ignoreWhitespace: false, | ||
| 95 | }); | ||
| 96 | expect(getFullThreadDiff).not.toHaveBeenCalled(); | ||
| 97 | }); | ||
| 98 | |||
| 99 | it("forwards whitespace-hidden checkpoint range to the provider API", async () => { | ||
| 51 | const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); | 100 | const getTurnDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
| 52 | const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); | 101 | const getFullThreadDiff = vi.fn().mockResolvedValue({ diff: "patch" }); |
| 53 | mockNativeApi({ getTurnDiff, getFullThreadDiff }); | 102 | mockNativeApi({ getTurnDiff, getFullThreadDiff }); |
| ... | @@ -57,6 +106,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -57,6 +106,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 57 | threadId, | 106 | threadId, |
| 58 | fromTurnCount: 3, | 107 | fromTurnCount: 3, |
| 59 | toTurnCount: 4, | 108 | toTurnCount: 4, |
| 109 | ignoreWhitespace: true, | ||
| 60 | cacheScope: "turn:abc", | 110 | cacheScope: "turn:abc", |
| 61 | }); | 111 | }); |
| 62 | 112 | ||
| ... | @@ -67,6 +117,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -67,6 +117,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 67 | threadId, | 117 | threadId, |
| 68 | fromTurnCount: 3, | 118 | fromTurnCount: 3, |
| 69 | toTurnCount: 4, | 119 | toTurnCount: 4, |
| 120 | ignoreWhitespace: true, | ||
| 70 | }); | 121 | }); |
| 71 | expect(getFullThreadDiff).not.toHaveBeenCalled(); | 122 | expect(getFullThreadDiff).not.toHaveBeenCalled(); |
| 72 | }); | 123 | }); |
| ... | @@ -81,6 +132,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -81,6 +132,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 81 | threadId, | 132 | threadId, |
| 82 | fromTurnCount: 0, | 133 | fromTurnCount: 0, |
| 83 | toTurnCount: 2, | 134 | toTurnCount: 2, |
| 135 | ignoreWhitespace: true, | ||
| 84 | cacheScope: "thread:all", | 136 | cacheScope: "thread:all", |
| 85 | }); | 137 | }); |
| 86 | 138 | ||
| ... | @@ -90,6 +142,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -90,6 +142,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 90 | expect(getFullThreadDiff).toHaveBeenCalledWith({ | 142 | expect(getFullThreadDiff).toHaveBeenCalledWith({ |
| 91 | threadId, | 143 | threadId, |
| 92 | toTurnCount: 2, | 144 | toTurnCount: 2, |
| 145 | ignoreWhitespace: true, | ||
| 93 | }); | 146 | }); |
| 94 | expect(getTurnDiff).not.toHaveBeenCalled(); | 147 | expect(getTurnDiff).not.toHaveBeenCalled(); |
| 95 | }); | 148 | }); |
| ... | @@ -104,6 +157,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -104,6 +157,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 104 | threadId, | 157 | threadId, |
| 105 | fromTurnCount: 4, | 158 | fromTurnCount: 4, |
| 106 | toTurnCount: 3, | 159 | toTurnCount: 3, |
| 160 | ignoreWhitespace: false, | ||
| 107 | cacheScope: "turn:invalid", | 161 | cacheScope: "turn:invalid", |
| 108 | }); | 162 | }); |
| 109 | 163 | ||
| ... | @@ -122,6 +176,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -122,6 +176,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 122 | threadId, | 176 | threadId, |
| 123 | fromTurnCount: 1, | 177 | fromTurnCount: 1, |
| 124 | toTurnCount: 2, | 178 | toTurnCount: 2, |
| 179 | ignoreWhitespace: false, | ||
| 125 | cacheScope: "turn:abc", | 180 | cacheScope: "turn:abc", |
| 126 | }); | 181 | }); |
| 127 | const retry = options.retry; | 182 | const retry = options.retry; |
| ... | @@ -147,6 +202,7 @@ describe("checkpointDiffQueryOptions", () => { | ... | @@ -147,6 +202,7 @@ describe("checkpointDiffQueryOptions", () => { |
| 147 | threadId, | 202 | threadId, |
| 148 | fromTurnCount: 1, | 203 | fromTurnCount: 1, |
| 149 | toTurnCount: 2, | 204 | toTurnCount: 2, |
| 205 | ignoreWhitespace: false, | ||
| 150 | cacheScope: "turn:abc", | 206 | cacheScope: "turn:abc", |
| 151 | }); | 207 | }); |
| 152 | const retryDelay = options.retryDelay; | 208 | const retryDelay = options.retryDelay; |
apps/web/src/lib/providerReactQuery.ts+4| ... | @@ -13,6 +13,7 @@ interface CheckpointDiffQueryInput { | ... | @@ -13,6 +13,7 @@ interface CheckpointDiffQueryInput { |
| 13 | threadId: ThreadId | null; | 13 | threadId: ThreadId | null; |
| 14 | fromTurnCount: number | null; | 14 | fromTurnCount: number | null; |
| 15 | toTurnCount: number | null; | 15 | toTurnCount: number | null; |
| 16 | ignoreWhitespace: boolean; | ||
| 16 | cacheScope?: string | null; | 17 | cacheScope?: string | null; |
| 17 | enabled?: boolean; | 18 | enabled?: boolean; |
| 18 | } | 19 | } |
| ... | @@ -27,6 +28,7 @@ export const providerQueryKeys = { | ... | @@ -27,6 +28,7 @@ export const providerQueryKeys = { |
| 27 | input.threadId, | 28 | input.threadId, |
| 28 | input.fromTurnCount, | 29 | input.fromTurnCount, |
| 29 | input.toTurnCount, | 30 | input.toTurnCount, |
| 31 | input.ignoreWhitespace, | ||
| 30 | input.cacheScope ?? null, | 32 | input.cacheScope ?? null, |
| 31 | ] as const, | 33 | ] as const, |
| 32 | }; | 34 | }; |
| ... | @@ -36,6 +38,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) { | ... | @@ -36,6 +38,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) { |
| 36 | return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({ | 38 | return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({ |
| 37 | threadId: input.threadId, | 39 | threadId: input.threadId, |
| 38 | toTurnCount: input.toTurnCount, | 40 | toTurnCount: input.toTurnCount, |
| 41 | ignoreWhitespace: input.ignoreWhitespace, | ||
| 39 | }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields }))); | 42 | }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields }))); |
| 40 | } | 43 | } |
| 41 | 44 | ||
| ... | @@ -43,6 +46,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) { | ... | @@ -43,6 +46,7 @@ function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) { |
| 43 | threadId: input.threadId, | 46 | threadId: input.threadId, |
| 44 | fromTurnCount: input.fromTurnCount, | 47 | fromTurnCount: input.fromTurnCount, |
| 45 | toTurnCount: input.toTurnCount, | 48 | toTurnCount: input.toTurnCount, |
| 49 | ignoreWhitespace: input.ignoreWhitespace, | ||
| 46 | }).pipe(Option.map((fields) => ({ kind: "turnDiff" as const, input: fields }))); | 50 | }).pipe(Option.map((fields) => ({ kind: "turnDiff" as const, input: fields }))); |
| 47 | } | 51 | } |
| 48 | 52 |
apps/web/src/localApi.test.ts+2| ... | @@ -537,6 +537,7 @@ describe("wsApi", () => { | ... | @@ -537,6 +537,7 @@ describe("wsApi", () => { |
| 537 | autoOpenPlanSidebar: false, | 537 | autoOpenPlanSidebar: false, |
| 538 | confirmThreadArchive: true, | 538 | confirmThreadArchive: true, |
| 539 | confirmThreadDelete: false, | 539 | confirmThreadDelete: false, |
| 540 | diffIgnoreWhitespace: true, | ||
| 540 | diffWordWrap: true, | 541 | diffWordWrap: true, |
| 541 | favorites: [], | 542 | favorites: [], |
| 542 | providerModelPreferences: {}, | 543 | providerModelPreferences: {}, |
| ... | @@ -597,6 +598,7 @@ describe("wsApi", () => { | ... | @@ -597,6 +598,7 @@ describe("wsApi", () => { |
| 597 | autoOpenPlanSidebar: false, | 598 | autoOpenPlanSidebar: false, |
| 598 | confirmThreadArchive: true, | 599 | confirmThreadArchive: true, |
| 599 | confirmThreadDelete: false, | 600 | confirmThreadDelete: false, |
| 601 | diffIgnoreWhitespace: true, | ||
| 600 | diffWordWrap: true, | 602 | diffWordWrap: true, |
| 601 | favorites: [], | 603 | favorites: [], |
| 602 | providerModelPreferences: {}, | 604 | providerModelPreferences: {}, |
packages/contracts/src/orchestration.test.ts+25| ... | @@ -8,6 +8,7 @@ import { | ... | @@ -8,6 +8,7 @@ import { |
| 8 | ModelSelection, | 8 | ModelSelection, |
| 9 | OrchestrationCommand, | 9 | OrchestrationCommand, |
| 10 | OrchestrationEvent, | 10 | OrchestrationEvent, |
| 11 | OrchestrationGetFullThreadDiffInput, | ||
| 11 | OrchestrationGetTurnDiffInput, | 12 | OrchestrationGetTurnDiffInput, |
| 12 | OrchestrationLatestTurn, | 13 | OrchestrationLatestTurn, |
| 13 | ProjectCreatedPayload, | 14 | ProjectCreatedPayload, |
| ... | @@ -24,6 +25,7 @@ import { | ... | @@ -24,6 +25,7 @@ import { |
| 24 | import { ProviderInstanceId } from "./providerInstance.ts"; | 25 | import { ProviderInstanceId } from "./providerInstance.ts"; |
| 25 | 26 | ||
| 26 | const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput); | 27 | const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput); |
| 28 | const decodeFullThreadDiffInput = Schema.decodeUnknownEffect(OrchestrationGetFullThreadDiffInput); | ||
| 27 | const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff); | 29 | const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff); |
| 28 | const decodeProjectCreateCommand = Schema.decodeUnknownEffect(ProjectCreateCommand); | 30 | const decodeProjectCreateCommand = Schema.decodeUnknownEffect(ProjectCreateCommand); |
| 29 | const decodeProjectCreatedPayload = Schema.decodeUnknownEffect(ProjectCreatedPayload); | 31 | const decodeProjectCreatedPayload = Schema.decodeUnknownEffect(ProjectCreatedPayload); |
| ... | @@ -59,6 +61,29 @@ it.effect("parses turn diff input when fromTurnCount <= toTurnCount", () => | ... | @@ -59,6 +61,29 @@ it.effect("parses turn diff input when fromTurnCount <= toTurnCount", () => |
| 59 | }), | 61 | }), |
| 60 | ); | 62 | ); |
| 61 | 63 | ||
| 64 | it.effect("parses turn diff input with whitespace ignoring enabled", () => | ||
| 65 | Effect.gen(function* () { | ||
| 66 | const parsed = yield* decodeTurnDiffInput({ | ||
| 67 | threadId: "thread-1", | ||
| 68 | fromTurnCount: 1, | ||
| 69 | toTurnCount: 2, | ||
| 70 | ignoreWhitespace: true, | ||
| 71 | }); | ||
| 72 | assert.strictEqual(parsed.ignoreWhitespace, true); | ||
| 73 | }), | ||
| 74 | ); | ||
| 75 | |||
| 76 | it.effect("parses full thread diff input with whitespace ignoring enabled", () => | ||
| 77 | Effect.gen(function* () { | ||
| 78 | const parsed = yield* decodeFullThreadDiffInput({ | ||
| 79 | threadId: "thread-1", | ||
| 80 | toTurnCount: 2, | ||
| 81 | ignoreWhitespace: true, | ||
| 82 | }); | ||
| 83 | assert.strictEqual(parsed.ignoreWhitespace, true); | ||
| 84 | }), | ||
| 85 | ); | ||
| 86 | |||
| 62 | it.effect("rejects turn diff input when fromTurnCount > toTurnCount", () => | 87 | it.effect("rejects turn diff input when fromTurnCount > toTurnCount", () => |
| 63 | Effect.gen(function* () { | 88 | Effect.gen(function* () { |
| 64 | const result = yield* Effect.exit( | 89 | const result = yield* Effect.exit( |
packages/contracts/src/orchestration.ts+5-1| ... | @@ -1171,7 +1171,10 @@ export const DispatchResult = Schema.Struct({ | ... | @@ -1171,7 +1171,10 @@ export const DispatchResult = Schema.Struct({ |
| 1171 | export type DispatchResult = typeof DispatchResult.Type; | 1171 | export type DispatchResult = typeof DispatchResult.Type; |
| 1172 | 1172 | ||
| 1173 | export const OrchestrationGetTurnDiffInput = TurnCountRange.mapFields( | 1173 | export const OrchestrationGetTurnDiffInput = TurnCountRange.mapFields( |
| 1174 | Struct.assign({ threadId: ThreadId }), | 1174 | Struct.assign({ |
| 1175 | threadId: ThreadId, | ||
| 1176 | ignoreWhitespace: Schema.optionalKey(Schema.Boolean), | ||
| 1177 | }), | ||
| 1175 | { unsafePreserveChecks: true }, | 1178 | { unsafePreserveChecks: true }, |
| 1176 | ); | 1179 | ); |
| 1177 | export type OrchestrationGetTurnDiffInput = typeof OrchestrationGetTurnDiffInput.Type; | 1180 | export type OrchestrationGetTurnDiffInput = typeof OrchestrationGetTurnDiffInput.Type; |
| ... | @@ -1182,6 +1185,7 @@ export type OrchestrationGetTurnDiffResult = typeof OrchestrationGetTurnDiffResu | ... | @@ -1182,6 +1185,7 @@ export type OrchestrationGetTurnDiffResult = typeof OrchestrationGetTurnDiffResu |
| 1182 | export const OrchestrationGetFullThreadDiffInput = Schema.Struct({ | 1185 | export const OrchestrationGetFullThreadDiffInput = Schema.Struct({ |
| 1183 | threadId: ThreadId, | 1186 | threadId: ThreadId, |
| 1184 | toTurnCount: NonNegativeInt, | 1187 | toTurnCount: NonNegativeInt, |
| 1188 | ignoreWhitespace: Schema.optionalKey(Schema.Boolean), | ||
| 1185 | }); | 1189 | }); |
| 1186 | export type OrchestrationGetFullThreadDiffInput = typeof OrchestrationGetFullThreadDiffInput.Type; | 1190 | export type OrchestrationGetFullThreadDiffInput = typeof OrchestrationGetFullThreadDiffInput.Type; |
| 1187 | 1191 |
packages/contracts/src/settings.ts+2| ... | @@ -32,6 +32,7 @@ export const ClientSettingsSchema = Schema.Struct({ | ... | @@ -32,6 +32,7 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 32 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | 32 | autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 33 | confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | 33 | confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 34 | confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | 34 | confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 35 | diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), | ||
| 35 | diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | 36 | diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 36 | // Model favorites. Historically keyed by provider kind, now | 37 | // Model favorites. Historically keyed by provider kind, now |
| 37 | // widened to `ProviderInstanceId` so users can favorite a specific model | 38 | // widened to `ProviderInstanceId` so users can favorite a specific model |
| ... | @@ -451,6 +452,7 @@ export const ClientSettingsPatch = Schema.Struct({ | ... | @@ -451,6 +452,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 451 | autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean), | 452 | autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean), |
| 452 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), | 453 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), |
| 453 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), | 454 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), |
| 455 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), | ||
| 454 | diffWordWrap: Schema.optionalKey(Schema.Boolean), | 456 | diffWordWrap: Schema.optionalKey(Schema.Boolean), |
| 455 | favorites: Schema.optionalKey( | 457 | favorites: Schema.optionalKey( |
| 456 | Schema.Array( | 458 | Schema.Array( |