From 6b23fce8c5a942955342f47416f808a206e8715a Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Thu, 5 Feb 2026 12:52:50 -0600 Subject: [PATCH] Claude 4.6 after light instruction basically one shotted this, it will be interesting...{ --- package.json | 5 + sharedUtils/index.ts | 15 +- src/extension.ts | 2 + src/projectManager/utils/merge/resolvers.ts | 32 +++- .../utils/migrationCompletionUtils.ts | 1 + src/projectManager/utils/migrationUtils.ts | 168 ++++++++++++++++++ .../codexCellEditorProvider/codexDocument.ts | 87 ++++++--- .../utils/cellUtils.ts | 1 + src/utils/editHistoryId.ts | 34 ++++ src/utils/editMapUtils.ts | 3 + types/index.d.ts | 6 + .../src/CodexCellEditor/Editor.tsx | 14 +- .../src/CodexCellEditor/TextCellEditor.tsx | 1 + 13 files changed, 331 insertions(+), 38 deletions(-) create mode 100644 src/utils/editHistoryId.ts diff --git a/package.json b/package.json index 66d7c8ded..58e2fb6d3 100644 --- a/package.json +++ b/package.json @@ -531,6 +531,11 @@ "default": false, "description": "Internal flag indicating the documentContext hoist migration has completed." }, + "codex-project-manager.editHistoryIdsMigrationCompleted": { + "type": "boolean", + "default": false, + "description": "Internal flag indicating the edit history IDs migration has completed." + }, "codex-project-manager.projectName": { "type": "string", "default": "", diff --git a/sharedUtils/index.ts b/sharedUtils/index.ts index 0927d1e79..ba24e5688 100644 --- a/sharedUtils/index.ts +++ b/sharedUtils/index.ts @@ -51,11 +51,16 @@ export const getCellValueData = (cell: QuillCellContent) => { // Ensure editHistory exists and is an array const editHistory = cell.editHistory || []; - // Find the latest edit that matches the current cell content - const latestEditThatMatchesCellValue = editHistory - .slice() - .reverse() - .find((edit) => EditMapUtils.isValue(edit.editMap) && edit.value === cell.cellContent); + // Find the edit matching the current cell content: prefer activeEditId, fall back to value scan + let latestEditThatMatchesCellValue = cell.activeEditId + ? editHistory.find((edit) => edit.id === cell.activeEditId) + : undefined; + if (!latestEditThatMatchesCellValue) { + latestEditThatMatchesCellValue = editHistory + .slice() + .reverse() + .find((edit) => EditMapUtils.isValue(edit.editMap) && edit.value === cell.cellContent); + } // Get audio validation from attachments instead of edits let audioValidatedBy: ValidationEntry[] = []; diff --git a/src/extension.ts b/src/extension.ts index e1758b1cc..cdda7d7e3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,6 +21,7 @@ import { migration_addGlobalReferences, migration_cellIdsToUuid, migration_recoverTempFilesAndMergeDuplicates, + migration_addEditHistoryIds, } from "./projectManager/utils/migrationUtils"; import { createIndexWithContext } from "./activationHelpers/contextAware/contentIndexes/indexes"; import { StatusBarItem } from "vscode"; @@ -612,6 +613,7 @@ export async function activate(context: vscode.ExtensionContext) { await migration_addGlobalReferences(context); await migration_cellIdsToUuid(context); await migration_recoverTempFilesAndMergeDuplicates(context); + await migration_addEditHistoryIds(context); // After migrations complete, trigger sync directly // (All migrations have finished executing since they're awaited sequentially) diff --git a/src/projectManager/utils/merge/resolvers.ts b/src/projectManager/utils/merge/resolvers.ts index dada483f7..cb8c20aaa 100644 --- a/src/projectManager/utils/merge/resolvers.ts +++ b/src/projectManager/utils/merge/resolvers.ts @@ -10,6 +10,7 @@ import { CodexCell } from "@/utils/codexNotebookUtils"; import { CodexCellTypes, EditType } from "../../../../types/enums"; import { EditHistory, ValidationEntry, FileEditHistory, ProjectEditHistory } from "../../../../types/index.d"; import { EditMapUtils, deduplicateFileMetadataEdits } from "../../../utils/editMapUtils"; +import { generateEditId } from "../../../utils/editHistoryId"; import { normalizeAttachmentUrl } from "@/utils/pathUtils"; import { formatJsonForNotebookFile } from "../../../utils/notebookFileFormattingUtils"; import { @@ -759,6 +760,18 @@ function migrateEditHistoryInContent(content: string): string { } } +/** + * Ensures all edits in an array have IDs. Adds deterministic IDs to any edits missing them + * (handles pre-migration data during merges). + */ +function ensureEditHistoryIds(edits: any[]): void { + for (const edit of edits) { + if (!edit.id && edit.editMap && edit.timestamp != null && edit.author) { + edit.id = generateEditId(edit.value, edit.timestamp, edit.author); + } + } +} + function mergeTwoCellsUsingResolverLogic( ourCell: CustomNotebookCellData, theirCell: CustomNotebookCellData @@ -774,6 +787,9 @@ function mergeTwoCellsUsingResolverLogic( ...(theirCell.metadata?.edits || []) ].sort((a, b) => a.timestamp - b.timestamp); + // Ensure all edits have IDs before dedup (handles pre-migration data) + ensureEditHistoryIds(allEdits); + // Remove duplicates based on timestamp, editMap and value, while merging validatedBy entries const editMap = new Map(); allEdits.forEach((edit) => { @@ -800,6 +816,15 @@ function mergeTwoCellsUsingResolverLogic( } mergedCell.metadata.edits = uniqueEdits; + // Set activeEditId on merged cell: find the latest value edit matching the winning cell value + const latestValueEdit = uniqueEdits + .slice() + .reverse() + .find((e: any) => EditMapUtils.isValue(e.editMap) && e.value === mergedCell.value); + if (latestValueEdit?.id) { + mergedCell.metadata.activeEditId = latestValueEdit.id; + } + // Merge attachments intelligently const mergedAttachments = mergeAttachments( ourCell.metadata?.attachments, @@ -921,12 +946,14 @@ export async function resolveCodexCustomMerge( const mergedMetadata = resolveMetadataConflictsUsingEditHistoryForFile(ourMetadata, theirMetadata); // Combine all metadata edits from both branches and deduplicate - // Similar to cell-level edits deduplication, remove duplicates based on timestamp, editMap and value const allMetadataEdits = [ ...(ourMetadata.edits || []), ...(theirMetadata.edits || []) ]; + // Ensure IDs exist on all edits before dedup (handles pre-migration data) + ensureEditHistoryIds(allMetadataEdits); + // Deduplicate edits using the same logic as cell-level edits mergedMetadata.edits = deduplicateFileMetadataEdits(allMetadataEdits); @@ -1675,6 +1702,9 @@ async function resolveMetadataJsonConflict(conflict: ConflictFile): Promise { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return; + } + + const migrationKey = "editHistoryIdsMigrationCompleted"; + const config = vscode.workspace.getConfiguration("codex-project-manager"); + let hasMigrationRun = false; + + try { + hasMigrationRun = config.get(migrationKey, false); + } catch (e) { + hasMigrationRun = !!context?.workspaceState.get(migrationKey); + } + + if (hasMigrationRun) { + debug("Edit history IDs migration already completed, skipping"); + return; + } + + debug("Running edit history IDs migration..."); + + const workspaceFolder = workspaceFolders[0]; + + const codexFiles = await vscode.workspace.findFiles( + new vscode.RelativePattern(workspaceFolder, "**/*.codex") + ); + const sourceFiles = await vscode.workspace.findFiles( + new vscode.RelativePattern(workspaceFolder, "**/*.source") + ); + + const allFiles = [...codexFiles, ...sourceFiles]; + + if (allFiles.length === 0) { + debug("No codex or source files found, skipping edit history IDs migration"); + try { + await config.update(migrationKey, true, vscode.ConfigurationTarget.Workspace); + } catch (e) { + await context?.workspaceState.update(migrationKey, true); + } + return; + } + + let processedFiles = 0; + let migratedFiles = 0; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Adding edit history IDs", + cancellable: false + }, + async (progress) => { + for (let i = 0; i < allFiles.length; i++) { + const file = allFiles[i]; + progress.report({ + message: `Processing ${path.basename(file.fsPath)}`, + increment: (100 / allFiles.length) + }); + + try { + const wasMigrated = await addEditHistoryIdsToFile(file); + processedFiles++; + if (wasMigrated) { + migratedFiles++; + } + } catch (error) { + console.error(`Error adding edit history IDs to ${file.fsPath}:`, error); + } + } + } + ); + + try { + await config.update(migrationKey, true, vscode.ConfigurationTarget.Workspace); + } catch (e) { + await context?.workspaceState.update(migrationKey, true); + } + + debug(`Edit history IDs migration completed: ${processedFiles} files processed, ${migratedFiles} files migrated`); + if (migratedFiles > 0) { + vscode.window.showInformationMessage( + `Edit history IDs migration complete: ${migratedFiles} files updated` + ); + } + + } catch (error) { + console.error("Error running edit history IDs migration:", error); + } +}; + +async function addEditHistoryIdsToFile(fileUri: vscode.Uri): Promise { + const fileContent = await vscode.workspace.fs.readFile(fileUri); + const text = new TextDecoder().decode(fileContent); + + let notebook: any; + try { + notebook = JSON.parse(text); + } catch { + return false; + } + + if (!notebook.cells || !Array.isArray(notebook.cells)) { + return false; + } + + let changed = false; + + // Process cell-level edits + for (const cell of notebook.cells) { + if (!cell.metadata?.edits || !Array.isArray(cell.metadata.edits)) { + continue; + } + + for (const edit of cell.metadata.edits) { + if (!edit.id && edit.editMap && edit.timestamp != null && edit.author) { + edit.id = generateEditId(edit.value, edit.timestamp, edit.author); + changed = true; + } + } + + // Set activeEditId if not already set: find latest value edit matching cell.value + if (!cell.metadata.activeEditId && cell.value != null) { + const valueEdits = cell.metadata.edits.filter( + (e: any) => Array.isArray(e.editMap) && e.editMap.length === 1 && e.editMap[0] === "value" + ); + for (let i = valueEdits.length - 1; i >= 0; i--) { + if (valueEdits[i].value === cell.value && valueEdits[i].id) { + cell.metadata.activeEditId = valueEdits[i].id; + changed = true; + break; + } + } + } + } + + // Process file-level metadata edits + if (notebook.metadata?.edits && Array.isArray(notebook.metadata.edits)) { + for (const edit of notebook.metadata.edits) { + if (!edit.id && edit.editMap && edit.timestamp != null && edit.author) { + edit.id = generateEditId(edit.value, edit.timestamp, edit.author); + changed = true; + } + } + } + + if (!changed) { + return false; + } + + const updatedText = formatJsonForNotebookFile(notebook); + await atomicWriteUriText(fileUri, updatedText); + return true; +} diff --git a/src/providers/codexCellEditorProvider/codexDocument.ts b/src/providers/codexCellEditorProvider/codexDocument.ts index 01a004130..438273283 100644 --- a/src/providers/codexCellEditorProvider/codexDocument.ts +++ b/src/providers/codexCellEditorProvider/codexDocument.ts @@ -17,6 +17,7 @@ import { CustomCellMetaData, } from "../../../types"; import { EditMapUtils, deduplicateFileMetadataEdits } from "../../utils/editMapUtils"; +import { generateEditId } from "../../utils/editHistoryId"; import { CodexCellTypes, EditType } from "../../../types/enums"; import { getAuthApi } from "@/extension"; import { randomUUID } from "crypto"; @@ -328,6 +329,7 @@ export class CodexCellDocument implements vscode.CustomDocument { const currentTimestamp = Date.now(); const previewEdit = { + id: generateEditId(newContent, currentTimestamp, this._author), editMap: EditMapUtils.value(), value: newContent, timestamp: currentTimestamp, @@ -364,14 +366,17 @@ export class CodexCellDocument implements vscode.CustomDocument { // If editing a source file's value for the first time, ensure an INITIAL_IMPORT exists if (cellToUpdate.metadata.edits.length === 0 && !!previousValue) { - cellToUpdate.metadata.edits.push({ + const initialImportTimestamp = currentTimestamp - 1000; + const initialImportEdit = { + id: generateEditId(previousValue, initialImportTimestamp, this._author), editMap: EditMapUtils.value(), value: previousValue, - timestamp: currentTimestamp - 1000, + timestamp: initialImportTimestamp, type: EditType.INITIAL_IMPORT, author: this._author, validatedBy: [], - }); + }; + cellToUpdate.metadata.edits.push(initialImportEdit); } // Update cell content and metadata in memory @@ -385,18 +390,23 @@ export class CodexCellDocument implements vscode.CustomDocument { if (editType === EditType.USER_EDIT) { if (retainValidations) { // Retain validations from only the current user if they exist (for search/replace operations) - // Find the edit corresponding to the previous value (same pattern as validateCellContent) + // Find the edit corresponding to the previous value: prefer activeEditId, fall back to value scan const previousEdits = cellToUpdate.metadata.edits || []; let targetEdit: any = null; - for (let i = previousEdits.length - 1; i >= 0; i--) { - const e = previousEdits[i]; - // Identify value edits using EditMapUtils and also match the exact value - const isValueEdit = EditMapUtils.isValue - ? EditMapUtils.isValue(e.editMap) - : EditMapUtils.equals(e.editMap, EditMapUtils.value()); - if (isValueEdit && e.value === previousValue) { - targetEdit = e; - break; + const prevActiveId = cellToUpdate.metadata.activeEditId; + if (prevActiveId) { + targetEdit = previousEdits.find((e) => e.id === prevActiveId) ?? null; + } + if (!targetEdit) { + for (let i = previousEdits.length - 1; i >= 0; i--) { + const e = previousEdits[i]; + const isValueEdit = EditMapUtils.isValue + ? EditMapUtils.isValue(e.editMap) + : EditMapUtils.equals(e.editMap, EditMapUtils.value()); + if (isValueEdit && e.value === previousValue) { + targetEdit = e; + break; + } } } @@ -439,7 +449,9 @@ export class CodexCellDocument implements vscode.CustomDocument { } } + const valueEditId = generateEditId(newContent, currentTimestamp, this._author); cellToUpdate.metadata.edits.push({ + id: valueEditId, editMap: EditMapUtils.value(), value: newContent, // TypeScript infers: string timestamp: currentTimestamp, @@ -447,8 +459,9 @@ export class CodexCellDocument implements vscode.CustomDocument { author: this._author, validatedBy, }); + cellToUpdate.metadata.activeEditId = valueEditId; - // Record the edit + // Record the edit // not being used ??? this._edits.push({ type: "updateCellContent", @@ -809,6 +822,7 @@ export class CodexCellDocument implements vscode.CustomDocument { ); if (!hasInitialStart && previousStartTime !== undefined) { cellToUpdate.metadata.edits.push({ + id: generateEditId(previousStartTime, currentTimestamp - 1000, this._author), editMap: EditMapUtils.dataStartTime(), value: previousStartTime, timestamp: currentTimestamp - 1000, @@ -819,6 +833,7 @@ export class CodexCellDocument implements vscode.CustomDocument { } const startTimeEditMap = EditMapUtils.dataStartTime(); cellToUpdate.metadata.edits.push({ + id: generateEditId(timestamps.startTime, currentTimestamp, this._author), editMap: startTimeEditMap, value: timestamps.startTime, timestamp: currentTimestamp, @@ -843,6 +858,7 @@ export class CodexCellDocument implements vscode.CustomDocument { ); if (!hasInitialEnd && previousEndTime !== undefined) { cellToUpdate.metadata.edits.push({ + id: generateEditId(previousEndTime, currentTimestamp - 1000, this._author), editMap: EditMapUtils.dataEndTime(), value: previousEndTime, timestamp: currentTimestamp - 1000, @@ -853,6 +869,7 @@ export class CodexCellDocument implements vscode.CustomDocument { } const endTimeEditMap = EditMapUtils.dataEndTime(); cellToUpdate.metadata.edits.push({ + id: generateEditId(timestamps.endTime, currentTimestamp, this._author), editMap: endTimeEditMap, value: timestamps.endTime, timestamp: currentTimestamp, @@ -936,6 +953,7 @@ export class CodexCellDocument implements vscode.CustomDocument { } const currentTimestamp = Date.now(); cellToSoftDelete.metadata.edits.push({ + id: generateEditId(true, currentTimestamp, this._author), editMap: EditMapUtils.dataDeleted(), value: true, timestamp: currentTimestamp, @@ -1104,6 +1122,7 @@ export class CodexCellDocument implements vscode.CustomDocument { // Add edit history entry with new structure this._documentData.metadata.edits.push({ + id: generateEditId(newValue, currentTimestamp, this._author), editMap, value: newValue, timestamp: currentTimestamp, @@ -2065,6 +2084,7 @@ export class CodexCellDocument implements vscode.CustomDocument { } const currentTimestamp = Date.now(); cellToUpdate.metadata.edits.push({ + id: generateEditId(newLabel, currentTimestamp, this._author), editMap: EditMapUtils.cellLabel(), value: newLabel, // TypeScript infers: string timestamp: currentTimestamp, @@ -2144,6 +2164,7 @@ export class CodexCellDocument implements vscode.CustomDocument { } const currentTimestamp = Date.now(); cellToUpdate.metadata.edits.push({ + id: generateEditId(isLocked, currentTimestamp, this._author), editMap: lockEditMap, value: isLocked, // TypeScript infers: boolean timestamp: currentTimestamp, @@ -2200,8 +2221,10 @@ export class CodexCellDocument implements vscode.CustomDocument { console.warn("No edits found for cell to validate"); // repair the edit history by adding an llm generation with author unknown, and then a user edit with validation const currentTimestamp = Date.now(); + const repairEditId = generateEditId(cellToUpdate.value, currentTimestamp, this._author); cellToUpdate.metadata.edits = [ { + id: generateEditId(cellToUpdate.value, currentTimestamp, "unknown"), editMap: EditMapUtils.value(), value: cellToUpdate.value, timestamp: currentTimestamp, @@ -2210,6 +2233,7 @@ export class CodexCellDocument implements vscode.CustomDocument { validatedBy: [], }, { + id: repairEditId, editMap: EditMapUtils.value(), value: cellToUpdate.value, timestamp: currentTimestamp, @@ -2218,28 +2242,38 @@ export class CodexCellDocument implements vscode.CustomDocument { validatedBy: [], }, ]; + cellToUpdate.metadata.activeEditId = repairEditId; } - // Find the correct edit corresponding to the CURRENT VALUE of the cell - // We must NOT validate metadata-only edits (e.g., label/timestamp). Validate the value edit - // whose value matches the current cell value. + // Find the correct edit corresponding to the CURRENT VALUE of the cell. + // Prefer activeEditId lookup, fall back to value-matching for backward compat. let targetEditIndex = -1; - for (let i = cellToUpdate.metadata.edits.length - 1; i >= 0; i--) { - const e = cellToUpdate.metadata.edits[i]; - // Identify value edits using EditMapUtils and also match the exact value - const isValueEdit = EditMapUtils.isValue - ? EditMapUtils.isValue(e.editMap) - : EditMapUtils.equals(e.editMap, EditMapUtils.value()); - if (isValueEdit && e.value === cellToUpdate.value) { - targetEditIndex = i; - break; + const activeId = cellToUpdate.metadata.activeEditId; + if (activeId) { + targetEditIndex = cellToUpdate.metadata.edits.findIndex( + (e) => e.id === activeId + ); + } + if (targetEditIndex === -1) { + // Fallback: scan for value edit matching cell value + for (let i = cellToUpdate.metadata.edits.length - 1; i >= 0; i--) { + const e = cellToUpdate.metadata.edits[i]; + const isValueEdit = EditMapUtils.isValue + ? EditMapUtils.isValue(e.editMap) + : EditMapUtils.equals(e.editMap, EditMapUtils.value()); + if (isValueEdit && e.value === cellToUpdate.value) { + targetEditIndex = i; + break; + } } } // If we didn't find a value edit that matches current value, create one so validation history is consistent if (targetEditIndex === -1) { const currentTimestamp = Date.now(); + const fallbackEditId = generateEditId(cellToUpdate.value, currentTimestamp, this._author); cellToUpdate.metadata.edits.push({ + id: fallbackEditId, editMap: EditMapUtils.value(), value: cellToUpdate.value, timestamp: currentTimestamp, @@ -2248,6 +2282,7 @@ export class CodexCellDocument implements vscode.CustomDocument { validatedBy: [], } as any); targetEditIndex = cellToUpdate.metadata.edits.length - 1; + cellToUpdate.metadata.activeEditId = fallbackEditId; } const latestEdit = cellToUpdate.metadata.edits[targetEditIndex]; diff --git a/src/providers/codexCellEditorProvider/utils/cellUtils.ts b/src/providers/codexCellEditorProvider/utils/cellUtils.ts index 8e0ac14be..e9d084325 100644 --- a/src/providers/codexCellEditorProvider/utils/cellUtils.ts +++ b/src/providers/codexCellEditorProvider/utils/cellUtils.ts @@ -65,6 +65,7 @@ export function convertCellToQuillContent(cell: CustomNotebookCellData): QuillCe cellContent: cell.value || "", cellType: cell.metadata?.type || CodexCellTypes.TEXT, editHistory: cell.metadata?.edits || [], + activeEditId: cell.metadata?.activeEditId, timestamps: cell.metadata?.data, cellLabel: cell.metadata?.cellLabel, merged: cell.metadata?.data?.merged, diff --git a/src/utils/editHistoryId.ts b/src/utils/editHistoryId.ts new file mode 100644 index 000000000..3c4dd57fe --- /dev/null +++ b/src/utils/editHistoryId.ts @@ -0,0 +1,34 @@ +/** + * Generates a deterministic edit ID from edit content using FNV-1a hash. + * Produces a 12-character hex string (48 bits) — collision-safe for per-cell edit lists. + * + * Deterministic so both sides of a merge produce the same ID for the same edit. + * Pure function, no external dependencies, works in both Node and browser contexts. + */ +export function generateEditId( + value: string | number | boolean | object, + timestamp: number, + author: string +): string { + const serializedValue = + typeof value === "object" && value !== null ? JSON.stringify(value) : String(value); + const input = `${serializedValue}|${timestamp}|${author}`; + + // FNV-1a 64-bit (using two 32-bit halves for JS integer safety) + let h1 = 0x811c9dc5; // FNV offset basis low + let h2 = 0xcbf29ce4; // FNV offset basis high + + for (let i = 0; i < input.length; i++) { + const c = input.charCodeAt(i); + h1 ^= c; + h2 ^= c >> 8; + // FNV prime multiply (lower half) + h1 = Math.imul(h1, 0x01000193); + h2 = Math.imul(h2, 0x01000193); + } + + // Combine into 12 hex chars (48 bits): 6 from each half + const low = (h1 >>> 0).toString(16).padStart(8, "0").slice(-6); + const high = (h2 >>> 0).toString(16).padStart(8, "0").slice(-6); + return `${high}${low}`; +} diff --git a/src/utils/editMapUtils.ts b/src/utils/editMapUtils.ts index 3f59752bb..41eb99cc0 100644 --- a/src/utils/editMapUtils.ts +++ b/src/utils/editMapUtils.ts @@ -29,6 +29,7 @@ type LanguagesEditMap = ["languages"]; type SpellcheckIsEnabledEditMap = ["spellcheckIsEnabled"]; import { EditType } from "../../types/enums"; +import { generateEditId } from "./editHistoryId"; // Utility functions for working with editMaps export const EditMapUtils = { @@ -292,6 +293,7 @@ export function addMetadataEdit( // Create the new edit entry const newEdit = { + id: generateEditId(value, currentTimestamp, author), editMap, value, timestamp: currentTimestamp, @@ -324,6 +326,7 @@ export function addProjectMetadataEdit( // Create the new edit entry const newEdit = { + id: generateEditId(value, currentTimestamp, author), editMap, value, timestamp: currentTimestamp, diff --git a/types/index.d.ts b/types/index.d.ts index 93f00017d..f0c5c7f9b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -841,6 +841,7 @@ type EditMapValueType = // Conditional type for EditHistory that infers value type based on editMap type EditHistoryBase = { + id?: string; author: string; timestamp: number; type: import("./enums").EditType; @@ -859,6 +860,7 @@ export type EditHistoryMutable = EditHistory; // Utility type for creating type-safe edits export type EditFor = { + id?: string; editMap: TEditMap; value: EditMapValueType; author: string; @@ -869,6 +871,7 @@ export type EditFor = { // File-level edit type for metadata edits (separate from EditHistory) export type FileEditHistory = { + id?: string; editMap: TEditMap; value: EditMapValueType; timestamp: number; @@ -878,6 +881,7 @@ export type FileEditHistory = { + id?: string; editMap: TEditMap; value: EditMapValueType; timestamp: number; @@ -904,6 +908,7 @@ type BaseCustomCellMetaData = { id: string; type: CodexCellTypes; edits: EditHistory[]; + activeEditId?: string; parentId?: string; // UUID of parent cell (for child cells like cues, paratext, etc.) isLocked?: boolean; }; @@ -1093,6 +1098,7 @@ interface QuillCellContent { cellContent: string; cellType: CodexCellTypes; editHistory: Array; + activeEditId?: string; timestamps?: Timestamps; cellLabel?: string; merged?: boolean; diff --git a/webviews/codex-webviews/src/CodexCellEditor/Editor.tsx b/webviews/codex-webviews/src/CodexCellEditor/Editor.tsx index 4cc6bdfeb..f1af2181a 100644 --- a/webviews/codex-webviews/src/CodexCellEditor/Editor.tsx +++ b/webviews/codex-webviews/src/CodexCellEditor/Editor.tsx @@ -44,6 +44,7 @@ export interface EditorProps { currentLineId: string; initialValue?: string; editHistory: EditHistory[]; + activeEditId?: string; onChange?: (changes: EditorContentChanged) => void; onDirtyChange?: (dirty: boolean, rawHtml: string) => void; spellCheckResponse?: SpellCheckResponse | null; @@ -1580,12 +1581,13 @@ const Editor = forwardRef((props, ref) => { ) : generateDiffHtml("", entry.value as string); - // Check if this is the most recent entry that matches the initial value - const isCurrentVersion = - entry.value === props.initialValue && - !array - .slice(0, index) - .some((e) => e.value === props.initialValue); + // Check if this is the current active edit + const isCurrentVersion = props.activeEditId + ? entry.id === props.activeEditId + : entry.value === props.initialValue && + !array + .slice(0, index) + .some((e) => e.value === props.initialValue); return (
= ({ initialValue={editorContent} spellCheckResponse={spellCheckResponse} editHistory={editHistory} + activeEditId={cell?.activeEditId} onChange={({ html }) => { // Clean spell check markup before processing const cleanedHtml = getCleanedHtml(html);