Skip to content

feat: inline edit for local diffs - #32

Merged
imfing merged 3 commits into
mainfrom
feat/inline-edit
Sep 28, 2026
Merged

imfing merged 3 commits into
mainfrom
feat/inline-edit

Conversation

@imfing

@imfing imfing commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds an inline edit experience for local (working-tree) diffs, built on the @pierre/diffs 1.4 edit-session API:

  • Edit file in the file actions menu starts an edit session on that diff (local mode only; PR/branch diffs review committed states and stay read-only)
  • The file header swaps to Cancel / Save while editing; Save accepts the session and writes the completed new-file contents to disk via a new PUT /api/worktree-file endpoint, Cancel rejects and restores the original diff
  • The diff recomputes live while typing (header stats, line numbers, word-level highlights); the file watcher picks up the save and reloads the diff

Editable: modified files, renames with changes, new (untracked) files. Not editable: deleted files and pure renames (no hunks, so no rows to edit).

Implementation notes

  • git::write_worktree_file mirrors read_worktree_file's path validation (safe relative path, canonicalized under the repo root) and only overwrites existing files; contents capped at the blob hydration limit
  • EditProvider + CodeView.onItemEditComplete wire up the 1.4 session lifecycle; edit toggles bump the item version since updateItem ignores same-version records
  • Local-mode loadDiffFiles now reads the new side from the worktree: the index-line oid of a worktree diff is computed on the fly and generally absent from the object database (blob hydration 404'd)
  • New files: patch-parsed diffs are all isPartial, and Pierre only hydrates change/rename diffs, so a new file's edit session never started. When editing starts, the item gets a copy marked complete (accurate, since a new file's patch holds every line) with a fresh cacheKey, because Pierre treats diffs with equal keys as the same target
  • Diff reloads (SSE/poll) are deferred while a session is active so a remount can't tear down the editor mid-edit; deferred reloads fire on session end
  • The n/p/m shortcuts are suppressed during an edit: the editor's input lives in shadow DOM, so the document-level handler would otherwise steal those keystrokes

Also fixes: stale diff after a file returns to HEAD

The watcher only broadcast when a touched path appeared in the current git status. A file saved back to its HEAD content drops out of status, so no refresh was sent and the page kept an empty -0 +0 entry. This predates the edit feature (saving back to HEAD from any external editor hit it too). The watcher now keeps the previous status and also broadcasts when a touched path was changed before and is clean now.

Test plan

  • pnpm test (oxlint + tsc/vite + cargo test) and cargo clippy -D warnings pass
  • New Rust tests: write_worktree_file (overwrite, no-create, traversal rejection) and a watcher regression test that fails without the fix
  • Verified end-to-end with headless Chromium against a scratch repo covering each diff shape:
    • Modified file with two hunks: edit + inserted line saved correctly, sidebar totals match git diff HEAD
    • Rename with changes: saves correctly
    • Edit back to HEAD content: file drops out of the diff after save
    • New untracked file: edits and saves
    • Pure rename: no Edit option
    • Cancel: view reverts, disk untouched

Uses the @pierre/diffs 1.4 edit-session API. Edit file from the file
actions menu starts a session (local diffs only); Save writes the new
file contents back to the worktree via PUT /api/worktree-file, Cancel
reverts. Diff reloads are deferred while a session is active, and the
n/p/m shortcuts are suppressed so they don't steal editor keystrokes.
Local-mode hydration now reads the new side from the worktree, since
the index-line oid of a worktree diff is generally not in the object
database.
…iles

- watcher: also broadcast when a touched path was changed in the previous
  status but is clean now (e.g. saved back to its HEAD content), so the
  file drops out of the diff instead of lingering as an empty entry
- edit: new-file diffs parse as partial and Pierre never hydrates them,
  so their edit session never started; mark them complete (with a fresh
  cacheKey) for the session since the patch already holds every line
- edit: hide Edit file for pure renames, which have no rows to edit
@imfing
imfing merged commit 82176c0 into main Sep 28, 2026
8 checks passed
@imfing
imfing deleted the feat/inline-edit branch September 28, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant