Edit history has two independent layers — local (IndexedDB) and remote (Drive) — that track file changes separately.
| Layer | Storage | How Diffs Are Created | Lifetime |
|---|---|---|---|
| Local | IndexedDB editHistory store |
Client auto-save computes diffs from base to current content | Per-file: cleared on Push (including reverted files); all cleared on Full Pull |
| Remote | Drive .history.json per file |
Server computes diff between old Drive content and newly pushed content | Retained per edit history settings |
The two layers are independent: local diffs are not uploaded to Drive. On Push, the server independently computes its own diff from the Drive-side old content vs the pushed new content and appends it to .history.json.
Each file has one CachedEditHistoryEntry with a diffs[] array. Each array element represents one diff session (commit point).
- Read old content from IndexedDB cache (before cache update)
- If last diff exists: reverse-apply it to old content to reconstruct the base
- Compute cumulative diff from base to new content
- Overwrite the last
diffs[]entry (same session updates accumulate into one diff)
If reverse-apply fails (e.g. patch mismatch due to external content change), a commit boundary is inserted to seal the corrupted diff, and a new session starts from the current old cache content.
Because the last diff is continuously overwritten during a session, a single active session always has exactly one non-empty diff entry representing base → current content.
Revert detection: When the cumulative diff produces zero additions and zero deletions (content matches session base), saveLocalEdit cleans up the stale diff entry. If no meaningful diffs remain, the editHistory entry is deleted entirely and "reverted" is returned, causing the file tree indicator to switch from yellow (modified) to green (cached).
Note:
saveLocalEditis also called from outside auto-save: AI chat file updates (ChatPanel), workflow execution file updates (useWorkflowExecution), file imports via drag-and-drop (DriveFileTree), and temp file merges (TempFilesDialog). All callers invokesaveLocalEditbeforesetCachedFile.
Explicit save events insert a commit boundary (empty diff entry) into diffs[]. This causes the next auto-save to append a new diff rather than overwriting the previous one, starting a new session.
addCommitBoundary(fileId) checks if the last diff is non-empty and appends an empty boundary if so.
Triggered by:
- File open, file reload, after pull updates editor content (
useFileWithCache) - Pull (per downloaded file), resolve conflict (remote), Full Pull (
useSync) - Temp diff accept (
MainViewer,WorkflowEditor) - Chat file update (
ChatPanel), Workflow execution file update (useWorkflowExecution) restoreToHistoryEntryadds boundaries directly around the restore diff entry
Since saveLocalEdit always replaces the last diffs[] entry, a commit boundary is consumed by the next auto-save. The three states that actually occur:
Case 1: Single session (no commits)
[0] { diff: "base → current" } ← continuously updated
Case 2: After commit + editing in session 2
[0] { diff: "base → end_of_session1" } ← sealed
[1] { diff: "end_of_session1 → current" } ← replaced the boundary, continuously updated
Case 3: After commit, no further edits yet
[0] { diff: "base → end_of_session1" } ← sealed
[1] { diff: "" } ← boundary (replaced on next edit)
- The last entry is always the one being updated by auto-save
- Earlier entries are sealed and never modified
- Commit boundaries are transient — they exist only between sessions and are replaced by the next auto-save
- Only stores cache (latest content) + diffs (no full base content copy)
- Base content is reconstructed via reverse-apply when needed
- Reverse-apply: swap
+/-lines and hunk header counts, then apply patch
Remote edit history is computed entirely on the server, independent of local edit history. When a file is updated on Drive, the server:
- Reads the old file content from Drive before overwriting
- Computes diff (old Drive content → new content)
- Appends the diff entry to the file's
.history.jsonon Drive
This happens in two paths:
- Push (
api.sync.tsxpushFilesaction): saves history in background (fire-and-forget, best-effort) - Direct file update (
api.drive.files.tsxupdateaction): saves history inline (awaited, best-effort)
After Push, local edit history in IndexedDB is cleared for the pushed files (the local diffs are no longer needed since the cache now matches Drive) and for reverted files (files whose content was edited then reverted to the synced state — detected by hasNetContentChange). Files that failed to push retain their local edit history.
Remote entries include metadata: id, timestamp, source (workflow/propose_edit/manual/auto), optional workflowName and model.
Right-click a file in the tree → "History" to open the Edit History modal.
The modal shows:
- Local entries (IndexedDB) by default — editing session diffs with Restore button
- Remote entries (Drive) on demand — click "Show remote history" to load past diffs from Drive
Each entry displays: timestamp, origin badge (local/remote), addition/deletion stats, and an expandable diff view. Non-newest entries also have a Copy (Save As) button that reconstructs the content at that point and saves it as a new Drive file.
Restore reverts a file to the state at the selected history entry — i.e., the state after that entry's change was applied. It reverse-applies diffs newer than the selected entry.
The caller (EditHistoryModal.handleRestore) performs steps 1-2. restoreToHistoryEntry (steps 3-4) computes the restored content and updates edit history. The caller performs steps 5-6.
- Read current content from IndexedDB cache (caller)
- Compute which diffs are newer than the target entry and pass them to
restoreToHistoryEntry(caller) - For each non-empty diff provided (the target itself is NOT reversed):
- Local diffs: always reverse-apply (swap
+/-lines, invert hunk headers, apply patch) - Remote diffs: try reverse-apply first; if it fails (content is at the OLD side, not yet pulled), skip
- Record the restore as a new history entry:
diff(current → restored)and add commit boundaries
- Local diffs: always reverse-apply (swap
- Return the restored content
- Update IndexedDB cache with restored content (caller)
- Dispatch
file-restoredevent to update the editor (caller)
When there is only one non-empty diff (active session, no commits):
diffs: [{ diff: "base → current" }]
cache: current
Restore (filteredIndex=0):
no diffs newer than index 0 → nothing to reverse
result: "current" (no change)
Restoring to the only entry is a no-op since the file is already at that state.
diffs: [
{ diff: "base → v1" }, ← index 0 (sealed)
{ diff: "v1 → current" }, ← index 1 (active)
]
cache: current
Restore to index 0 (filteredIndex=0):
reverse-apply diff[1] on "current" → "v1"
result: "v1"
(the state at the end of session 0, i.e. after "base → v1" was applied)
Restore to index 1 (filteredIndex=1):
no diffs newer than index 1 → nothing to reverse
result: "current" (no change)
- If
reverseApplyDifffails (patch mismatch), restore returns null and nothing changes - After restore, the editor immediately reflects the restored content; the change is local-only until the next Push
Located in Settings → Sync → Edit History:
| Action | Description |
|---|---|
| Prune | Remove old edit history entries to free Drive storage |
| Stats | View edit history storage usage and entry counts |
Retention settings (per user):
maxEntriesPerFile: Maximum entries per file (0 = unlimited)maxAgeInDays: Maximum age in days (0 = unlimited)
Diff settings:
contextLines: Number of context lines in remote diffs (default: 3). Local diffs use a hardcoded value of 3.
| File | Role |
|---|---|
app/services/edit-history-local.ts |
Client-side edit history: auto-save (saveLocalEdit), commit boundary (addCommitBoundary), restore (restoreToHistoryEntry), reverse-apply diff, net change check (hasNetContentChange) |
app/services/edit-history.server.ts |
Server-side edit history: save to Drive .history.json on Push, load history, retention policy |
app/services/indexeddb-cache.ts |
IndexedDB stores: editHistory CRUD, CachedEditHistoryEntry / EditHistoryDiff types |
app/hooks/useFileWithCache.ts |
Cache-first file reads, auto-save integration (saveToCache calls saveLocalEdit), file-restored event handler |
app/components/ide/EditHistoryModal.tsx |
History modal UI: display local/remote entries, restore handler, clear remote history |
app/components/shared/DiffView.tsx |
Unified diff visualization component |
app/routes/api.settings.edit-history.tsx |
API: GET remote history, DELETE remote history |
app/routes/api.settings.edit-history-stats.tsx |
API: GET history stats |
app/routes/api.settings.edit-history-prune.tsx |
API: POST prune old history |