File Viewer: edit mode for text files (edit + save in the viewer) - #213
Merged
Conversation
Closes #212. The file-preview overlay can now edit workspace text files in place, phone-first: agent writes a file, you review it in the viewer, tweak two lines, save, tell the agent to continue. Backend (file-routes.ts, policy in src/config/file-editing.ts): - GET file-content?edit=1: read-for-edit that never truncates (a truncated buffer must never become an edit buffer), 512KB cap (413 over it), and returns the sha256 hash + detected EOL the client echoes back on save. - PUT /api/sessions/:id/file-content: edit-in-place only, with no O_CREAT anywhere in the handler. Confinement matches the read path (realpath + workspace boundary + ownership via findSessionOrFail), plus sensitive-path and attachment-guard blocklists, a .git subtree deny, and an extension allowlist (svg and env deliberately excluded). Optimistic concurrency via baseHash: mismatch is a 409 unless force. Writes are wx-temp + fchmod + fsync + rename, closing the validate-then-write TOCTOU window. - Corruption guards: NUL sniff + UTF-8 round-trip compare (refuses binary and latin-1), and server-side EOL re-application so a textarea's LF normalization cannot rewrite every line of a CRLF file. - Plain reads gain an additive editable flag the UI keys the button off. Frontend (panels-ui.js + overlay markup/styles): - Edit button on editable text previews; textarea editor with Save/Cancel, dirty indicator, discard-confirm on cancel/close, and a conflict dialog that offers overwrite (force) when the file changed on disk mid-edit. - Phone: full-bleed window sized by --app-height so the editor and Save bar track the OS keyboard; 16px editor font (iOS zoom guard); no autofocus. - zh-CN strings for the new chrome. Tests: pure policy unit tests plus a route suite that deliberately does NOT mock node:fs. It runs against a real temp workspace so symlink escapes, write-through of in-workspace symlinks, mode preservation, CRLF round-trip, 409/force, and the no-create property are exercised for real. Also verified end to end on an isolated beta instance: 39-check curl matrix, Playwright desktop flow (real clicks and typing, bytes asserted on disk, live conflict with an external rewrite), and a 393px phone profile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #212.
The file-preview overlay can now edit workspace text files in place, phone-first: the agent writes a file, you review it in the viewer, tweak two lines, save, and tell the agent to continue. Design doc:
docs/file-viewer-edit-plan.md; invariants:docs/architecture-invariants.md#file-viewer-edit-mode.What's in here
Backend (
src/web/routes/file-routes.ts, policy insrc/config/file-editing.ts)GET /api/sessions/:id/file-content?edit=1: read-for-edit that never truncates (the plain preview truncates to 500 lines; saving such a buffer would silently delete the rest, so the editor always loads through this path), 512KB cap with a 413 over it, returns the sha256hash+ detectedeolthe client echoes back.PUT /api/sessions/:id/file-content: edit-in-place only. There is no O_CREAT anywhere in the handler, which makes "never create, never delete" structural rather than a convention.findSessionOrFail) and adds: sensitive-path + attachment-guard blocklists, a.git/subtree deny (hooks are code execution), and an extension allowlist (svgandenvdeliberately excluded in v1).baseHashis a 409CONFLICTunlessforce:true.wxtemp +fchmod+fsync+renamein the target directory, closing the validate-then-write TOCTOU window;wxcannot follow a pre-existing symlink andrename()replaces a symlink final component instead of following it. In-workspace symlinks write through to their target (matching what tapping the file shows).Frontend (
panels-ui.js, overlay markup + styles)editable.--app-height(tracks the OS keyboard via the existing KeyboardHandler), 16px editor font (iOS zoom guard), no autofocus.Testing
test/file-editing-policy.test.ts: 13 pure policy tests.test/routes/file-write-routes.test.ts: 27 route tests that deliberately do not mocknode:fs; they run against a real temp workspace so symlink escape, write-through, mode preservation, CRLF round-trip, 409/force, multi-user scoping, and the no-create property are exercised for real.npm run test:cisweep: 3942 passed, 0 failed..env/.git, binary/latin-1, size caps, conflict/force, CRLF bytes, file mode), a Playwright desktop flow (real clicks and typing through the File Viewer, bytes asserted on disk, live conflict triggered by rewriting the file externally mid-edit), and a 393px phone profile (full-bleed layout, 16px font, reachable Save bar, typed save verified on disk).Out of scope per the issue: creation/deletion/rename, syntax highlighting, and editing through Docker/remote overlays (Docker cases work naturally via the bind mount; remote SSH cases fail closed with the same 404 the read routes give today).