@nkzw/codiff-core 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { filterPatchToHunkIds, getSectionWalkthroughHunks, isGeneratedWalkthroughFile, isSyntheticWalkthroughHunk } from "./lib/narrative-walkthrough-diff.mjs";
1
+ import { filterPatchToHunkIds, getHunkOrdinal, getSectionWalkthroughHunks, isGeneratedWalkthroughFile, isSyntheticWalkthroughHunk } from "./lib/narrative-walkthrough-diff.mjs";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import { cva } from "class-variance-authority";
4
- import { Fragment, Suspense, forwardRef, lazy, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useOptimistic, useRef, useState, useTransition } from "react";
4
+ import { Fragment, Suspense, forwardRef, lazy, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useOptimistic, useRef, useState, useSyncExternalStore, useTransition } from "react";
5
5
  import { createPortal, useFormStatus } from "react-dom";
6
6
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { MarkdownEditor } from "@nkzw/mdx-editor";
@@ -14,7 +14,7 @@ import { ChatCircleIcon } from "@phosphor-icons/react/ChatCircle";
14
14
  import { PathIcon } from "@phosphor-icons/react/Path";
15
15
  import { TreeStructureIcon } from "@phosphor-icons/react/TreeStructure";
16
16
  import { FileTree, useFileTree } from "@pierre/trees/react";
17
- import { hydratePartialDiff, parseDiffFromFile, parsePatchFiles, registerCustomTheme, setCustomExtension } from "@pierre/diffs";
17
+ import { cloneFileDiffMetadata, getFiletypeFromFileName, getHighlighterOptions, hydratePartialDiff, parseDiffFromFile, parsePatchFiles, preloadHighlighter, registerCustomTheme, setCustomExtension } from "@pierre/diffs";
18
18
  import useRelativeTime from "@nkzw/use-relative-time";
19
19
  import { CaretDownIcon } from "@phosphor-icons/react/CaretDown";
20
20
  import "@phosphor-icons/react/CaretUp";
@@ -26,8 +26,9 @@ import { WarningOctagonIcon } from "@phosphor-icons/react/WarningOctagon";
26
26
  import { XIcon } from "@phosphor-icons/react/X";
27
27
  import { ColumnsIcon } from "@phosphor-icons/react/Columns";
28
28
  import { ImageBrokenIcon } from "@phosphor-icons/react/ImageBroken";
29
+ import { PencilSimpleIcon } from "@phosphor-icons/react/PencilSimple";
29
30
  import { SquareSplitVerticalIcon } from "@phosphor-icons/react/SquareSplitVertical";
30
- import { CodeView, WorkerPoolContextProvider } from "@pierre/diffs/react";
31
+ import { CodeView, EditProvider, WorkerPoolContextProvider } from "@pierre/diffs/react";
31
32
  import { CrosshairSimpleIcon } from "@phosphor-icons/react/CrosshairSimple";
32
33
  import { ProhibitIcon } from "@phosphor-icons/react/Prohibit";
33
34
  import { PersistentMarkdownEditor } from "@nkzw/mdx-editor/persistence";
@@ -3574,14 +3575,6 @@ registerCustomTheme("Licht", async () => licht_default);
3574
3575
  registerCustomTheme("Dunkel", async () => dunkel_default);
3575
3576
  setCustomExtension("cts", "typescript");
3576
3577
  setCustomExtension("mts", "typescript");
3577
- const statusLabel = {
3578
- added: "Added",
3579
- conflicted: "Conflicted",
3580
- deleted: "Deleted",
3581
- modified: "Modified",
3582
- renamed: "Renamed",
3583
- untracked: "Untracked"
3584
- };
3585
3578
  const sectionLabel = {
3586
3579
  commit: "Commit",
3587
3580
  "pull-request": "PR",
@@ -3601,7 +3594,7 @@ const workerHighlighterOptions = {
3601
3594
  light: "Licht"
3602
3595
  },
3603
3596
  tokenizeMaxLineLength: 2e4,
3604
- useTokenTransformer: false
3597
+ useTokenTransformer: true
3605
3598
  };
3606
3599
  const codeViewUnsafeCSS = `
3607
3600
  :host {
@@ -3934,7 +3927,7 @@ const getItemVersion = (value) => {
3934
3927
  //#region lib/keyboard.ts
3935
3928
  const isNativeInputTarget = (target) => {
3936
3929
  const candidate = target;
3937
- return candidate?.closest?.("input, select, textarea") != null || candidate?.isContentEditable === true;
3930
+ return candidate?.closest?.("input, select, textarea, [contenteditable=\"true\"]") != null || candidate?.isContentEditable === true || candidate?.shadowRoot?.activeElement != null && isNativeInputTarget(candidate.shadowRoot.activeElement);
3938
3931
  };
3939
3932
  const isMacPlatform = (platform = navigator.platform) => platform.toLowerCase().includes("mac");
3940
3933
  const isPrimaryModifier = (event, platform = navigator.platform) => isMacPlatform(platform) ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
@@ -4052,6 +4045,477 @@ const getSourceKey = (source) => source.type === "commit" ? `commit:${source.ref
4052
4045
  const getShortRef = (ref) => ref.slice(0, 7);
4053
4046
  const getSourceLabel = (source) => source.type === "commit" ? getShortRef(source.ref) : source.type === "branch" || source.type === "branch-diff" ? `Branch vs ${source.ref}` : source.type === "branch-working-tree" ? `Local + branch vs ${source.ref}` : source.type === "range" ? rangeLabel(source) : source.type === "pull-request" ? source.number ? `${source.provider === "gitlab" ? "MR" : "PR"} #${source.number}` : source.provider === "gitlab" ? "Merge request" : "Pull request" : "Uncommitted";
4054
4047
  //#endregion
4048
+ //#region config/defaults.ts
4049
+ const defaultConfigTemplate = {
4050
+ settings: {
4051
+ "agentBackend": "codex",
4052
+ "checkForUpdates": true,
4053
+ "claudeModel": "claude-sonnet-4-6",
4054
+ "codeFontFamily": "",
4055
+ "codeFontSize": 13,
4056
+ "copyCommentsOnClose": false,
4057
+ "diffStyle": "split",
4058
+ "editorCommand": "",
4059
+ "lastRepositoryPath": "",
4060
+ "openAIModel": "gpt-5.6-terra",
4061
+ "opencodeModel": "opencode-default",
4062
+ "piModel": "pi-default",
4063
+ "reviewCommentsPrefix": "# Address these Review Comments",
4064
+ "sidebarPosition": "left",
4065
+ "showOutdated": false,
4066
+ "showWhitespace": false,
4067
+ "theme": "system",
4068
+ "walkthroughPrompt": "",
4069
+ "wordWrap": false
4070
+ },
4071
+ keymap: {
4072
+ "askAgent": "Mod+Alt+Enter",
4073
+ "closeSearch": "Escape",
4074
+ "commandBar": "Mod+k",
4075
+ "diffSearch": "Mod+f",
4076
+ "discardComment": "Escape",
4077
+ "doneEditing": "Mod+s",
4078
+ "editFile": "Mod+e",
4079
+ "fileFilter": "Mod+p",
4080
+ "nextHunk": ["Ctrl+ArrowDown", "j"],
4081
+ "nextSearchMatch": "Enter",
4082
+ "openFile": "Mod+Shift+o",
4083
+ "prevHunk": ["Ctrl+ArrowUp", "k"],
4084
+ "prevSearchMatch": "Shift+Enter",
4085
+ "shortcutsHelp": "Shift+?",
4086
+ "submitComment": "Mod+Enter",
4087
+ "toggleSidebar": "Mod+Shift+b",
4088
+ "toggleWordWrap": "Alt+z"
4089
+ }
4090
+ };
4091
+ const createDefaultConfig = () => ({
4092
+ keymap: { ...defaultConfigTemplate.keymap },
4093
+ settings: { ...defaultConfigTemplate.settings }
4094
+ });
4095
+ const defaultKeymap = Object.freeze({ ...defaultConfigTemplate.keymap });
4096
+ Object.freeze({ ...defaultConfigTemplate.settings });
4097
+ const createFileEditingStore = (onChange) => {
4098
+ const sessionRef = { current: null };
4099
+ const draftRef = { current: "" };
4100
+ let timer;
4101
+ let pendingWrite = null;
4102
+ const clearTimer = () => {
4103
+ clearTimeout(timer);
4104
+ timer = void 0;
4105
+ };
4106
+ const setSession = (session) => {
4107
+ sessionRef.current = session ? {
4108
+ ...session,
4109
+ dirty: draftRef.current !== session.document.content,
4110
+ hasChanges: session.finishing === "revert" ? session.hasChanges : draftRef.current !== session.originalContent
4111
+ } : null;
4112
+ if (!session) clearTimer();
4113
+ onChange();
4114
+ };
4115
+ const scheduleSave = () => {
4116
+ clearTimer();
4117
+ timer = setTimeout(() => void flush(), 300);
4118
+ };
4119
+ const flush = () => {
4120
+ clearTimer();
4121
+ if (pendingWrite) return pendingWrite.then((saved) => saved ? flush() : false);
4122
+ const current = sessionRef.current;
4123
+ if (!current || draftRef.current === current.document.content) return Promise.resolve(true);
4124
+ const content = draftRef.current;
4125
+ setSession({
4126
+ ...current,
4127
+ saveError: void 0,
4128
+ saving: true
4129
+ });
4130
+ pendingWrite = Promise.resolve().then(() => window.codiff.saveRepositoryFile({
4131
+ ...current.document,
4132
+ baseVersion: current.document.version,
4133
+ content
4134
+ })).then((result) => {
4135
+ if (result.status === "conflict") throw new Error("The file changed on disk. Your edits are kept; copy them before refreshing.");
4136
+ const latest = sessionRef.current;
4137
+ if (latest) setSession({
4138
+ ...latest,
4139
+ document: result.document,
4140
+ saving: false
4141
+ });
4142
+ return true;
4143
+ }).catch((error) => {
4144
+ const latest = sessionRef.current;
4145
+ if (latest) setSession({
4146
+ ...latest,
4147
+ saveError: error instanceof Error ? error.message : String(error),
4148
+ saving: false
4149
+ });
4150
+ return false;
4151
+ }).finally(() => {
4152
+ pendingWrite = null;
4153
+ const latest = sessionRef.current;
4154
+ if (latest && !latest.finishing && !latest.saveError && draftRef.current !== latest.document.content) scheduleSave();
4155
+ });
4156
+ return pendingWrite;
4157
+ };
4158
+ const finish = async (revert, beforeClose) => {
4159
+ const current = sessionRef.current;
4160
+ if (!current || current.finishing) return false;
4161
+ clearTimer();
4162
+ setSession({
4163
+ ...current,
4164
+ finishing: revert ? "revert" : "done"
4165
+ });
4166
+ let previousDraft;
4167
+ try {
4168
+ if (pendingWrite && !await pendingWrite) return false;
4169
+ if (revert) {
4170
+ previousDraft = draftRef.current;
4171
+ draftRef.current = current.originalContent;
4172
+ }
4173
+ do {
4174
+ do
4175
+ if (!await flush()) {
4176
+ if (previousDraft !== void 0) draftRef.current = previousDraft;
4177
+ return false;
4178
+ }
4179
+ while (!revert && draftRef.current !== sessionRef.current?.document.content);
4180
+ await beforeClose?.();
4181
+ } while (!revert && draftRef.current !== sessionRef.current?.document.content);
4182
+ setSession(null);
4183
+ return true;
4184
+ } catch (error) {
4185
+ if (previousDraft !== void 0) draftRef.current = previousDraft;
4186
+ throw error;
4187
+ } finally {
4188
+ const latest = sessionRef.current;
4189
+ if (latest) setSession({
4190
+ ...latest,
4191
+ finishing: void 0
4192
+ });
4193
+ }
4194
+ };
4195
+ return {
4196
+ draftRef,
4197
+ finish,
4198
+ flush,
4199
+ getSnapshot: () => sessionRef.current,
4200
+ setDraft: (content) => {
4201
+ const current = sessionRef.current;
4202
+ if (!current || current.finishing === "revert" || content === draftRef.current) return;
4203
+ const wasDirty = draftRef.current !== current.document.content;
4204
+ draftRef.current = content;
4205
+ const isDirty = content !== current.document.content;
4206
+ if (isDirty && !current.finishing && !current.saveError) scheduleSave();
4207
+ else clearTimer();
4208
+ if (wasDirty !== isDirty || current.hasChanges !== (content !== current.originalContent)) setSession({ ...current });
4209
+ },
4210
+ setSession
4211
+ };
4212
+ };
4213
+ const getCodeEditKey = (sourceKey, itemId) => JSON.stringify([sourceKey, itemId]);
4214
+ const createCodeEditingStore = () => {
4215
+ const entries = /* @__PURE__ */ new Map();
4216
+ const listeners = /* @__PURE__ */ new Set();
4217
+ let snapshot = /* @__PURE__ */ new Map();
4218
+ const hasFile = (path) => [...snapshot.values()].some((session) => session.document.path === path);
4219
+ return {
4220
+ clear: () => {
4221
+ for (const entry of entries.values()) entry.setSession(null);
4222
+ },
4223
+ flush: (sourceKey) => Promise.all([...entries.values()].filter((entry) => entry.getSnapshot()?.sourceKey === sourceKey).map((entry) => entry.flush())),
4224
+ get: (sourceKey, itemId) => entries.get(getCodeEditKey(sourceKey, itemId)),
4225
+ getForItem: (item) => item.type === "diff" ? [...entries.values()].find((entry) => {
4226
+ const session = entry.getSnapshot();
4227
+ return session?.item.id === item.id && session.item.fileDiff === item.fileDiff;
4228
+ }) : void 0,
4229
+ getSnapshot: () => snapshot,
4230
+ hasFile,
4231
+ hasUnsavedEdits: () => [...entries.values()].some((entry) => {
4232
+ const session = entry.getSnapshot();
4233
+ return session && (session.saving || entry.draftRef.current !== session.document.content);
4234
+ }),
4235
+ start: (session) => {
4236
+ if (hasFile(session.document.path)) return null;
4237
+ const key = getCodeEditKey(session.sourceKey, session.item.id);
4238
+ const entry = createFileEditingStore(() => {
4239
+ const next = new Map(snapshot);
4240
+ const current = entry.getSnapshot();
4241
+ if (current) next.set(key, current);
4242
+ else {
4243
+ next.delete(key);
4244
+ entries.delete(key);
4245
+ }
4246
+ snapshot = next;
4247
+ for (const listener of listeners) listener();
4248
+ });
4249
+ entries.set(key, entry);
4250
+ entry.draftRef.current = session.document.content;
4251
+ entry.setSession(session);
4252
+ return entry;
4253
+ },
4254
+ subscribe: (listener) => {
4255
+ listeners.add(listener);
4256
+ return () => {
4257
+ listeners.delete(listener);
4258
+ };
4259
+ }
4260
+ };
4261
+ };
4262
+ //#endregion
4263
+ //#region app/hooks/useCodeEditing.ts
4264
+ let editorModule = null;
4265
+ let editorModulePromise = null;
4266
+ const loadEditor = () => editorModulePromise ??= import("@pierre/diffs/edit").then((module) => {
4267
+ editorModule = module;
4268
+ return module;
4269
+ }).catch((error) => {
4270
+ editorModulePromise = null;
4271
+ throw error;
4272
+ });
4273
+ const prepareEditor = (fileDiff) => {
4274
+ const lang = fileDiff.lang ?? getFiletypeFromFileName(fileDiff.name);
4275
+ return Promise.all([loadEditor(), preloadHighlighter(getHighlighterOptions(lang, workerHighlighterOptions))]);
4276
+ };
4277
+ const createCodeEditor = (type, options, key) => {
4278
+ if (!editorModule) throw new Error("The code editor has not loaded.");
4279
+ return new editorModule.Editor(type, options, key);
4280
+ };
4281
+ function useUnsavedCodeEditsGuard(store) {
4282
+ useEffect(() => {
4283
+ const beforeUnload = (event) => {
4284
+ if (store.hasUnsavedEdits()) {
4285
+ event.preventDefault();
4286
+ event.returnValue = "";
4287
+ }
4288
+ };
4289
+ window.addEventListener("beforeunload", beforeUnload);
4290
+ return () => window.removeEventListener("beforeunload", beforeUnload);
4291
+ }, [store]);
4292
+ }
4293
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
4294
+ const getFirstVisibleChangedLine = (root, viewport) => {
4295
+ const viewportRect = viewport.getBoundingClientRect();
4296
+ const header = root.querySelector("[data-diffs-header][data-sticky]");
4297
+ const top = Math.max(viewportRect.top, header?.getBoundingClientRect().bottom ?? viewportRect.top) + 11;
4298
+ for (const row of root.querySelectorAll("[data-content] > [data-line-type=\"change-addition\"]")) {
4299
+ const rect = row.getBoundingClientRect();
4300
+ if (rect.height > 0 && rect.top >= top && rect.top < viewportRect.bottom) {
4301
+ const line = Number(row.dataset.line);
4302
+ if (Number.isInteger(line) && line > 0) return line;
4303
+ }
4304
+ }
4305
+ };
4306
+ function useCodeEditing({ codeViewRef, items, keymap = defaultKeymap, onLoadSectionContents, onRefresh, selectedItemId, sourceKey, store: sharedStore }) {
4307
+ const [localStore] = useState(createCodeEditingStore);
4308
+ const store = sharedStore ?? localStore;
4309
+ const sessions = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
4310
+ useUnsavedCodeEditsGuard(store);
4311
+ const busyRef = useRef(/* @__PURE__ */ new Set());
4312
+ const [busyItemKeys, setBusyItemKeys] = useState(() => /* @__PURE__ */ new Set());
4313
+ const [errors, setErrors] = useState(() => /* @__PURE__ */ new Map());
4314
+ const setBusy = useCallback((key, busy) => {
4315
+ if (busy) busyRef.current.add(key);
4316
+ else busyRef.current.delete(key);
4317
+ setBusyItemKeys(new Set(busyRef.current));
4318
+ }, []);
4319
+ const setError = useCallback((key, message) => {
4320
+ setErrors((current) => {
4321
+ const next = new Map(current);
4322
+ if (message) next.set(key, message);
4323
+ else next.delete(key);
4324
+ return next;
4325
+ });
4326
+ }, []);
4327
+ const currentSessions = useMemo(() => new Map([...sessions.values()].filter((session) => session.sourceKey === sourceKey).map((session) => [session.item.id, session])), [sessions, sourceKey]);
4328
+ const editorOptions = useMemo(() => {
4329
+ const focusedEditors = /* @__PURE__ */ new WeakSet();
4330
+ return { onAttach: (editor, fileInstance) => {
4331
+ if (!focusedEditors.has(editor)) {
4332
+ focusedEditors.add(editor);
4333
+ const viewer = codeViewRef.current?.getInstance();
4334
+ const root = viewer?.getRenderedItems().find(({ instance }) => instance === fileInstance)?.element.shadowRoot;
4335
+ const viewport = viewer?.getContainerElement();
4336
+ editor.focus({
4337
+ character: 0,
4338
+ lineNumber: (root && viewport ? getFirstVisibleChangedLine(root, viewport) : void 0) ?? "first-visible",
4339
+ offset: 11,
4340
+ preventScroll: true
4341
+ });
4342
+ const position = editor.getViewState().selections?.[0]?.start;
4343
+ const indentation = (position && editor.getEditState()?.document.getLineText(position.line))?.match(/^\s*/)?.[0].length ?? 0;
4344
+ if (position && indentation > 0) editor.focus({
4345
+ character: indentation,
4346
+ lineNumber: position.line + 1,
4347
+ preventScroll: true
4348
+ });
4349
+ }
4350
+ } };
4351
+ }, [codeViewRef]);
4352
+ const loadEditContents = useCallback((item, metadata) => {
4353
+ if (!item.fileDiff.isPartial) return Promise.resolve({
4354
+ newFile: {
4355
+ contents: item.fileDiff.additionLines.join(""),
4356
+ name: item.fileDiff.name
4357
+ },
4358
+ oldFile: null
4359
+ });
4360
+ return loadSectionContents(metadata.file, metadata.section, onLoadSectionContents);
4361
+ }, [onLoadSectionContents]);
4362
+ const prepareEdit = useCallback(async (item, metadata) => {
4363
+ if (item.type === "diff" && onLoadSectionContents) {
4364
+ const [, loaded] = await Promise.all([prepareEditor(item.fileDiff), loadEditContents(item, metadata)]);
4365
+ const rendered = codeViewRef.current?.getInstance()?.getRenderedItems().find(({ id }) => id === item.id);
4366
+ if (rendered?.type === "diff") {
4367
+ const fileDiff = item.fileDiff.isPartial ? hydratePartialDiff("clone", item.fileDiff, loaded) : item.fileDiff;
4368
+ await rendered.instance.primeHighlightCache(fileDiff);
4369
+ }
4370
+ }
4371
+ }, [
4372
+ codeViewRef,
4373
+ loadEditContents,
4374
+ onLoadSectionContents
4375
+ ]);
4376
+ const startEdit = useCallback(async (item, metadata) => {
4377
+ const itemKey = getCodeEditKey(sourceKey, item.id);
4378
+ if (item.type !== "diff" || !onLoadSectionContents || store.hasFile(metadata.file.path) || busyRef.current.has(itemKey)) return;
4379
+ setBusy(itemKey, true);
4380
+ setError(itemKey);
4381
+ try {
4382
+ const [, loaded, document] = await Promise.all([
4383
+ prepareEditor(item.fileDiff),
4384
+ loadEditContents(item, metadata),
4385
+ window.codiff.getRepositoryFile(metadata.file.path)
4386
+ ]);
4387
+ if (!loaded.newFile || loaded.newFile.contents !== document.content) throw new Error("The file changed on disk. Refresh the diff before editing.");
4388
+ const key = crypto.randomUUID();
4389
+ const fileDiff = item.fileDiff.isPartial ? hydratePartialDiff("clone", item.fileDiff, loaded) : cloneFileDiffMetadata(item.fileDiff);
4390
+ fileDiff.cacheKey ??= `code-edit:${key}`;
4391
+ const rendered = codeViewRef.current?.getInstance()?.getRenderedItems().find(({ id }) => id === item.id);
4392
+ if (rendered?.type === "diff") await rendered.instance.primeHighlightCache(fileDiff);
4393
+ const nextSession = {
4394
+ dirty: false,
4395
+ document,
4396
+ hasChanges: false,
4397
+ item: {
4398
+ ...item,
4399
+ collapsed: false,
4400
+ edit: true,
4401
+ fileDiff,
4402
+ version: getItemVersion(`code-edit:${key}`)
4403
+ },
4404
+ metadata,
4405
+ originalContent: document.content,
4406
+ sourceKey
4407
+ };
4408
+ if (store.start(nextSession)) codeViewRef.current?.clearSelectedLines();
4409
+ } catch (error) {
4410
+ setError(itemKey, errorMessage(error));
4411
+ } finally {
4412
+ setBusy(itemKey, false);
4413
+ }
4414
+ }, [
4415
+ codeViewRef,
4416
+ loadEditContents,
4417
+ onLoadSectionContents,
4418
+ setBusy,
4419
+ setError,
4420
+ sourceKey,
4421
+ store
4422
+ ]);
4423
+ const finishEdit = useCallback(async (itemId, revert) => {
4424
+ const itemKey = getCodeEditKey(sourceKey, itemId);
4425
+ const entry = store.get(sourceKey, itemId);
4426
+ const current = entry?.getSnapshot();
4427
+ if (!entry || !current || busyRef.current.has(itemKey)) return;
4428
+ setBusy(itemKey, true);
4429
+ setError(itemKey);
4430
+ const needsRefresh = current.hasChanges || current.document.content !== current.originalContent || current.saving;
4431
+ try {
4432
+ await entry.finish(revert, async () => {
4433
+ const latest = entry.getSnapshot();
4434
+ if ((needsRefresh || latest?.hasChanges) && onRefresh && !await onRefresh(current.metadata.file, current.metadata.section)) throw new Error("File saved. Refresh the review to see the updated diff.");
4435
+ });
4436
+ } catch (error) {
4437
+ setError(itemKey, errorMessage(error));
4438
+ } finally {
4439
+ setBusy(itemKey, false);
4440
+ }
4441
+ }, [
4442
+ onRefresh,
4443
+ setBusy,
4444
+ setError,
4445
+ sourceKey,
4446
+ store
4447
+ ]);
4448
+ const doneEdit = useCallback((itemId) => finishEdit(itemId, false), [finishEdit]);
4449
+ const revertEdit = useCallback((itemId) => finishEdit(itemId, true), [finishEdit]);
4450
+ const onItemEditChange = useCallback((event, item) => {
4451
+ store.getForItem(item)?.setDraft(event.file.contents);
4452
+ }, [store]);
4453
+ const onItemEditComplete = useCallback((_event, item, nextItem) => {
4454
+ const entry = store.getForItem(item);
4455
+ const current = entry?.getSnapshot();
4456
+ if (!entry || !current || nextItem.type !== "diff") return "reject";
4457
+ nextItem.fileDiff.cacheKey = `code-edit:${crypto.randomUUID()}`;
4458
+ entry.setSession({
4459
+ ...current,
4460
+ item: {
4461
+ ...nextItem,
4462
+ edit: true
4463
+ }
4464
+ });
4465
+ return "accept";
4466
+ }, [store]);
4467
+ const editedItems = useMemo(() => {
4468
+ if (currentSessions.size === 0) return items;
4469
+ const found = /* @__PURE__ */ new Set();
4470
+ const next = items.map((item) => {
4471
+ const session = currentSessions.get(item.id);
4472
+ if (!session) return item;
4473
+ found.add(item.id);
4474
+ return {
4475
+ ...session.item,
4476
+ collapsed: item.collapsed
4477
+ };
4478
+ });
4479
+ for (const session of currentSessions.values()) if (!found.has(session.item.id)) next.push(session.item);
4480
+ return next;
4481
+ }, [currentSessions, items]);
4482
+ useEffect(() => {
4483
+ if (currentSessions.size === 0) return;
4484
+ const keyDown = (event) => {
4485
+ if (event.defaultPrevented || event.repeat || event.isComposing || !matchesShortcut(event, keymap, "doneEditing")) return;
4486
+ const path = event.composedPath();
4487
+ if (isNativeInputTarget(event.target) && !path.some((node) => node.matches?.("[data-content][contenteditable=\"true\"]"))) return;
4488
+ const itemId = codeViewRef.current?.getInstance()?.getRenderedItems().find(({ element, id }) => currentSessions.has(id) && path.includes(element))?.id ?? selectedItemId ?? (currentSessions.size === 1 ? currentSessions.keys().next().value : void 0);
4489
+ if (itemId == null || !currentSessions.has(itemId)) return;
4490
+ event.preventDefault();
4491
+ event.stopPropagation();
4492
+ doneEdit(itemId);
4493
+ };
4494
+ window.addEventListener("keydown", keyDown, true);
4495
+ return () => window.removeEventListener("keydown", keyDown, true);
4496
+ }, [
4497
+ codeViewRef,
4498
+ currentSessions,
4499
+ doneEdit,
4500
+ keymap,
4501
+ selectedItemId
4502
+ ]);
4503
+ return {
4504
+ canStartEdit: (itemId, path) => !busyItemKeys.has(getCodeEditKey(sourceKey, itemId)) && ![...sessions.values()].some((session) => session.document.path === path),
4505
+ doneEdit,
4506
+ editorOptions,
4507
+ getError: (itemId) => currentSessions.get(itemId)?.saveError ?? errors.get(getCodeEditKey(sourceKey, itemId)),
4508
+ isBusy: (itemId) => busyItemKeys.has(getCodeEditKey(sourceKey, itemId)) || currentSessions.get(itemId)?.finishing != null,
4509
+ items: editedItems,
4510
+ onItemEditChange,
4511
+ onItemEditComplete,
4512
+ prepareEdit,
4513
+ revertEdit,
4514
+ sessions: currentSessions,
4515
+ startEdit
4516
+ };
4517
+ }
4518
+ //#endregion
4055
4519
  //#region app/hooks/useCodeViewAnnotations.ts
4056
4520
  function useCodeViewAnnotations() {
4057
4521
  const [getAnnotations] = useState(() => {
@@ -4484,14 +4948,17 @@ const resolveWalkthroughHunkFile = (hunk, files) => {
4484
4948
  };
4485
4949
  };
4486
4950
  const walkthroughHunkRunKey = (item, hunks) => `${item.id}:${hunks.map((hunk) => hunk.id).join(",")}`;
4487
- /** Resolve item hunks in authored order, coalescing adjacent hunks from the same file section. */
4951
+ /** Resolve authored hunk order, coalescing only forward runs within each file section. */
4488
4952
  const resolveWalkthroughHunkRuns = (item, files) => {
4489
4953
  const runs = [];
4490
4954
  for (const hunk of item.hunks) {
4491
4955
  const resolved = resolveWalkthroughHunkFile(hunk, files);
4492
4956
  if (!resolved) continue;
4493
4957
  const previous = runs.at(-1);
4494
- if (previous && previous.resolved.file.path === resolved.file.path && previous.resolved.section.id === resolved.section.id) {
4958
+ const previousHunk = previous?.hunks.at(-1);
4959
+ const previousOrdinal = previousHunk ? getHunkOrdinal(resolved.section.id, previousHunk.id) : null;
4960
+ const ordinal = getHunkOrdinal(resolved.section.id, hunk.id);
4961
+ if (previous && previous.resolved.file.path === resolved.file.path && previous.resolved.section.id === resolved.section.id && previousOrdinal != null && ordinal != null && previousOrdinal < ordinal) {
4495
4962
  const hunks = [...previous.hunks, hunk];
4496
4963
  runs[runs.length - 1] = {
4497
4964
  ...previous,
@@ -5095,124 +5562,165 @@ function CopyFilePathButton({ path }) {
5095
5562
  })
5096
5563
  });
5097
5564
  }
5098
- function CodeViewHeader({ allowViewedToggle, canCreateFileComment, isSectionLoading, meta, onCreateFileComment, onLoadSection, onOpenFile, onToggleCollapsed, onToggleMarkdownPreview, onToggleViewed, readOnly }) {
5565
+ function CodeEditButton({ disabled, isBusy, item, meta, onEdit, onPrepare }) {
5566
+ useEffect(() => {
5567
+ onPrepare(item, meta).catch(() => {});
5568
+ }, [
5569
+ item,
5570
+ meta,
5571
+ onPrepare
5572
+ ]);
5573
+ return /* @__PURE__ */ jsxs(Button, {
5574
+ "aria-label": `Edit ${meta.file.path}`,
5575
+ disabled: disabled || isBusy,
5576
+ onClick: onEdit,
5577
+ title: "Edit working-tree file",
5578
+ type: "button",
5579
+ children: [/* @__PURE__ */ jsx(PencilSimpleIcon, {
5580
+ "aria-hidden": true,
5581
+ size: 14,
5582
+ weight: "bold"
5583
+ }), "Edit"]
5584
+ });
5585
+ }
5586
+ function CodeViewHeader({ allowViewedToggle, canCreateFileComment, editActions, isEditing = false, isSectionLoading, meta, onCreateFileComment, onLoadSection, onOpenFile, onToggleCollapsed, onToggleMarkdownPreview, onToggleViewed, readOnly }) {
5099
5587
  const { canRenderMarkdown, file, isCollapsed, isMarkdownPreview, isSelected, isViewed, lineCount, reviewIdentity, section, sectionCount, walkthroughNote } = meta;
5100
5588
  const canOpenFile = file.status !== "deleted";
5101
5589
  const canLoadSection = shouldLoadDiffSectionContents(section);
5590
+ const directoryEnd = file.path.lastIndexOf("/") + 1;
5102
5591
  return /* @__PURE__ */ jsxs("div", {
5103
5592
  className: `codiff-file-header${walkthroughNote ? " with-note" : ""}${isCollapsed ? " collapsed" : ""}${isSelected ? " selected" : ""}${isViewed ? " viewed" : ""}`,
5104
- children: [
5105
- /* @__PURE__ */ jsxs("div", {
5106
- "aria-expanded": !isCollapsed,
5107
- "aria-label": isCollapsed ? "Expand file" : "Collapse file",
5108
- className: "codiff-header-toggle",
5109
- onClick: () => onToggleCollapsed(file, isCollapsed, reviewIdentity.key),
5110
- onKeyDown: (event) => {
5111
- if (event.key === "Enter" || event.key === " ") {
5112
- event.preventDefault();
5113
- onToggleCollapsed(file, isCollapsed, reviewIdentity.key);
5114
- }
5115
- },
5116
- role: "button",
5117
- tabIndex: 0,
5118
- title: isCollapsed ? "Expand" : "Collapse",
5593
+ children: [/* @__PURE__ */ jsxs("div", {
5594
+ "aria-expanded": !isCollapsed,
5595
+ "aria-label": isCollapsed ? "Expand file" : "Collapse file",
5596
+ className: "codiff-header-toggle",
5597
+ onClick: () => onToggleCollapsed(file, isCollapsed, reviewIdentity.key),
5598
+ onKeyDown: (event) => {
5599
+ if (event.key === "Enter" || event.key === " ") {
5600
+ event.preventDefault();
5601
+ onToggleCollapsed(file, isCollapsed, reviewIdentity.key);
5602
+ }
5603
+ },
5604
+ role: "button",
5605
+ tabIndex: 0,
5606
+ title: isCollapsed ? "Expand" : "Collapse",
5607
+ children: [/* @__PURE__ */ jsx("span", {
5608
+ className: "codiff-chevron-box",
5609
+ children: /* @__PURE__ */ jsx(CaretDownIcon, {
5610
+ "aria-hidden": true,
5611
+ className: isCollapsed ? "codiff-chevron collapsed" : "codiff-chevron",
5612
+ size: 16,
5613
+ weight: "bold"
5614
+ })
5615
+ }), /* @__PURE__ */ jsxs("span", {
5616
+ className: "codiff-file-heading",
5119
5617
  children: [
5120
- /* @__PURE__ */ jsx("span", {
5121
- className: "codiff-chevron-box",
5122
- children: /* @__PURE__ */ jsx(CaretDownIcon, {
5123
- "aria-hidden": true,
5124
- className: isCollapsed ? "codiff-chevron collapsed" : "codiff-chevron",
5125
- size: 16,
5126
- weight: "bold"
5127
- })
5128
- }),
5129
5618
  /* @__PURE__ */ jsxs("span", {
5130
- className: "codiff-file-heading",
5619
+ className: "codiff-file-path-row",
5131
5620
  children: [
5132
5621
  /* @__PURE__ */ jsxs("span", {
5133
- className: "codiff-file-path-row",
5134
- children: [/* @__PURE__ */ jsx("span", {
5135
- className: "codiff-file-path",
5136
- children: file.path
5137
- }), /* @__PURE__ */ jsx(CopyFilePathButton, { path: file.path })]
5622
+ className: "codiff-file-path",
5623
+ title: file.path,
5624
+ children: [directoryEnd > 0 ? /* @__PURE__ */ jsx("span", {
5625
+ className: "codiff-file-directory",
5626
+ children: file.path.slice(0, directoryEnd)
5627
+ }) : null, /* @__PURE__ */ jsx("span", {
5628
+ className: "codiff-file-name",
5629
+ children: file.path.slice(directoryEnd)
5630
+ })]
5138
5631
  }),
5139
- file.oldPath ? /* @__PURE__ */ jsx("span", {
5140
- className: "codiff-file-old-path",
5141
- children: file.oldPath
5142
- }) : null,
5143
- walkthroughNote ? /* @__PURE__ */ jsx("span", {
5144
- className: "codiff-file-note",
5145
- children: walkthroughNote.reason
5632
+ /* @__PURE__ */ jsx(CopyFilePathButton, { path: file.path }),
5633
+ !readOnly && onOpenFile ? /* @__PURE__ */ jsx("button", {
5634
+ "aria-label": `Open ${file.path} in editor`,
5635
+ className: "codiff-open-file-button",
5636
+ disabled: !canOpenFile,
5637
+ onClick: (event) => {
5638
+ event.stopPropagation();
5639
+ onOpenFile(file);
5640
+ },
5641
+ onKeyDown: (event) => {
5642
+ if (event.key === "Enter" || event.key === " ") event.stopPropagation();
5643
+ },
5644
+ title: canOpenFile ? "Open file in editor" : "Deleted files cannot be opened",
5645
+ type: "button",
5646
+ children: /* @__PURE__ */ jsx(ArrowSquareOutIcon, {
5647
+ "aria-hidden": true,
5648
+ size: 16,
5649
+ weight: "bold"
5650
+ })
5146
5651
  }) : null
5147
5652
  ]
5148
5653
  }),
5149
- sectionCount > 1 ? /* @__PURE__ */ jsx("span", {
5150
- className: `codiff-section-badge ${section.kind}`,
5151
- children: sectionLabel[section.kind]
5654
+ file.oldPath ? /* @__PURE__ */ jsx("span", {
5655
+ className: "codiff-file-old-path",
5656
+ children: file.oldPath
5152
5657
  }) : null,
5153
- isGeneratedWalkthroughFile(file) ? /* @__PURE__ */ jsx("span", {
5154
- className: "codiff-generated-badge",
5155
- title: "Generated file",
5156
- children: "Generated"
5658
+ walkthroughNote ? /* @__PURE__ */ jsx("span", {
5659
+ className: "codiff-file-note",
5660
+ children: walkthroughNote.reason
5157
5661
  }) : null
5158
5662
  ]
5159
- }),
5160
- /* @__PURE__ */ jsx(DiffLineCountBadge, { lineCount }),
5161
- /* @__PURE__ */ jsx("div", {
5162
- className: `codiff-status-badge ${file.status}`,
5163
- children: statusLabel[file.status]
5164
- }),
5165
- canCreateFileComment ? /* @__PURE__ */ jsxs(Button, {
5166
- className: "codiff-file-comment-button",
5167
- onClick: onCreateFileComment,
5168
- title: "Comment on file",
5169
- type: "button",
5170
- children: [/* @__PURE__ */ jsx(ChatCircleIcon, {
5171
- "aria-hidden": true,
5172
- className: "codiff-file-comment-icon",
5173
- size: 14,
5174
- weight: "bold"
5175
- }), "Comment"]
5176
- }) : null,
5177
- canRenderMarkdown ? /* @__PURE__ */ jsx(Button, {
5178
- "aria-pressed": isMarkdownPreview,
5179
- className: `codiff-markdown-button${isMarkdownPreview ? " active" : ""}`,
5180
- onClick: () => onToggleMarkdownPreview(file, section),
5181
- title: isMarkdownPreview ? "View as Diff" : "View as Markdown",
5182
- type: "button",
5183
- children: isMarkdownPreview ? "View as Diff" : "View as Markdown"
5184
- }) : null,
5185
- canLoadSection && !readOnly ? /* @__PURE__ */ jsx("button", {
5186
- className: "codiff-load-button",
5187
- disabled: isSectionLoading,
5188
- onClick: () => onLoadSection(file, section),
5189
- title: isSectionLoading ? "Loading file contents" : "Load file contents",
5190
- type: "button",
5191
- children: isSectionLoading ? "Loading..." : "Load"
5192
- }) : null,
5193
- !readOnly && onOpenFile ? /* @__PURE__ */ jsx(Button, {
5194
- disabled: !canOpenFile,
5195
- onClick: () => onOpenFile(file),
5196
- title: canOpenFile ? "Open file in editor" : "Deleted files cannot be opened",
5197
- type: "button",
5198
- children: "Open"
5199
- }) : null,
5200
- !readOnly || allowViewedToggle ? /* @__PURE__ */ jsxs("button", {
5201
- "aria-pressed": isViewed,
5202
- className: `codiff-viewed-button${isViewed ? " active" : ""}`,
5203
- onClick: () => onToggleViewed(file, isViewed, reviewIdentity),
5204
- type: "button",
5205
- children: [/* @__PURE__ */ jsx("span", {
5206
- "aria-hidden": true,
5207
- className: "codiff-viewed-checkbox",
5208
- children: isViewed ? /* @__PURE__ */ jsx(CheckIcon, {
5209
- className: "codiff-viewed-check",
5210
- size: 10,
5663
+ })]
5664
+ }), /* @__PURE__ */ jsxs("div", {
5665
+ className: "codiff-file-details",
5666
+ children: [
5667
+ /* @__PURE__ */ jsx(DiffLineCountBadge, { lineCount }),
5668
+ sectionCount > 1 ? /* @__PURE__ */ jsx("span", {
5669
+ className: `codiff-section-badge ${section.kind}`,
5670
+ children: sectionLabel[section.kind]
5671
+ }) : null,
5672
+ isGeneratedWalkthroughFile(file) ? /* @__PURE__ */ jsx("span", {
5673
+ className: "codiff-generated-badge",
5674
+ title: "Generated file",
5675
+ children: "Generated"
5676
+ }) : null,
5677
+ canCreateFileComment ? /* @__PURE__ */ jsxs(Button, {
5678
+ className: "codiff-file-comment-button",
5679
+ onClick: onCreateFileComment,
5680
+ title: "Comment on file",
5681
+ type: "button",
5682
+ children: [/* @__PURE__ */ jsx(ChatCircleIcon, {
5683
+ "aria-hidden": true,
5684
+ className: "codiff-file-comment-icon",
5685
+ size: 14,
5211
5686
  weight: "bold"
5212
- }) : null
5213
- }), "Viewed"]
5214
- }) : null
5215
- ]
5687
+ }), "Comment"]
5688
+ }) : null,
5689
+ canRenderMarkdown ? /* @__PURE__ */ jsx(Button, {
5690
+ "aria-pressed": isMarkdownPreview,
5691
+ className: `codiff-markdown-button${isMarkdownPreview ? " active" : ""}`,
5692
+ disabled: isEditing,
5693
+ onClick: () => onToggleMarkdownPreview(file, section),
5694
+ title: isMarkdownPreview ? "View as Diff" : "View as Markdown",
5695
+ type: "button",
5696
+ children: isMarkdownPreview ? "View as Diff" : "View as Markdown"
5697
+ }) : null,
5698
+ editActions,
5699
+ canLoadSection && !readOnly ? /* @__PURE__ */ jsx("button", {
5700
+ className: "codiff-load-button",
5701
+ disabled: isSectionLoading,
5702
+ onClick: () => onLoadSection(file, section),
5703
+ title: isSectionLoading ? "Loading file contents" : "Load file contents",
5704
+ type: "button",
5705
+ children: isSectionLoading ? "Loading..." : "Load"
5706
+ }) : null,
5707
+ !readOnly || allowViewedToggle ? /* @__PURE__ */ jsxs("button", {
5708
+ "aria-pressed": isViewed,
5709
+ className: `codiff-viewed-button${isViewed ? " active" : ""}`,
5710
+ onClick: () => onToggleViewed(file, isViewed, reviewIdentity),
5711
+ type: "button",
5712
+ children: [/* @__PURE__ */ jsx("span", {
5713
+ "aria-hidden": true,
5714
+ className: "codiff-viewed-checkbox",
5715
+ children: isViewed ? /* @__PURE__ */ jsx(CheckIcon, {
5716
+ className: "codiff-viewed-check",
5717
+ size: 10,
5718
+ weight: "bold"
5719
+ }) : null
5720
+ }), "Viewed"]
5721
+ }) : null
5722
+ ]
5723
+ })]
5216
5724
  });
5217
5725
  }
5218
5726
  const getReviewAuthorDisplayName = (author) => author.name || author.login || "Git user";
@@ -6701,8 +7209,27 @@ const getHunkSelectionRange = (hunk) => {
6701
7209
  } : null;
6702
7210
  };
6703
7211
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
6704
- function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedReviewKeys = emptyExpandedReviewKeys, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds = emptyMarkdownPreviewSectionIds, isReadOnly = false, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread = noopResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription = true, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme = "system", viewed, walkthroughNotes, wordWrap }) {
7212
+ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedToggle = false, blocks, bottomInset = codeViewLayout.paddingBottom, codeEditingStore, codeQualityFindings = [], collapsed, comments, commitMetadata, diffLineHeight = 20, diffStyle, disableWorkerPool = false, expandedReviewKeys = emptyExpandedReviewKeys, files, focusCommentId, focusCommentRequest, forceExpandedPaths, gitIdentity, hunkNavigation, initialMarkdownPreviewSectionIds = emptyMarkdownPreviewSectionIds, isReadOnly = false, itemVersionByKey, keymap, loadingSectionIds, onActiveBlockChange, onAskCodex, onCommentDraftChange, onCreateComment, onDeleteComment, onFindDefinitions, onLoadImageContent, onLoadSection, onLoadSectionContents, onOpenDefinition, onOpenFile, onRefreshMarkdown, onResolveThread = noopResolveThread, onSaveCommentEdit, onSelectPathFromScroll, onSubmitComment, onToggleCollapsed, onToggleViewed, onUpdateComment, onUpdateSourceDescription, onUpdateSourceTitle, onUploadSourceDescriptionAsset, reviewIdentityByPath, scrollTarget, searchQuery, selectedPath, showSourceDescription = true, showWhitespace, source, sourceDescriptionActions, sourceDescriptionFooter, supportsReviewCommentActions, theme = "system", viewed, walkthroughNotes, wordWrap }) {
6705
7213
  const codeViewRef = useRef(null);
7214
+ const [codeViewElement, setCodeViewElement] = useState(null);
7215
+ const [compactHeaders, setCompactHeaders] = useState(false);
7216
+ useLayoutEffect(() => {
7217
+ if (!codeViewElement) return;
7218
+ const measure = () => {
7219
+ const width = codeViewElement.getBoundingClientRect().width;
7220
+ if (width > 0) setCompactHeaders(width <= 760);
7221
+ };
7222
+ measure();
7223
+ const observer = new ResizeObserver(measure);
7224
+ observer.observe(codeViewElement);
7225
+ return () => observer.disconnect();
7226
+ }, [codeViewElement]);
7227
+ const headerHeight = compactHeaders ? 90 : codeViewItemMetrics.diffHeaderHeight;
7228
+ const itemMetrics = useMemo(() => ({
7229
+ ...codeViewItemMetrics,
7230
+ diffHeaderHeight: headerHeight
7231
+ }), [headerHeight]);
7232
+ const codeViewStyle = { "--codiff-file-header-height": `${headerHeight}px` };
6706
7233
  const getPlaceholderFile = useCodeViewPlaceholderFile();
6707
7234
  const getAnnotations = useCodeViewAnnotations();
6708
7235
  const [markdownPreviewHeights] = useState(() => /* @__PURE__ */ new Map());
@@ -7013,6 +7540,91 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7013
7540
  shouldShowCommitMessage,
7014
7541
  sourceDescriptionItemId
7015
7542
  ]);
7543
+ const highlightedCodeItems = codeViewItems.filter((item) => itemMetadata.get(item.id)?.isSelected);
7544
+ const selectedCodeItemId = highlightedCodeItems.find((item) => item.id === selectedLines?.id && itemMetadata.get(item.id)?.section.kind === "unstaged")?.id ?? highlightedCodeItems.find((item) => itemMetadata.get(item.id)?.section.kind === "unstaged")?.id ?? highlightedCodeItems[0]?.id;
7545
+ const codeEditing = useCodeEditing({
7546
+ codeViewRef,
7547
+ items: codeViewItems,
7548
+ keymap,
7549
+ onLoadSectionContents,
7550
+ onRefresh: onRefreshMarkdown,
7551
+ selectedItemId: selectedCodeItemId,
7552
+ sourceKey,
7553
+ store: codeEditingStore
7554
+ });
7555
+ const canEditCodeItem = useCallback((item, meta) => meta != null && item.type === "diff" && !isReadOnly && !meta.section.binary && (meta.section.loadState == null || meta.section.loadState === "ready" || meta.section.loadState === "deferred") && !meta.isMarkdownPreview && meta.section.kind === "unstaged" && isEditableWorkingTreeSection(source.type, meta.file, meta.section) && onLoadSectionContents != null && window.codiff?.getRepositoryFile != null, [
7556
+ isReadOnly,
7557
+ onLoadSectionContents,
7558
+ source.type
7559
+ ]);
7560
+ const startCodeEdit = useCallback((item, meta) => {
7561
+ if (meta.isCollapsed) onToggleCollapsed(meta.file, true, meta.reviewIdentity.key);
7562
+ codeEditing.startEdit(item, meta);
7563
+ }, [codeEditing, onToggleCollapsed]);
7564
+ const editAfterExpandRef = useRef(null);
7565
+ useEffect(() => {
7566
+ const pending = editAfterExpandRef.current;
7567
+ if (!pending) return;
7568
+ if (pending.sourceKey !== sourceKey || !highlightedCodeItems.some((item) => itemMetadata.get(item.id)?.file.path === pending.path)) {
7569
+ editAfterExpandRef.current = null;
7570
+ return;
7571
+ }
7572
+ const item = highlightedCodeItems.find((candidate) => {
7573
+ const meta = itemMetadata.get(candidate.id);
7574
+ return meta?.file.path === pending.path && canEditCodeItem(candidate, meta);
7575
+ });
7576
+ const meta = item && itemMetadata.get(item.id);
7577
+ if (item && meta) {
7578
+ editAfterExpandRef.current = null;
7579
+ startCodeEdit(item, meta);
7580
+ }
7581
+ }, [
7582
+ canEditCodeItem,
7583
+ highlightedCodeItems,
7584
+ itemMetadata,
7585
+ sourceKey,
7586
+ startCodeEdit
7587
+ ]);
7588
+ useEffect(() => {
7589
+ const handleKeyDown = (event) => {
7590
+ if (event.defaultPrevented || event.repeat || event.isComposing || isNativeInputTarget(event.target) || !matchesShortcut(event, keymap, "editFile")) return;
7591
+ const item = codeViewItems.find((candidate) => candidate.id === selectedCodeItemId);
7592
+ const meta = item && itemMetadata.get(item.id);
7593
+ if (!item || !meta || !codeEditing.canStartEdit(item.id, meta.file.path)) return;
7594
+ if (!canEditCodeItem(item, meta)) {
7595
+ if (!meta.isCollapsed || !getVisibleDiffSections(meta.file, showWhitespace).some(({ fileDiff, section }) => canEditCodeItem({
7596
+ fileDiff,
7597
+ id: item.id,
7598
+ type: "diff"
7599
+ }, {
7600
+ ...meta,
7601
+ section
7602
+ }))) return;
7603
+ event.preventDefault();
7604
+ editAfterExpandRef.current = {
7605
+ path: meta.file.path,
7606
+ sourceKey
7607
+ };
7608
+ onToggleCollapsed(meta.file, true, meta.reviewIdentity.key);
7609
+ return;
7610
+ }
7611
+ event.preventDefault();
7612
+ startCodeEdit(item, meta);
7613
+ };
7614
+ window.addEventListener("keydown", handleKeyDown);
7615
+ return () => window.removeEventListener("keydown", handleKeyDown);
7616
+ }, [
7617
+ canEditCodeItem,
7618
+ codeEditing,
7619
+ codeViewItems,
7620
+ itemMetadata,
7621
+ keymap,
7622
+ onToggleCollapsed,
7623
+ selectedCodeItemId,
7624
+ showWhitespace,
7625
+ sourceKey,
7626
+ startCodeEdit
7627
+ ]);
7016
7628
  const clearCommentLineHighlight = useCallback(() => {
7017
7629
  codeViewRef.current?.clearSelectedLines();
7018
7630
  navigatedSelectionRef.current = null;
@@ -7195,7 +7807,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7195
7807
  expandUnchanged: false,
7196
7808
  expansionLineCount: 100,
7197
7809
  hunkSeparators: "line-info-basic",
7198
- itemMetrics: codeViewItemMetrics,
7810
+ itemMetrics,
7199
7811
  layout: {
7200
7812
  ...codeViewLayout,
7201
7813
  paddingBottom: bottomInset
@@ -7203,11 +7815,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7203
7815
  lineHoverHighlight: "both",
7204
7816
  loadDiffFiles,
7205
7817
  onGutterUtilityClick: (range, context) => {
7206
- if (isReadOnly) return;
7818
+ if (isReadOnly || context.item.edit) return;
7207
7819
  ignoreNextLineSelectionEndRef.current = context.item.type === "diff";
7208
7820
  createCommentForRange(range, context);
7209
7821
  },
7210
7822
  onLineClick: (line, context) => {
7823
+ if (context.item.edit) return;
7211
7824
  const lineElement = "lineElement" in line ? line.lineElement : null;
7212
7825
  if (isPrimaryModifier(line.event) && onFindDefinitions) {
7213
7826
  line.event.preventDefault();
@@ -7272,7 +7885,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7272
7885
  });
7273
7886
  },
7274
7887
  onLineSelectionEnd: (range, context) => {
7275
- if (isReadOnly) return;
7888
+ if (isReadOnly || context.item.edit) return;
7276
7889
  if (ignoreNextLineSelectionEndRef.current) {
7277
7890
  ignoreNextLineSelectionEndRef.current = false;
7278
7891
  return;
@@ -7287,10 +7900,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7287
7900
  node.classList.toggle("codiff-selected-item", metadata?.isSelected === true || selectedHeaderItemIds.has(context.item.id));
7288
7901
  node.classList.toggle("codiff-markdown-preview-item", metadata?.isMarkdownPreview === true);
7289
7902
  node.classList.toggle("codiff-image-preview-item", context.item.type === "file" && Boolean(metadata && canRenderImagePreview(metadata.file.path, metadata.section)));
7290
- node.classList.toggle("codiff-loadable-summary-item", metadata != null && shouldLoadDiffSectionContents(metadata.section));
7903
+ node.classList.toggle("codiff-loadable-summary-item", !context.item.edit && metadata != null && shouldLoadDiffSectionContents(metadata.section));
7291
7904
  node.classList.toggle("codiff-loading-summary-item", Boolean(metadata && loadingSectionIds.has(metadata.section.id)));
7292
7905
  if (definitionModifierActiveRef.current) window.requestAnimationFrame(() => {
7293
- if (definitionModifierActiveRef.current && node.isConnected) applyIdentifierNavigationState([{ element: node }], true);
7906
+ if (definitionModifierActiveRef.current && node.isConnected && !codeViewRef.current?.getItem(context.item.id)?.edit) applyIdentifierNavigationState([{ element: node }], true);
7294
7907
  });
7295
7908
  },
7296
7909
  overflow: wordWrap ? "wrap" : "scroll",
@@ -7301,7 +7914,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7301
7914
  },
7302
7915
  themeType: theme,
7303
7916
  tokenizeMaxLength: 1e5,
7304
- unsafeCSS: codeViewUnsafeCSS
7917
+ unsafeCSS: codeViewUnsafeCSS,
7918
+ useTokenTransformer: workerHighlighterOptions.useTokenTransformer
7305
7919
  }), [
7306
7920
  bottomInset,
7307
7921
  cancelPendingEmptyCommentDeletes,
@@ -7309,6 +7923,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7309
7923
  diffStyle,
7310
7924
  isReadOnly,
7311
7925
  itemMetadata,
7926
+ itemMetrics,
7312
7927
  loadDiffFiles,
7313
7928
  loadingSectionIds,
7314
7929
  onCreateComment,
@@ -7478,7 +8093,6 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7478
8093
  const viewer = handle?.getInstance();
7479
8094
  if (!handle || !viewer) return;
7480
8095
  handledHunkNavRef.current = hunkNavigation.request;
7481
- const headerHeight = codeViewItemMetrics.diffHeaderHeight;
7482
8096
  const anchors = [];
7483
8097
  const seen = /* @__PURE__ */ new Set();
7484
8098
  const lineScrollTarget = (id, lineNumber, side) => ({
@@ -7597,6 +8211,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7597
8211
  clearCommentLineHighlight,
7598
8212
  diffLineHeight,
7599
8213
  diffStyle,
8214
+ headerHeight,
7600
8215
  hunkNavigation,
7601
8216
  itemMetadata,
7602
8217
  items
@@ -7625,7 +8240,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7625
8240
  if (highlightFrameRef.current != null) window.cancelAnimationFrame(highlightFrameRef.current);
7626
8241
  highlightFrameRef.current = window.requestAnimationFrame(() => {
7627
8242
  highlightFrameRef.current = null;
7628
- applySearchHighlights(viewer.getRenderedItems(), searchQuery, resolvedActiveSearchMatch);
8243
+ applySearchHighlights(viewer.getRenderedItems().filter(({ item }) => !item.edit), searchQuery, resolvedActiveSearchMatch);
7629
8244
  });
7630
8245
  }, [resolvedActiveSearchMatch, searchQuery]);
7631
8246
  const updateRenderedIdentifierNavigation = useCallback((active, viewer) => {
@@ -7636,12 +8251,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7636
8251
  definitionHighlightFrameRef.current = null;
7637
8252
  }
7638
8253
  if (!active) {
7639
- applyIdentifierNavigationState(nextViewer.getRenderedItems(), false);
8254
+ applyIdentifierNavigationState(nextViewer.getRenderedItems().filter(({ item }) => !item.edit), false);
7640
8255
  return;
7641
8256
  }
7642
8257
  definitionHighlightFrameRef.current = window.requestAnimationFrame(() => {
7643
8258
  definitionHighlightFrameRef.current = null;
7644
- applyIdentifierNavigationState(nextViewer.getRenderedItems(), true);
8259
+ applyIdentifierNavigationState(nextViewer.getRenderedItems().filter(({ item }) => !item.edit), true);
7645
8260
  });
7646
8261
  }, []);
7647
8262
  const scheduleStickyHeaderStateUpdate = useCallback((viewer) => {
@@ -7783,10 +8398,47 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7783
8398
  toggleSourceDescriptionCollapsed
7784
8399
  ]);
7785
8400
  const renderCustomHeader = useCallback((item) => {
7786
- const meta = itemMetadata.get(item.id);
7787
- return meta ? /* @__PURE__ */ jsx(CodeViewHeader, {
8401
+ const editSession = codeEditing.sessions.get(item.id);
8402
+ const isEditing = editSession != null;
8403
+ const meta = editSession?.metadata ?? itemMetadata.get(item.id);
8404
+ const isBusy = codeEditing.isBusy(item.id);
8405
+ const isAutosaving = editSession?.saving && !isBusy;
8406
+ const canEdit = canEditCodeItem(item, meta);
8407
+ const editError = codeEditing.getError(item.id);
8408
+ return meta ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CodeViewHeader, {
7788
8409
  allowViewedToggle,
7789
8410
  canCreateFileComment: canCreateFileComments,
8411
+ editActions: isEditing ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
8412
+ "aria-label": `${editSession?.hasChanges ? "Revert edits to" : "Cancel editing"} ${meta.file.path}`,
8413
+ className: "codiff-edit-revert-button",
8414
+ "data-autosaving": isAutosaving ? "" : void 0,
8415
+ disabled: isBusy || editSession.saving,
8416
+ onClick: () => void codeEditing.revertEdit(item.id),
8417
+ title: editSession?.hasChanges ? "Restore the file to when editing started" : "Cancel editing",
8418
+ type: "button",
8419
+ variant: editSession?.hasChanges ? "destructive" : "default",
8420
+ children: editSession?.hasChanges ? "Revert" : "Cancel"
8421
+ }), /* @__PURE__ */ jsxs(Button, {
8422
+ "aria-label": `Done editing ${meta.file.path}`,
8423
+ "data-autosaving": isAutosaving ? "" : void 0,
8424
+ disabled: isBusy || editSession.saving,
8425
+ onClick: () => void codeEditing.doneEdit(item.id),
8426
+ title: "Finish editing; changes are saved automatically",
8427
+ type: "button",
8428
+ children: [/* @__PURE__ */ jsx(CheckIcon, {
8429
+ "aria-hidden": true,
8430
+ size: 14,
8431
+ weight: "bold"
8432
+ }), "Done"]
8433
+ })] }) : canEdit ? /* @__PURE__ */ jsx(CodeEditButton, {
8434
+ disabled: !codeEditing.canStartEdit(item.id, meta.file.path),
8435
+ isBusy,
8436
+ item,
8437
+ meta,
8438
+ onEdit: () => startCodeEdit(item, meta),
8439
+ onPrepare: codeEditing.prepareEdit
8440
+ }) : null,
8441
+ isEditing,
7790
8442
  isSectionLoading: loadingSectionIds.has(meta.section.id),
7791
8443
  meta,
7792
8444
  onCreateFileComment: () => createFileComment(meta, item.id),
@@ -7796,11 +8448,17 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7796
8448
  onToggleMarkdownPreview: toggleMarkdownPreview,
7797
8449
  onToggleViewed,
7798
8450
  readOnly: isReadOnly
7799
- }) : null;
8451
+ }), editError ? /* @__PURE__ */ jsx("div", {
8452
+ className: "codiff-code-edit-error",
8453
+ role: "alert",
8454
+ children: editError
8455
+ }) : null] }) : null;
7800
8456
  }, [
7801
8457
  allowViewedToggle,
7802
8458
  canCreateFileComments,
8459
+ canEditCodeItem,
7803
8460
  createFileComment,
8461
+ codeEditing,
7804
8462
  itemMetadata,
7805
8463
  isReadOnly,
7806
8464
  loadingSectionIds,
@@ -7808,6 +8466,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7808
8466
  onOpenFile,
7809
8467
  onToggleCollapsed,
7810
8468
  onToggleViewed,
8469
+ startCodeEdit,
7811
8470
  toggleMarkdownPreview
7812
8471
  ]);
7813
8472
  const renderAnnotation = useCallback((annotation, item) => {
@@ -7909,10 +8568,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7909
8568
  scheduleStickyHeaderStateUpdate,
7910
8569
  updateRenderedIdentifierNavigation
7911
8570
  ]);
7912
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7913
- className: "code-view",
8571
+ const codeView = /* @__PURE__ */ jsx(CodeView, {
8572
+ className: `code-view${compactHeaders ? " compact-file-headers" : ""}`,
8573
+ containerRef: setCodeViewElement,
7914
8574
  disableWorkerPool,
7915
- items: codeViewItems,
8575
+ editorOptions: codeEditing.editorOptions,
8576
+ items: codeEditing.items,
8577
+ onItemEditChange: codeEditing.onItemEditChange,
8578
+ onItemEditComplete: codeEditing.onItemEditComplete,
7916
8579
  onScroll: handleScroll,
7917
8580
  onSelectedLinesChange: setCodeViewSelectedLines,
7918
8581
  options: codeViewOptions,
@@ -7920,14 +8583,20 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7920
8583
  renderAnnotation,
7921
8584
  renderCodeViewHeader: sourceDescriptionItemId ? renderCodeViewHeader : void 0,
7922
8585
  renderCustomHeader,
7923
- selectedLines: isReadOnly ? null : selectedLines
7924
- }) : /* @__PURE__ */ jsx(WorkerPoolContextProvider, {
8586
+ selectedLines: isReadOnly ? null : selectedLines,
8587
+ style: codeViewStyle
8588
+ });
8589
+ const renderedCodeView = disableWorkerPool ? codeView : /* @__PURE__ */ jsx(WorkerPoolContextProvider, {
7925
8590
  highlighterOptions: workerHighlighterOptions,
7926
8591
  poolOptions: workerPoolOptions,
7927
8592
  children: /* @__PURE__ */ jsx(CodeView, {
7928
- className: "code-view",
8593
+ className: `code-view${compactHeaders ? " compact-file-headers" : ""}`,
8594
+ containerRef: setCodeViewElement,
7929
8595
  disableWorkerPool: false,
7930
- items: codeViewItems,
8596
+ editorOptions: codeEditing.editorOptions,
8597
+ items: codeEditing.items,
8598
+ onItemEditChange: codeEditing.onItemEditChange,
8599
+ onItemEditComplete: codeEditing.onItemEditComplete,
7931
8600
  onScroll: handleScroll,
7932
8601
  onSelectedLinesChange: setCodeViewSelectedLines,
7933
8602
  options: codeViewOptions,
@@ -7935,8 +8604,13 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7935
8604
  renderAnnotation,
7936
8605
  renderCodeViewHeader: sourceDescriptionItemId ? renderCodeViewHeader : void 0,
7937
8606
  renderCustomHeader,
7938
- selectedLines: isReadOnly ? null : selectedLines
8607
+ selectedLines: isReadOnly ? null : selectedLines,
8608
+ style: codeViewStyle
7939
8609
  })
8610
+ });
8611
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(EditProvider, {
8612
+ createEditor: createCodeEditor,
8613
+ children: renderedCodeView
7940
8614
  }), definitionLookup?.sourceKey === sourceKey && onOpenDefinition ? /* @__PURE__ */ jsx(DefinitionPopover, {
7941
8615
  anchor: definitionLookup.anchor,
7942
8616
  getDestination: (candidate) => getDefinitionDiffTarget(candidate) ? "diff" : candidate.canOpenInEditor ? "editor" : "unavailable",
@@ -9653,54 +10327,6 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
9653
10327
  };
9654
10328
  }
9655
10329
  //#endregion
9656
- //#region config/defaults.ts
9657
- const defaultConfigTemplate = {
9658
- settings: {
9659
- "agentBackend": "codex",
9660
- "checkForUpdates": true,
9661
- "claudeModel": "claude-sonnet-4-6",
9662
- "codeFontFamily": "",
9663
- "codeFontSize": 13,
9664
- "copyCommentsOnClose": false,
9665
- "diffStyle": "split",
9666
- "editorCommand": "",
9667
- "lastRepositoryPath": "",
9668
- "openAIModel": "gpt-5.6-terra",
9669
- "opencodeModel": "opencode-default",
9670
- "piModel": "pi-default",
9671
- "reviewCommentsPrefix": "# Address these Review Comments",
9672
- "sidebarPosition": "left",
9673
- "showOutdated": false,
9674
- "showWhitespace": false,
9675
- "theme": "system",
9676
- "walkthroughPrompt": "",
9677
- "wordWrap": false
9678
- },
9679
- keymap: {
9680
- "askAgent": "Mod+Alt+Enter",
9681
- "closeSearch": "Escape",
9682
- "commandBar": "Mod+k",
9683
- "diffSearch": "Mod+f",
9684
- "discardComment": "Escape",
9685
- "fileFilter": "Mod+p",
9686
- "nextHunk": ["Ctrl+ArrowDown", "j"],
9687
- "nextSearchMatch": "Enter",
9688
- "openFile": "Mod+Shift+o",
9689
- "prevHunk": ["Ctrl+ArrowUp", "k"],
9690
- "prevSearchMatch": "Shift+Enter",
9691
- "shortcutsHelp": "Shift+?",
9692
- "submitComment": "Mod+Enter",
9693
- "toggleSidebar": "Mod+Shift+b",
9694
- "toggleWordWrap": "Alt+z"
9695
- }
9696
- };
9697
- const createDefaultConfig = () => ({
9698
- keymap: { ...defaultConfigTemplate.keymap },
9699
- settings: { ...defaultConfigTemplate.settings }
9700
- });
9701
- Object.freeze({ ...defaultConfigTemplate.keymap });
9702
- Object.freeze({ ...defaultConfigTemplate.settings });
9703
- //#endregion
9704
10330
  //#region lib/app-constants.ts
9705
10331
  const AGENT_LABELS = {
9706
10332
  claude: "Claude Code",