Skip to content

File Viewer: edit mode for text files (edit + save in the viewer) - #213

Merged
Ark0N merged 1 commit into
masterfrom
feat/file-viewer-edit-mode
Aug 5, 2026
Merged

File Viewer: edit mode for text files (edit + save in the viewer)#213
Ark0N merged 1 commit into
masterfrom
feat/file-viewer-edit-mode

Conversation

@Ark0N

@Ark0N Ark0N commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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 in src/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 sha256 hash + detected eol the 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.
  • Confinement inherits the read path (realpath + workspace boundary, ownership via findSessionOrFail) and adds: sensitive-path + attachment-guard blocklists, a .git/ subtree deny (hooks are code execution), and an extension allowlist (svg and env deliberately excluded in v1).
  • Optimistic concurrency by content hash (not mtime): stale baseHash is a 409 CONFLICT unless force:true.
  • Atomic writes: wx temp + fchmod + fsync + rename in the target directory, closing the validate-then-write TOCTOU window; wx cannot follow a pre-existing symlink and rename() replaces a symlink final component instead of following it. In-workspace symlinks write through to their target (matching what tapping the file shows).
  • Corruption guards: NUL sniff + UTF-8 round-trip compare (refuses binary and latin-1, whose U+FFFD decode would destroy the original bytes on save), and server-side EOL re-application so a textarea's LF normalization cannot turn a two-line edit of a CRLF file into a whole-file diff.

Frontend (panels-ui.js, overlay markup + styles)

  • Edit (pencil) button on text previews, shown only when the server advertises editable.
  • Textarea editor with Save/Cancel, dirty indicator, discard-confirm on cancel/close, and a conflict dialog offering overwrite when the file changed on disk mid-edit.
  • Phone: full-bleed window sized by --app-height (tracks the OS keyboard via the existing KeyboardHandler), 16px editor font (iOS zoom guard), no autofocus.
  • zh-CN strings for the new chrome.

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 mock node: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.
  • Full npm run test:ci sweep: 3942 passed, 0 failed.
  • End-to-end on an isolated beta instance: a 39-check curl matrix (traversal, symlink escape, .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).

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>
@Ark0N
Ark0N merged commit cd5a101 into master Aug 5, 2026
2 checks passed
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.

File Viewer: edit mode for text files (edit + save in the viewer)

2 participants