@nkzw/codiff-core 1.13.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";
@@ -1505,7 +1506,7 @@ const parseSectionDiffWithOptions = (file, section, showWhitespace) => {
1505
1506
  }
1506
1507
  else if (section.patch.trim().length === 0) fileDiff = createEmptyFileDiff(file, section);
1507
1508
  else {
1508
- const parsedFileDiff = parsePatchFiles(section.patch)[0]?.files[0];
1509
+ const parsedFileDiff = parsePatchFiles(section.patch, cacheKey)[0]?.files[0];
1509
1510
  if (parsedFileDiff) {
1510
1511
  const loaded = parsedFileDiff.isPartial ? getLoadedSectionContents(file, section) : void 0;
1511
1512
  let hydrated = null;
@@ -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 {
@@ -3887,11 +3880,10 @@ const wrapIdentifierNavigationText = (root) => {
3887
3880
  };
3888
3881
  const applyIdentifierNavigationState = (renderedItems, active) => {
3889
3882
  for (const { element } of renderedItems) {
3890
- element.toggleAttribute(definitionModifierAttribute, active);
3891
3883
  const root = element.shadowRoot;
3892
- if (!root) continue;
3893
- clearIdentifierNavigationWrappers(root);
3894
- if (active) wrapIdentifierNavigationText(root);
3884
+ const enabled = active && !hasActiveTextSelection(root);
3885
+ element.toggleAttribute(definitionModifierAttribute, enabled);
3886
+ if (root && enabled) wrapIdentifierNavigationText(root);
3895
3887
  }
3896
3888
  };
3897
3889
  const getIdentifierAtOffset = (text, offset) => {
@@ -3935,7 +3927,7 @@ const getItemVersion = (value) => {
3935
3927
  //#region lib/keyboard.ts
3936
3928
  const isNativeInputTarget = (target) => {
3937
3929
  const candidate = target;
3938
- 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);
3939
3931
  };
3940
3932
  const isMacPlatform = (platform = navigator.platform) => platform.toLowerCase().includes("mac");
3941
3933
  const isPrimaryModifier = (event, platform = navigator.platform) => isMacPlatform(platform) ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
@@ -4039,6 +4031,7 @@ const highlightTextContainer = (container, normalizedQuery, activeMatch) => {
4039
4031
  const applySearchHighlights = (renderedItems, query, activeMatch) => {
4040
4032
  const normalizedQuery = query.trim().toLowerCase();
4041
4033
  for (const { element, id } of renderedItems) for (const root of getSearchableRoots(element)) {
4034
+ if (normalizedQuery) clearIdentifierNavigationWrappers(root);
4042
4035
  clearSearchHighlights(root);
4043
4036
  if (!normalizedQuery) continue;
4044
4037
  const matchForItem = activeMatch && activeMatch.itemId === id ? activeMatch : null;
@@ -4052,6 +4045,516 @@ 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
4519
+ //#region app/hooks/useCodeViewAnnotations.ts
4520
+ function useCodeViewAnnotations() {
4521
+ const [getAnnotations] = useState(() => {
4522
+ const cache = /* @__PURE__ */ new Map();
4523
+ return (id, key, annotations) => {
4524
+ const previous = cache.get(id);
4525
+ if (previous?.key === key) return previous.annotations;
4526
+ cache.set(id, {
4527
+ annotations,
4528
+ key
4529
+ });
4530
+ return annotations;
4531
+ };
4532
+ });
4533
+ return getAnnotations;
4534
+ }
4535
+ //#endregion
4536
+ //#region app/hooks/useCodeViewPlaceholderFile.ts
4537
+ function useCodeViewPlaceholderFile() {
4538
+ const [getPlaceholderFile] = useState(() => {
4539
+ const files = /* @__PURE__ */ new Map();
4540
+ return (name, contents = " ") => {
4541
+ const cacheKey = `placeholder:${contents.length}:${name}`;
4542
+ let file = files.get(cacheKey);
4543
+ if (!file) {
4544
+ file = {
4545
+ cacheKey,
4546
+ contents,
4547
+ lang: "text",
4548
+ name
4549
+ };
4550
+ files.set(cacheKey, file);
4551
+ }
4552
+ return file;
4553
+ };
4554
+ });
4555
+ return getPlaceholderFile;
4556
+ }
4557
+ //#endregion
4055
4558
  //#region app/components/DefinitionPopover.tsx
4056
4559
  function DefinitionPopover({ anchor, getDestination, identifier, onClose, onOpen, result }) {
4057
4560
  const popoverRef = useRef(null);
@@ -4445,14 +4948,17 @@ const resolveWalkthroughHunkFile = (hunk, files) => {
4445
4948
  };
4446
4949
  };
4447
4950
  const walkthroughHunkRunKey = (item, hunks) => `${item.id}:${hunks.map((hunk) => hunk.id).join(",")}`;
4448
- /** 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. */
4449
4952
  const resolveWalkthroughHunkRuns = (item, files) => {
4450
4953
  const runs = [];
4451
4954
  for (const hunk of item.hunks) {
4452
4955
  const resolved = resolveWalkthroughHunkFile(hunk, files);
4453
4956
  if (!resolved) continue;
4454
4957
  const previous = runs.at(-1);
4455
- 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) {
4456
4962
  const hunks = [...previous.hunks, hunk];
4457
4963
  runs[runs.length - 1] = {
4458
4964
  ...previous,
@@ -5056,124 +5562,165 @@ function CopyFilePathButton({ path }) {
5056
5562
  })
5057
5563
  });
5058
5564
  }
5059
- 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 }) {
5060
5587
  const { canRenderMarkdown, file, isCollapsed, isMarkdownPreview, isSelected, isViewed, lineCount, reviewIdentity, section, sectionCount, walkthroughNote } = meta;
5061
5588
  const canOpenFile = file.status !== "deleted";
5062
5589
  const canLoadSection = shouldLoadDiffSectionContents(section);
5590
+ const directoryEnd = file.path.lastIndexOf("/") + 1;
5063
5591
  return /* @__PURE__ */ jsxs("div", {
5064
5592
  className: `codiff-file-header${walkthroughNote ? " with-note" : ""}${isCollapsed ? " collapsed" : ""}${isSelected ? " selected" : ""}${isViewed ? " viewed" : ""}`,
5065
- children: [
5066
- /* @__PURE__ */ jsxs("div", {
5067
- "aria-expanded": !isCollapsed,
5068
- "aria-label": isCollapsed ? "Expand file" : "Collapse file",
5069
- className: "codiff-header-toggle",
5070
- onClick: () => onToggleCollapsed(file, isCollapsed, reviewIdentity.key),
5071
- onKeyDown: (event) => {
5072
- if (event.key === "Enter" || event.key === " ") {
5073
- event.preventDefault();
5074
- onToggleCollapsed(file, isCollapsed, reviewIdentity.key);
5075
- }
5076
- },
5077
- role: "button",
5078
- tabIndex: 0,
5079
- 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",
5080
5617
  children: [
5081
- /* @__PURE__ */ jsx("span", {
5082
- className: "codiff-chevron-box",
5083
- children: /* @__PURE__ */ jsx(CaretDownIcon, {
5084
- "aria-hidden": true,
5085
- className: isCollapsed ? "codiff-chevron collapsed" : "codiff-chevron",
5086
- size: 16,
5087
- weight: "bold"
5088
- })
5089
- }),
5090
5618
  /* @__PURE__ */ jsxs("span", {
5091
- className: "codiff-file-heading",
5619
+ className: "codiff-file-path-row",
5092
5620
  children: [
5093
5621
  /* @__PURE__ */ jsxs("span", {
5094
- className: "codiff-file-path-row",
5095
- children: [/* @__PURE__ */ jsx("span", {
5096
- className: "codiff-file-path",
5097
- children: file.path
5098
- }), /* @__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
+ })]
5099
5631
  }),
5100
- file.oldPath ? /* @__PURE__ */ jsx("span", {
5101
- className: "codiff-file-old-path",
5102
- children: file.oldPath
5103
- }) : null,
5104
- walkthroughNote ? /* @__PURE__ */ jsx("span", {
5105
- className: "codiff-file-note",
5106
- 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
+ })
5107
5651
  }) : null
5108
5652
  ]
5109
5653
  }),
5110
- sectionCount > 1 ? /* @__PURE__ */ jsx("span", {
5111
- className: `codiff-section-badge ${section.kind}`,
5112
- children: sectionLabel[section.kind]
5654
+ file.oldPath ? /* @__PURE__ */ jsx("span", {
5655
+ className: "codiff-file-old-path",
5656
+ children: file.oldPath
5113
5657
  }) : null,
5114
- isGeneratedWalkthroughFile(file) ? /* @__PURE__ */ jsx("span", {
5115
- className: "codiff-generated-badge",
5116
- title: "Generated file",
5117
- children: "Generated"
5658
+ walkthroughNote ? /* @__PURE__ */ jsx("span", {
5659
+ className: "codiff-file-note",
5660
+ children: walkthroughNote.reason
5118
5661
  }) : null
5119
5662
  ]
5120
- }),
5121
- /* @__PURE__ */ jsx(DiffLineCountBadge, { lineCount }),
5122
- /* @__PURE__ */ jsx("div", {
5123
- className: `codiff-status-badge ${file.status}`,
5124
- children: statusLabel[file.status]
5125
- }),
5126
- canCreateFileComment ? /* @__PURE__ */ jsxs(Button, {
5127
- className: "codiff-file-comment-button",
5128
- onClick: onCreateFileComment,
5129
- title: "Comment on file",
5130
- type: "button",
5131
- children: [/* @__PURE__ */ jsx(ChatCircleIcon, {
5132
- "aria-hidden": true,
5133
- className: "codiff-file-comment-icon",
5134
- size: 14,
5135
- weight: "bold"
5136
- }), "Comment"]
5137
- }) : null,
5138
- canRenderMarkdown ? /* @__PURE__ */ jsx(Button, {
5139
- "aria-pressed": isMarkdownPreview,
5140
- className: `codiff-markdown-button${isMarkdownPreview ? " active" : ""}`,
5141
- onClick: () => onToggleMarkdownPreview(file, section),
5142
- title: isMarkdownPreview ? "View as Diff" : "View as Markdown",
5143
- type: "button",
5144
- children: isMarkdownPreview ? "View as Diff" : "View as Markdown"
5145
- }) : null,
5146
- canLoadSection && !readOnly ? /* @__PURE__ */ jsx("button", {
5147
- className: "codiff-load-button",
5148
- disabled: isSectionLoading,
5149
- onClick: () => onLoadSection(file, section),
5150
- title: isSectionLoading ? "Loading file contents" : "Load file contents",
5151
- type: "button",
5152
- children: isSectionLoading ? "Loading..." : "Load"
5153
- }) : null,
5154
- !readOnly && onOpenFile ? /* @__PURE__ */ jsx(Button, {
5155
- disabled: !canOpenFile,
5156
- onClick: () => onOpenFile(file),
5157
- title: canOpenFile ? "Open file in editor" : "Deleted files cannot be opened",
5158
- type: "button",
5159
- children: "Open"
5160
- }) : null,
5161
- !readOnly || allowViewedToggle ? /* @__PURE__ */ jsxs("button", {
5162
- "aria-pressed": isViewed,
5163
- className: `codiff-viewed-button${isViewed ? " active" : ""}`,
5164
- onClick: () => onToggleViewed(file, isViewed, reviewIdentity),
5165
- type: "button",
5166
- children: [/* @__PURE__ */ jsx("span", {
5167
- "aria-hidden": true,
5168
- className: "codiff-viewed-checkbox",
5169
- children: isViewed ? /* @__PURE__ */ jsx(CheckIcon, {
5170
- className: "codiff-viewed-check",
5171
- 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,
5172
5686
  weight: "bold"
5173
- }) : null
5174
- }), "Viewed"]
5175
- }) : null
5176
- ]
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
+ })]
5177
5724
  });
5178
5725
  }
5179
5726
  const getReviewAuthorDisplayName = (author) => author.name || author.login || "Git user";
@@ -5223,28 +5770,48 @@ const formatBytes = (size) => {
5223
5770
  }
5224
5771
  return `${size} B`;
5225
5772
  };
5226
- function MarkdownPreview({ contents, editable, layoutKey, onEditorRef, onLayoutReady, path, sectionId }) {
5227
- useLayoutEffect(() => {
5228
- onLayoutReady(sectionId);
5773
+ function MarkdownPreview({ cacheKey, contents, editable, heightCache, onEditorRef, onLayoutReady, path, sectionId }) {
5774
+ const contentRef = useRef(null);
5775
+ const [ready, setReady] = useState(false);
5776
+ const measure = useCallback((reportedHeight) => {
5777
+ const content = contentRef.current;
5778
+ if (!content?.isConnected || content.querySelector(".codiff-markdown-editor-message:not(.error), .codiff-readonly-markdown-loading")) return;
5779
+ const height = content.getBoundingClientRect().height || reportedHeight;
5780
+ if (height == null || height <= 0) return;
5781
+ setReady(true);
5782
+ if (heightCache.get(cacheKey) !== height) {
5783
+ heightCache.set(cacheKey, height);
5784
+ onLayoutReady(sectionId);
5785
+ }
5229
5786
  }, [
5230
- layoutKey,
5787
+ cacheKey,
5788
+ heightCache,
5231
5789
  onLayoutReady,
5232
5790
  sectionId
5233
5791
  ]);
5234
- return editable ? /* @__PURE__ */ jsx("div", {
5235
- className: "codiff-markdown-preview editable",
5236
- children: /* @__PURE__ */ jsx(RepositoryMarkdownEditor, {
5237
- onHeightChange: () => onLayoutReady(sectionId),
5238
- path,
5239
- ref: (editor) => onEditorRef(sectionId, editor)
5240
- })
5241
- }) : /* @__PURE__ */ jsx("div", {
5242
- className: "codiff-markdown-preview",
5243
- children: /* @__PURE__ */ jsx(ReadOnlyMarkdown, {
5244
- ariaLabel: `Preview ${path}`,
5245
- className: "codiff-markdown-preview-editor",
5246
- onHeightChange: () => onLayoutReady(sectionId),
5247
- value: contents
5792
+ useLayoutEffect(() => {
5793
+ const content = contentRef.current;
5794
+ if (!content) return;
5795
+ const observer = new ResizeObserver(() => measure());
5796
+ observer.observe(content);
5797
+ measure();
5798
+ return () => observer.disconnect();
5799
+ }, [measure]);
5800
+ return /* @__PURE__ */ jsx("div", {
5801
+ style: { minHeight: ready ? void 0 : heightCache.get(cacheKey) },
5802
+ children: /* @__PURE__ */ jsx("div", {
5803
+ className: `codiff-markdown-preview${editable ? " editable" : ""}`,
5804
+ ref: contentRef,
5805
+ children: editable ? /* @__PURE__ */ jsx(RepositoryMarkdownEditor, {
5806
+ onHeightChange: measure,
5807
+ path,
5808
+ ref: (editor) => onEditorRef(sectionId, editor)
5809
+ }) : /* @__PURE__ */ jsx(ReadOnlyMarkdown, {
5810
+ ariaLabel: `Preview ${path}`,
5811
+ className: "codiff-markdown-preview-editor",
5812
+ onHeightChange: measure,
5813
+ value: contents
5814
+ })
5248
5815
  })
5249
5816
  });
5250
5817
  }
@@ -6642,8 +7209,30 @@ const getHunkSelectionRange = (hunk) => {
6642
7209
  } : null;
6643
7210
  };
6644
7211
  const isSameSelection = (a, b) => a.start === b.start && a.end === b.end && a.side === b.side;
6645
- 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 }) {
6646
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` };
7233
+ const getPlaceholderFile = useCodeViewPlaceholderFile();
7234
+ const getAnnotations = useCodeViewAnnotations();
7235
+ const [markdownPreviewHeights] = useState(() => /* @__PURE__ */ new Map());
6647
7236
  const markdownEditorRefs = useRef(/* @__PURE__ */ new Map());
6648
7237
  const refreshingMarkdownSectionsRef = useRef(/* @__PURE__ */ new Set());
6649
7238
  const deferredTimersRef = useRef(/* @__PURE__ */ new Set());
@@ -6751,12 +7340,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6751
7340
  }
6752
7341
  }],
6753
7342
  collapsed: false,
6754
- file: {
6755
- cacheKey: `walkthrough-header:${block.id}`,
6756
- contents: " ",
6757
- lang: "text",
6758
- name: headerId
6759
- },
7343
+ file: getPlaceholderFile(headerId),
6760
7344
  id: headerId,
6761
7345
  type: "file",
6762
7346
  version: getItemVersion(`${block.id}:walkthrough-header`)
@@ -6861,12 +7445,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6861
7445
  }
6862
7446
  }],
6863
7447
  collapsed: isCollapsed,
6864
- file: {
6865
- cacheKey: `image-preview:${file.fingerprint}:${section.id}`,
6866
- contents: " ",
6867
- lang: "text",
6868
- name: file.path
6869
- },
7448
+ file: getPlaceholderFile(file.path),
6870
7449
  id,
6871
7450
  type: "file",
6872
7451
  version: getItemVersion(`${reviewVersionPrefix}:image:${sectionStateVersionKey}:${imagePreviewLayoutPassBySection[section.id] ?? 0}`)
@@ -6877,7 +7456,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6877
7456
  const markdownPreviewAddedLinesDigest = getAddedLinesDigest(markdownPreview.addedLines);
6878
7457
  const markdownPreviewLayoutKey = `${section.id}:${markdownPreview.contents.length}:${markdownPreviewAddedLinesDigest}`;
6879
7458
  nextItems.push({
6880
- annotations: [...fileCommentAnnotations, {
7459
+ annotations: getAnnotations(id, JSON.stringify([
7460
+ markdownPreview.contents,
7461
+ markdownPreviewAddedLinesDigest,
7462
+ canEditMarkdown,
7463
+ fileCommentAnnotations
7464
+ ]), [...fileCommentAnnotations, {
6881
7465
  lineNumber: 1,
6882
7466
  metadata: {
6883
7467
  addedLines: markdownPreview.addedLines,
@@ -6888,14 +7472,9 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6888
7472
  sectionId: section.id,
6889
7473
  type: "markdown-preview"
6890
7474
  }
6891
- }],
7475
+ }]),
6892
7476
  collapsed: isCollapsed,
6893
- file: {
6894
- cacheKey: `markdown-preview:${section.newFile?.cacheKey ?? file.fingerprint}:${markdownPreview.contents.length}:${markdownPreviewAddedLinesDigest}`,
6895
- contents: " ",
6896
- lang: "text",
6897
- name: file.path
6898
- },
7477
+ file: getPlaceholderFile(file.path),
6899
7478
  id,
6900
7479
  type: "file",
6901
7480
  version: getItemVersion(`${reviewVersionPrefix}:markdown:${sectionStateVersionKey}:${markdownPreviewLayoutKey}:${markdownPreviewLayoutPassBySection[section.id] ?? 0}`)
@@ -6931,6 +7510,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6931
7510
  diffStyle,
6932
7511
  expandedReviewKeys,
6933
7512
  forceExpandedPaths,
7513
+ getAnnotations,
7514
+ getPlaceholderFile,
6934
7515
  imagePreviewLayoutPassBySection,
6935
7516
  isReadOnly,
6936
7517
  itemVersionByKey,
@@ -6949,20 +7530,101 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
6949
7530
  if (items.length > 0 || !sourceDescriptionItemId) return items;
6950
7531
  return [{
6951
7532
  collapsed: true,
6952
- file: {
6953
- cacheKey: sourceDescriptionItemId,
6954
- contents: "",
6955
- lang: "text",
6956
- name: shouldShowCommitMessage ? "commit-message.md" : "source-description.md"
6957
- },
7533
+ file: getPlaceholderFile(shouldShowCommitMessage ? "commit-message.md" : "source-description.md", ""),
6958
7534
  id: sourceDescriptionItemId,
6959
7535
  type: "file"
6960
7536
  }];
6961
7537
  }, [
7538
+ getPlaceholderFile,
6962
7539
  items,
6963
7540
  shouldShowCommitMessage,
6964
7541
  sourceDescriptionItemId
6965
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
+ ]);
6966
7628
  const clearCommentLineHighlight = useCallback(() => {
6967
7629
  codeViewRef.current?.clearSelectedLines();
6968
7630
  navigatedSelectionRef.current = null;
@@ -7145,7 +7807,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7145
7807
  expandUnchanged: false,
7146
7808
  expansionLineCount: 100,
7147
7809
  hunkSeparators: "line-info-basic",
7148
- itemMetrics: codeViewItemMetrics,
7810
+ itemMetrics,
7149
7811
  layout: {
7150
7812
  ...codeViewLayout,
7151
7813
  paddingBottom: bottomInset
@@ -7153,11 +7815,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7153
7815
  lineHoverHighlight: "both",
7154
7816
  loadDiffFiles,
7155
7817
  onGutterUtilityClick: (range, context) => {
7156
- if (isReadOnly) return;
7818
+ if (isReadOnly || context.item.edit) return;
7157
7819
  ignoreNextLineSelectionEndRef.current = context.item.type === "diff";
7158
7820
  createCommentForRange(range, context);
7159
7821
  },
7160
7822
  onLineClick: (line, context) => {
7823
+ if (context.item.edit) return;
7161
7824
  const lineElement = "lineElement" in line ? line.lineElement : null;
7162
7825
  if (isPrimaryModifier(line.event) && onFindDefinitions) {
7163
7826
  line.event.preventDefault();
@@ -7222,7 +7885,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7222
7885
  });
7223
7886
  },
7224
7887
  onLineSelectionEnd: (range, context) => {
7225
- if (isReadOnly) return;
7888
+ if (isReadOnly || context.item.edit) return;
7226
7889
  if (ignoreNextLineSelectionEndRef.current) {
7227
7890
  ignoreNextLineSelectionEndRef.current = false;
7228
7891
  return;
@@ -7237,10 +7900,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7237
7900
  node.classList.toggle("codiff-selected-item", metadata?.isSelected === true || selectedHeaderItemIds.has(context.item.id));
7238
7901
  node.classList.toggle("codiff-markdown-preview-item", metadata?.isMarkdownPreview === true);
7239
7902
  node.classList.toggle("codiff-image-preview-item", context.item.type === "file" && Boolean(metadata && canRenderImagePreview(metadata.file.path, metadata.section)));
7240
- 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));
7241
7904
  node.classList.toggle("codiff-loading-summary-item", Boolean(metadata && loadingSectionIds.has(metadata.section.id)));
7242
7905
  if (definitionModifierActiveRef.current) window.requestAnimationFrame(() => {
7243
- 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);
7244
7907
  });
7245
7908
  },
7246
7909
  overflow: wordWrap ? "wrap" : "scroll",
@@ -7251,7 +7914,8 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7251
7914
  },
7252
7915
  themeType: theme,
7253
7916
  tokenizeMaxLength: 1e5,
7254
- unsafeCSS: codeViewUnsafeCSS
7917
+ unsafeCSS: codeViewUnsafeCSS,
7918
+ useTokenTransformer: workerHighlighterOptions.useTokenTransformer
7255
7919
  }), [
7256
7920
  bottomInset,
7257
7921
  cancelPendingEmptyCommentDeletes,
@@ -7259,6 +7923,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7259
7923
  diffStyle,
7260
7924
  isReadOnly,
7261
7925
  itemMetadata,
7926
+ itemMetrics,
7262
7927
  loadDiffFiles,
7263
7928
  loadingSectionIds,
7264
7929
  onCreateComment,
@@ -7428,7 +8093,6 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7428
8093
  const viewer = handle?.getInstance();
7429
8094
  if (!handle || !viewer) return;
7430
8095
  handledHunkNavRef.current = hunkNavigation.request;
7431
- const headerHeight = codeViewItemMetrics.diffHeaderHeight;
7432
8096
  const anchors = [];
7433
8097
  const seen = /* @__PURE__ */ new Set();
7434
8098
  const lineScrollTarget = (id, lineNumber, side) => ({
@@ -7547,6 +8211,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7547
8211
  clearCommentLineHighlight,
7548
8212
  diffLineHeight,
7549
8213
  diffStyle,
8214
+ headerHeight,
7550
8215
  hunkNavigation,
7551
8216
  itemMetadata,
7552
8217
  items
@@ -7575,7 +8240,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7575
8240
  if (highlightFrameRef.current != null) window.cancelAnimationFrame(highlightFrameRef.current);
7576
8241
  highlightFrameRef.current = window.requestAnimationFrame(() => {
7577
8242
  highlightFrameRef.current = null;
7578
- applySearchHighlights(viewer.getRenderedItems(), searchQuery, resolvedActiveSearchMatch);
8243
+ applySearchHighlights(viewer.getRenderedItems().filter(({ item }) => !item.edit), searchQuery, resolvedActiveSearchMatch);
7579
8244
  });
7580
8245
  }, [resolvedActiveSearchMatch, searchQuery]);
7581
8246
  const updateRenderedIdentifierNavigation = useCallback((active, viewer) => {
@@ -7586,12 +8251,12 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7586
8251
  definitionHighlightFrameRef.current = null;
7587
8252
  }
7588
8253
  if (!active) {
7589
- applyIdentifierNavigationState(nextViewer.getRenderedItems(), false);
8254
+ applyIdentifierNavigationState(nextViewer.getRenderedItems().filter(({ item }) => !item.edit), false);
7590
8255
  return;
7591
8256
  }
7592
8257
  definitionHighlightFrameRef.current = window.requestAnimationFrame(() => {
7593
8258
  definitionHighlightFrameRef.current = null;
7594
- applyIdentifierNavigationState(nextViewer.getRenderedItems(), true);
8259
+ applyIdentifierNavigationState(nextViewer.getRenderedItems().filter(({ item }) => !item.edit), true);
7595
8260
  });
7596
8261
  }, []);
7597
8262
  const scheduleStickyHeaderStateUpdate = useCallback((viewer) => {
@@ -7621,30 +8286,44 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7621
8286
  ]);
7622
8287
  useEffect(() => {
7623
8288
  if (!onFindDefinitions) return;
8289
+ let pointerDown = false;
7624
8290
  const updateDefinitionModifierState = (active) => {
7625
8291
  if (definitionModifierActiveRef.current === active) return;
7626
8292
  definitionModifierActiveRef.current = active;
7627
8293
  updateRenderedIdentifierNavigation(active);
7628
8294
  };
7629
8295
  const handleModifierChange = (event) => {
7630
- updateDefinitionModifierState(isPrimaryModifier(event));
8296
+ updateDefinitionModifierState(isPrimaryModifier(event) && !pointerDown);
7631
8297
  };
7632
8298
  const handleModifierPointer = (event) => {
7633
- updateDefinitionModifierState(isPrimaryModifier(event));
8299
+ pointerDown = (event.buttons & 1) !== 0;
8300
+ updateDefinitionModifierState(isPrimaryModifier(event) && !pointerDown);
8301
+ };
8302
+ const handleSelectionChange = () => {
8303
+ updateRenderedIdentifierNavigation(definitionModifierActiveRef.current);
8304
+ };
8305
+ const clearDefinitionModifierState = () => {
8306
+ pointerDown = false;
8307
+ updateDefinitionModifierState(false);
7634
8308
  };
7635
- const clearDefinitionModifierState = () => updateDefinitionModifierState(false);
7636
8309
  window.addEventListener("keydown", handleModifierChange);
7637
8310
  window.addEventListener("keyup", handleModifierChange);
7638
8311
  window.addEventListener("pointerdown", handleModifierPointer);
7639
8312
  window.addEventListener("pointermove", handleModifierPointer);
8313
+ window.addEventListener("pointerup", handleModifierPointer);
8314
+ window.addEventListener("pointercancel", clearDefinitionModifierState);
7640
8315
  window.addEventListener("blur", clearDefinitionModifierState);
8316
+ document.addEventListener("selectionchange", handleSelectionChange);
7641
8317
  return () => {
7642
8318
  clearDefinitionModifierState();
7643
8319
  window.removeEventListener("keydown", handleModifierChange);
7644
8320
  window.removeEventListener("keyup", handleModifierChange);
7645
8321
  window.removeEventListener("pointerdown", handleModifierPointer);
7646
8322
  window.removeEventListener("pointermove", handleModifierPointer);
8323
+ window.removeEventListener("pointerup", handleModifierPointer);
8324
+ window.removeEventListener("pointercancel", clearDefinitionModifierState);
7647
8325
  window.removeEventListener("blur", clearDefinitionModifierState);
8326
+ document.removeEventListener("selectionchange", handleSelectionChange);
7648
8327
  };
7649
8328
  }, [onFindDefinitions, updateRenderedIdentifierNavigation]);
7650
8329
  const invalidateDefinitionLookup = useCallback(() => {
@@ -7719,10 +8398,47 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7719
8398
  toggleSourceDescriptionCollapsed
7720
8399
  ]);
7721
8400
  const renderCustomHeader = useCallback((item) => {
7722
- const meta = itemMetadata.get(item.id);
7723
- 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, {
7724
8409
  allowViewedToggle,
7725
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,
7726
8442
  isSectionLoading: loadingSectionIds.has(meta.section.id),
7727
8443
  meta,
7728
8444
  onCreateFileComment: () => createFileComment(meta, item.id),
@@ -7732,11 +8448,17 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7732
8448
  onToggleMarkdownPreview: toggleMarkdownPreview,
7733
8449
  onToggleViewed,
7734
8450
  readOnly: isReadOnly
7735
- }) : null;
8451
+ }), editError ? /* @__PURE__ */ jsx("div", {
8452
+ className: "codiff-code-edit-error",
8453
+ role: "alert",
8454
+ children: editError
8455
+ }) : null] }) : null;
7736
8456
  }, [
7737
8457
  allowViewedToggle,
7738
8458
  canCreateFileComments,
8459
+ canEditCodeItem,
7739
8460
  createFileComment,
8461
+ codeEditing,
7740
8462
  itemMetadata,
7741
8463
  isReadOnly,
7742
8464
  loadingSectionIds,
@@ -7744,6 +8466,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7744
8466
  onOpenFile,
7745
8467
  onToggleCollapsed,
7746
8468
  onToggleViewed,
8469
+ startCodeEdit,
7747
8470
  toggleMarkdownPreview
7748
8471
  ]);
7749
8472
  const renderAnnotation = useCallback((annotation, item) => {
@@ -7758,9 +8481,10 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7758
8481
  }) : null;
7759
8482
  }
7760
8483
  if (annotation.metadata.type === "markdown-preview") return /* @__PURE__ */ jsx(MarkdownPreview, {
8484
+ cacheKey: `${sourceKey}:${item.id}`,
7761
8485
  contents: annotation.metadata.contents,
7762
8486
  editable: annotation.metadata.editable,
7763
- layoutKey: annotation.metadata.layoutKey,
8487
+ heightCache: markdownPreviewHeights,
7764
8488
  onEditorRef: setMarkdownEditorRef,
7765
8489
  onLayoutReady: markMarkdownPreviewLayoutReady,
7766
8490
  path: annotation.metadata.path,
@@ -7802,6 +8526,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7802
8526
  itemMetadata,
7803
8527
  keymap,
7804
8528
  markMarkdownPreviewLayoutReady,
8529
+ markdownPreviewHeights,
7805
8530
  markImagePreviewLayoutReady,
7806
8531
  markCommentLayoutChanged,
7807
8532
  onAskCodex,
@@ -7815,6 +8540,7 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7815
8540
  replyToThread,
7816
8541
  setMarkdownEditorRef,
7817
8542
  source,
8543
+ sourceKey,
7818
8544
  supportsReviewCommentActions
7819
8545
  ]);
7820
8546
  const handleScroll = useCallback((_scrollTop, viewer) => {
@@ -7842,10 +8568,14 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7842
8568
  scheduleStickyHeaderStateUpdate,
7843
8569
  updateRenderedIdentifierNavigation
7844
8570
  ]);
7845
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [disableWorkerPool ? /* @__PURE__ */ jsx(CodeView, {
7846
- className: "code-view",
8571
+ const codeView = /* @__PURE__ */ jsx(CodeView, {
8572
+ className: `code-view${compactHeaders ? " compact-file-headers" : ""}`,
8573
+ containerRef: setCodeViewElement,
7847
8574
  disableWorkerPool,
7848
- items: codeViewItems,
8575
+ editorOptions: codeEditing.editorOptions,
8576
+ items: codeEditing.items,
8577
+ onItemEditChange: codeEditing.onItemEditChange,
8578
+ onItemEditComplete: codeEditing.onItemEditComplete,
7849
8579
  onScroll: handleScroll,
7850
8580
  onSelectedLinesChange: setCodeViewSelectedLines,
7851
8581
  options: codeViewOptions,
@@ -7853,14 +8583,20 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7853
8583
  renderAnnotation,
7854
8584
  renderCodeViewHeader: sourceDescriptionItemId ? renderCodeViewHeader : void 0,
7855
8585
  renderCustomHeader,
7856
- selectedLines: isReadOnly ? null : selectedLines
7857
- }) : /* @__PURE__ */ jsx(WorkerPoolContextProvider, {
8586
+ selectedLines: isReadOnly ? null : selectedLines,
8587
+ style: codeViewStyle
8588
+ });
8589
+ const renderedCodeView = disableWorkerPool ? codeView : /* @__PURE__ */ jsx(WorkerPoolContextProvider, {
7858
8590
  highlighterOptions: workerHighlighterOptions,
7859
8591
  poolOptions: workerPoolOptions,
7860
8592
  children: /* @__PURE__ */ jsx(CodeView, {
7861
- className: "code-view",
8593
+ className: `code-view${compactHeaders ? " compact-file-headers" : ""}`,
8594
+ containerRef: setCodeViewElement,
7862
8595
  disableWorkerPool: false,
7863
- items: codeViewItems,
8596
+ editorOptions: codeEditing.editorOptions,
8597
+ items: codeEditing.items,
8598
+ onItemEditChange: codeEditing.onItemEditChange,
8599
+ onItemEditComplete: codeEditing.onItemEditComplete,
7864
8600
  onScroll: handleScroll,
7865
8601
  onSelectedLinesChange: setCodeViewSelectedLines,
7866
8602
  options: codeViewOptions,
@@ -7868,8 +8604,13 @@ function ReviewCodeView({ activeSearchMatch, agentId, agentLabel, allowViewedTog
7868
8604
  renderAnnotation,
7869
8605
  renderCodeViewHeader: sourceDescriptionItemId ? renderCodeViewHeader : void 0,
7870
8606
  renderCustomHeader,
7871
- selectedLines: isReadOnly ? null : selectedLines
8607
+ selectedLines: isReadOnly ? null : selectedLines,
8608
+ style: codeViewStyle
7872
8609
  })
8610
+ });
8611
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(EditProvider, {
8612
+ createEditor: createCodeEditor,
8613
+ children: renderedCodeView
7873
8614
  }), definitionLookup?.sourceKey === sourceKey && onOpenDefinition ? /* @__PURE__ */ jsx(DefinitionPopover, {
7874
8615
  anchor: definitionLookup.anchor,
7875
8616
  getDestination: (candidate) => getDefinitionDiffTarget(candidate) ? "diff" : candidate.canOpenInEditor ? "editor" : "unavailable",
@@ -9586,54 +10327,6 @@ function useReviewFileState({ initialSelectedPath = null, onViewedChange } = {})
9586
10327
  };
9587
10328
  }
9588
10329
  //#endregion
9589
- //#region config/defaults.ts
9590
- const defaultConfigTemplate = {
9591
- settings: {
9592
- "agentBackend": "codex",
9593
- "checkForUpdates": true,
9594
- "claudeModel": "claude-sonnet-4-6",
9595
- "codeFontFamily": "",
9596
- "codeFontSize": 13,
9597
- "copyCommentsOnClose": false,
9598
- "diffStyle": "split",
9599
- "editorCommand": "",
9600
- "lastRepositoryPath": "",
9601
- "openAIModel": "gpt-5.6-terra",
9602
- "opencodeModel": "opencode-default",
9603
- "piModel": "pi-default",
9604
- "reviewCommentsPrefix": "# Address these Review Comments",
9605
- "sidebarPosition": "left",
9606
- "showOutdated": false,
9607
- "showWhitespace": false,
9608
- "theme": "system",
9609
- "walkthroughPrompt": "",
9610
- "wordWrap": false
9611
- },
9612
- keymap: {
9613
- "askAgent": "Mod+Alt+Enter",
9614
- "closeSearch": "Escape",
9615
- "commandBar": "Mod+k",
9616
- "diffSearch": "Mod+f",
9617
- "discardComment": "Escape",
9618
- "fileFilter": "Mod+p",
9619
- "nextHunk": ["Ctrl+ArrowDown", "j"],
9620
- "nextSearchMatch": "Enter",
9621
- "openFile": "Mod+Shift+o",
9622
- "prevHunk": ["Ctrl+ArrowUp", "k"],
9623
- "prevSearchMatch": "Shift+Enter",
9624
- "shortcutsHelp": "Shift+?",
9625
- "submitComment": "Mod+Enter",
9626
- "toggleSidebar": "Mod+Shift+b",
9627
- "toggleWordWrap": "Alt+z"
9628
- }
9629
- };
9630
- const createDefaultConfig = () => ({
9631
- keymap: { ...defaultConfigTemplate.keymap },
9632
- settings: { ...defaultConfigTemplate.settings }
9633
- });
9634
- Object.freeze({ ...defaultConfigTemplate.keymap });
9635
- Object.freeze({ ...defaultConfigTemplate.settings });
9636
- //#endregion
9637
10330
  //#region lib/app-constants.ts
9638
10331
  const AGENT_LABELS = {
9639
10332
  claude: "Claude Code",