Skip to content

feat(files): edit a text file from the file viewer, opt-in (#771) - #3064

Merged
devswha merged 8 commits into
mainfrom
feat/text-file-save
Nov 5, 2026
Merged

devswha merged 8 commits into
mainfrom
feat/text-file-save

Conversation

@devswha

@devswha devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Change

Fixes #771. Settings > Chat > File viewer > Edit text files (a device setting, off by default)
adds Edit file to the file viewer for a text file in the pane's folder. Off, the viewer is
exactly as it is today. This is the first route that writes a file, so the server side is narrow
and was design-reviewed (Codex, adversarial) before any code.

  • UI (TextFileEditor.tsx): the file's whole text in a plain mono text box under one bar.
    Save asks once more in the same bar ("Save changes to NAME?", Keep editing / Save); Cancel
    leaves without writing. While the text differs from the file, Escape, a press on the scrim and the
    sheet swipe do not close the viewer. HTML and SVG are only ever text in the box, never rendered.
    A file that changed meanwhile follows Zed's "changed on disk" handling: the typed text stays in
    the box to copy and Reload file shows the file as it is now, but there is no Overwrite, so a
    save never replaces a change the editor has not seen. A refusal says why (outside the pane's
    folder, too large, not UTF-8, mixed line endings, read-only, another PC that can't save yet).
  • API (shared/files.ts, server/file-edit.ts): GET /api/fs/text?pane_id=&path= answers
    { path, text, revision, line_endings, size, modified }, with revision the SHA-256 of the
    bytes; PUT /api/fs/text (application/json only) takes { pane_id, path, revision, text } and
    answers the new revision, or 409 stale_revision with the file's current revision and nothing
    written.
  • Who may (textEditAllowed, checked in dispatch.ts before the route and before the PC
    proxy): a device that drives this PC (paired device, token, this PC's own browser, a relay phone
    with drive). Never a watching device, a Portal visitor (even one given drive) or an open LAN
    window that signed in as nobody. On another PC, that PC's own bridge token is accepted for its
    own route only, never for a PC behind it. Mutations keep the existing same-origin gate, and a
    text/plain or form body is refused before it is read.
  • Which files: an existing regular file whose real path (links followed) lies below the pane
    folder's real path, never in .git and never when the pane's folder is a filesystem root; owned
    by this server's user, with one link, at most 1 MiB, UTF-8 without NUL (the whole file is checked,
    not a sniffed prefix) and one kind of line ending (CRLF is given back as LF and written back as
    CRLF; a BOM round-trips). The request body is read as a stream capped at 8 MiB, and the text must
    have no CR, NUL or unpaired surrogate.
  • How it is written: to .NAME.ovis-<hex>.tmp beside the file (O_EXCL|O_NOFOLLOW), given
    the file's mode and group (fchmod, fchown; a failure refuses the save), fsynced and renamed
    over it, so a reader sees the old text or the new. The original must open for writing first, so a
    read-only file is refused rather than replaced. The revision is checked before the temp file is
    written and again right before the rename.
  • Folder swaps: on Linux every step after the file was proven to lie in the pane's folder goes
    through an O_DIRECTORY|O_NOFOLLOW descriptor of its folder (/proc/self/fd/N/<name>), whose
    kernel path must lie in the pane's folder right before the rename. A folder on the way swapped for
    a link cannot send the rename anywhere else, and a folder moved out of the pane's folder gets
    nothing. On macOS the existing ownedFolders proof (every folder from the pane's folder down is
    this user's own and writable by nobody else) runs again right before the rename, as for rendered
    pages' media. Windows and other platforms answer 501 unsupported_platform for now.
  • Another PC: fs/text joins the PC proxy's routes. This server checks the caller's role,
    and the other PC applies the same file rules itself.

What is not guaranteed: another program writing the file in the moment between the last check and
the rename is not detected (no lock), the same as editors that do not lock a file. Node has no
renameat, which is why the Linux path goes through /proc/self/fd.

Design reference: Zed's handling of a buffer whose file changed on disk (keep the buffer, offer a
reload), without its Overwrite. The Codex and Claude apps, Paseo and T3 Code do not edit files in
their viewers.

Design review (before code)

Codex (Terra, xhigh) attacked the first design. Each finding and what changed:

Finding Resolution
HIGH: a parent folder swapped for a link between the last check and the rename Linux: folder-descriptor-anchored temp, re-check and rename; macOS: owned-folders proof right before the rename; other platforms: 501
HIGH: hash-then-rename is not a true compare-and-swap the guarantee is narrowed and documented (above, and in shared/files.ts); a second hash check runs right before the rename
MEDIUM: the proxy's caller identity, an SSH PC's bridge token role checked on this server before forwarding; the bridge token is accepted on the PC's own route only
MEDIUM: rename changes permissions, breaks hard links, owner must open for writing first; hard-linked and other-owner files refused; mode and group kept or the save is refused
MEDIUM: line endings, surrogates mixed or lone-CR files refused; CRLF restored; unpaired surrogates refused
MEDIUM: size bounds file read to 1 MiB + 1 byte from the descriptor; streaming body cap; whole file validated
LOW: root and .git filesystem-root pane folder refused; .git anywhere refused (case-folded on macOS/Windows); containment by relative()
Open: the open LAN window denied

Screenshots

Owned herdr pane, real server. Editing with changes, the save confirm, a conflict, a read-only
file, and the setting (off by default).

Dark Light
1280 editing
1280 conflict
1280 read-only
390 editing
390 confirm
390 conflict
390 setting

Validation

  • New server/file-edit.test.ts (21 tests): open and save keeping mode with nothing left beside
    it; CRLF and BOM round trip; stale revision refused with nothing written; a write by another
    program between the checks refused; ../, absolute, linked file, linked parent folder, .git
    and a filesystem-root pane all refused for GET and PUT; a folder swapped for a link right before
    the rename (textSaveHooks.beforeRename) cannot send the save outside, and a folder moved out
    gets nothing; binary past the sniffed prefix, non-UTF-8, mixed and lone-CR endings, oversized,
    read-only and hard-linked files refused; content type, other origin, CR, lone surrogate, bad
    revision, 8 MiB body, missing pane, method; 501 off Linux/macOS; relay watch refused, relay drive
    served, open LAN window refused; the role table. Each guard was removed once to see its test go
    red (re-check before rename, folder anchor, Portal rule, containment check), then restored.
  • server/machine-proxy.test.ts: a save is forwarded with its body, content type and the PC's
    credential, the PC's 409 comes back, and a watching device is refused before the PC is asked.
  • New src/components/TextFileEditor.test.tsx: nothing is sent until the second Save, which names
    the revision opened; Cancel sends nothing; a conflict keeps the typed text and Reload file shows
    the PC's; a refusal shows its reason and no text box.
  • New scripts/text-file-edit-regression.ts (added to scripts/ci-browser.sh): at 1280 and 390,
    light and dark, on an owned herdr pane: off by default (no Edit; the Settings switch off); on,
    Escape keeps unsaved changes, the first Save writes nothing and the second writes the file; Cancel
    writes nothing; a file changed on the PC is not overwritten and Reload file shows it; a read-only
    file says it can't be written. 16/16 PASS locally.
  • bun run check fast: passed at c18f2f5 (origin/main 86b7951 merged in; workflow syntax, generated files, typecheck, build, unit tests: 12348 pass, 0 fail).
  • File viewer and Settings browser regressions in the self-hosted CI runner image (herdr-ci-runner:local,
    scripts/ci-browser-parallel.ts, 4 workers): 8/8 passed at baa07f1 (before that merge, which
    touched only settings.legacy-fixture.ts): text-file-edit, file-viewer, file-viewer-mobile,
    files-panel-demo, file-text-size-demo, files-prompt, deep-link-demo, settings-pages-demo.
  • Skipped locally: bun run check integration and the full browser lane (CI runs them); no
    Windows or macOS run (Windows answers 501 by design; the macOS branch reuses ownedFolders).

Settings > Chat > File viewer > Edit text files (off by default) adds Edit to
the viewer for a text file in the pane's folder. Save asks once more and is
refused when the file changed since it was opened.

GET/PUT /api/fs/text (server/file-edit.ts): only an existing UTF-8 file under
the pane folder's real path (never .git, never a filesystem root), owned by
this user, with one link and one kind of line ending, up to 1 MiB; written to
a temp file beside it with its mode and group and renamed over it. On Linux
every step after the proof goes through an O_DIRECTORY|O_NOFOLLOW descriptor
of its folder (/proc/self/fd/N), on macOS the owned-folders proof runs again
right before the rename; other platforms answer 501. The SHA-256 revision is
checked before the temp file is written and again right before the rename.
Only a device that drives the PC (not watch, Portal or an open LAN window);
the PC proxy forwards fs/text and the other PC applies the same rules.
# Conflicts:
#	src/lib/settings.legacy-fixture.ts
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2d8581e and 95ef324.

Files selected for processing: 34

  • DESIGN.md
  • changelog.d/edit-text-files.md
  • docs/development.md
  • docs/guide.md
  • scripts/ci-browser.sh
  • scripts/text-file-edit-regression.ts
  • server/dispatch.ts
  • server/file-attributes.ts
  • server/file-edit.test.ts
  • server/file-edit.ts
  • server/file-pool.ts
  • server/file-routes.ts
  • server/file-tasks.ts
  • server/file-view.ts
  • server/machine-api.ts
  • server/machine-proxy.test.ts
  • shared/files.ts
  • src/App.tsx
  • src/components/FileViewer.css
  • src/components/FileViewer.tsx
  • src/components/SettingsDialog.tsx
  • src/components/TextFileEditor.test.tsx
  • src/components/TextFileEditor.tsx
  • src/lib/api.ts
  • src/lib/i18n.test.ts
  • src/lib/i18n/files.ja.ts
  • src/lib/i18n/files.ko.ts
  • src/lib/i18n/files.zh.ts
  • src/lib/i18n/settings.ja.ts
  • src/lib/i18n/settings.ko.ts
  • src/lib/i18n/settings.zh.ts
  • src/lib/machineContext.tsx
  • src/lib/settings.legacy-fixture.ts
  • src/lib/settings.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

…hread (#771)

Review round on the text file save:
- A draft typed and not saved is kept above the viewer (by PC and file), so
  Close, Back, moving the viewer beside the pane or opening another file no
  longer loses it; reopening the file opens its editor with it. Only Save and
  Cancel drop it, and a reload or closed tab asks first (beforeunload).
- The new file gets the original's extended attributes and access list
  (file-attributes.ts, through libc: Linux posix ACLs are an attribute, macOS
  ACLs via acl_get_fd_np); a save whose access list cannot be kept is refused
  (403 access_list) instead of widening who may read the file.
- The mode is set after the write, which cleared set-id bits given before it.
- Reads and saves run on a file thread (file-pool.ts text-read/text-save, the
  long deadline), one save at a time, so a slow disk no longer holds the
  server's thread.
- The temp file has a short name of its own, so a long file name saves.
- A file or folder removed during a save is a 409, not a 500.
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review round 1 (omo BLOCK, Codex Terra xhigh) at c18f2f5, and what changed

Fixed in fa71d86:

Finding Source Resolution
Moving the viewer beside the pane (or Close, Back, opening another file) discards an unsaved draft. Reproduced. omo (MEDIUM), Codex (MEDIUM) A draft is kept above the viewer by PC and file. Reopening the file opens its editor with the draft; only Save and Cancel drop it, and a reload or closed tab asks first (beforeunload). A component test (unmount and remount keeps the text without a second read) and a browser step at 1280 (Show beside the pane while editing) cover it.
A save drops ACLs and extended attributes, and the ACL loss widens group read. Reproduced. omo (MEDIUM), Codex (MEDIUM) server/file-attributes.ts copies every extended attribute and the access list to the new file through libc (Linux: flistxattr/fgetxattr/fsetxattr, where a posix ACL is system.posix_acl_access; macOS: the same calls plus acl_get_fd_np/acl_set_fd_np). An access list that cannot be kept refuses the save (403 access_list). Tested with a user.* attribute, and with setfacl where it is installed.
Mode 4755 becomes 0755: the write clears set-id bits given before it. Reproduced. Codex (MEDIUM) The mode is set after the write (and after the group). Test: 2755 stays 2755 (red with the old order).
Synchronous filesystem calls on the server's thread. Codex (MEDIUM) Read and save run on a file thread (file-pool.ts ops text-read/text-save, the 15 s long deadline), one save at a time. A save that outlives its deadline answers 503 and may still land; the next save's revision check sees it. One test runs on the real threads.
A ~240-byte file name cannot be saved (ENAMETOOLONG). Reproduced. omo (LOW), Codex (LOW) The temp file is .ovis-edit-<hex>.tmp, whatever the file's name. Tested with a 243-byte name.
A folder deleted during a save gives a 500. omo (note) A removed file or folder (ENOENT, or /proc/self/fd/N reading (deleted)) answers 409 stale_revision. Tested.

Not changed, with the reason:

  • Codex HIGH: the folder moved out of the pane folder after the last check, before the rename.
    Not reproduced (it needs a move in the microseconds between the final /proc/self/fd/N
    check and rename). The rename goes through the folder's descriptor, so it can only replace the
    entry of the opened file in its own folder, wherever that folder now is. It never writes to
    another file or folder, and a move before the last check is refused (tested). On macOS, a swap in
    that moment needs a process of the same user (ownedFolders), the same stance as rendered
    pages' media. I rate it LOW.
  • Codex MEDIUM: a different inode with identical bytes passes the revision check. By design:
    the revision is the content (like a strong ETag). Many agents and editors save with
    write-and-rename, which makes a new inode for every save, so tying the revision to the inode
    would report a conflict when nothing changed. The replacement file is checked afresh before the
    save (owner, one link, writable), and the bytes the user edited are exactly what it holds.
  • omo note: a PC on an older version answers 404 to /api/fs/text, shown as "The file could not
    be saved."
    Left as is: there is no version check for this route, and the message is not wrong.

Re-review of the text file save:
- The new file loses any attribute it took from its folder that the file did
  not have, so a folder's default access list no longer opens a saved file to
  more users (Linux fremovexattr; macOS an empty ACL when the file had none).
- A failed attribute or access list read is no longer taken for "none": only
  ENOTSUP/EOPNOTSUPP (and ENOENT for a macOS ACL) mean none; otherwise the save
  is refused (errno read through libc).
- One save at a time across every file thread, for as long as it really runs:
  a lock in shared memory (withSaveLock) replaces the request queue, which let
  the next save in when an earlier one only passed its deadline.
- A save to a file removed since it was opened is a 409 conflict, not a 404.
- Editor: while any draft is kept the page asks before a reload, not only
  while its editor is open; a save answering after its editor was closed
  removes only the draft it wrote.
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review round 2 (omo BLOCK and Codex Terra xhigh at 1af02b2), fixed in e1bf1a1

Finding Source Resolution
The new file inherits the folder's default access list when the original had none, so user:nobody:r-- appears on the saved file. Reproduced. omo HIGH, Codex HIGH copyFileAttributes removes every attribute the new file has that the original did not have: on Linux fremovexattr, where failing to remove system.posix_acl_access refuses the save; on macOS an empty ACL is set when the original had none. Tested with setfacl -d on the folder and setfacl -b on the file (red without the removal).
A save that passed its deadline (503) let the next save in while its thread still ran, so two saves could overlap. Reproduced by Codex with a delayed worker. Codex HIGH, omo MEDIUM withSaveLock uses a lock in shared memory that every file thread holds for the whole save, so a save the server stopped waiting for still holds it until it really ends. A save that waits past 10 s is refused with 503 file_busy. It replaces the request queue. Tested (red without the lock).
A failed attribute or ACL read was taken for "none" (copyFileAttributes(-1, fd) returned true). Codex MEDIUM The calling thread's errno is read through libc. Only ENOTSUP/EOPNOTSUPP means none (plus ENOENT for a macOS ACL), and a listed ACL that cannot be read refuses the save. Tested.
Once the viewer is closed, a reload loses a kept draft without a warning. Codex MEDIUM, omo LOW The beforeunload prompt is held while any draft is kept, not only while the editor is open. Tested.
A save that answers after its editor was closed deletes a draft typed since then. Codex MEDIUM, omo LOW A late answer removes only the draft it wrote, and tells an unmounted editor nothing. Tested (red without the check).
A file removed before the save request answers 404. Codex LOW 409 stale_revision, so the editor offers to reload. Tested.

Left, with the reason:

  • A kept draft can't be reached once its file is gone, grows past 1 MB or stops being text
    (Codex MEDIUM, omo LOW, not reproduced). The text stays in memory until a reload and is not shown
    anywhere. It needs a new recovery view outside the editor, so it is left out of this PR.
  • musl systems (Alpine): libc.so.6 does not load there, so every save is refused with
    access_list. That is a refusal, not a silent loss (omo note).

…ft (#771)

Third review of the text file save:
- A save runs on the server's own thread again, synchronously; reads stay on
  a file thread. The cross-thread lock of the last round let a save that
  waited past its deadline write after its 503, held the lock for good when a
  worker died, and filled the file threads with waiting saves. Without it two
  saves never interleave and no save outlives its answer. A save is at most
  1 MiB to the disk the file was just read from.
- A save answering late removes only the draft entry it submitted, so an
  editor opened again meanwhile keeps its draft (and the reload prompt).
- The Linux-specific save tests run on Linux only.
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review round 3 (omo CLEAR and Codex Terra xhigh at 0cbf95b), fixed in 593662a

Codex reproduced problems in the cross-thread save lock that round 2 added, so this round removes it
instead of adding more:

Finding Source Resolution
A save that waited for the lock past its deadline wrote after its 503. Reproduced. Codex HIGH A save runs on the server's own thread again, synchronously; reads stay on a file thread. Two saves never interleave, and no save goes on after its answer.
A worker that died while holding the lock blocked every later save. Reproduced. Codex MEDIUM No lock any more.
Saves waiting for the lock filled the file threads. Reproduced. Codex MEDIUM, omo LOW No save runs on a file thread.
A late save answer removed the draft an editor opened again meanwhile had adopted. Reproduced. Codex MEDIUM A late answer removes only the draft entry it submitted (by identity), and a reopened editor keeps an entry of its own. Tested (red with the old text comparison).
The save tests assume Linux. omo LOW The Linux-specific suite runs on Linux only (no CI job runs it elsewhere).

The trade-off: a save is at most 1 MiB written to the disk the file was just read from, and a disk
that stalls during it holds the server's thread for that long (the round 1 Codex MEDIUM). This is
documented in server/file-edit.ts.

Left, with the reason:

  • macOS: a folder with an access list that lets another local user rename entries (Codex HIGH
    in omo's CLEAR review, not reproduced: it needs a second macOS account with an ACL grant and a
    timed swap). ownedFolders checks the owner and mode bits, as it already does for rendered pages'
    media on main. Refusing folders with extended ACLs there would change that shared check, so it
    belongs in its own change.
  • A kept draft can't be reached once its file is gone or no longer editable (omo MEDIUM, not
    reproduced). As in round 2.

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Final review at 95ef324: omo CLEAR (with Codex Terra)

No HIGH. What remains, none reproduced and none blocking:

  • MEDIUM: the folder moved out of the pane's folder in the moment between the last check and the
    rename
    (Linux). This is the round 1 Codex HIGH, now rated MEDIUM. The rename goes through the
    folder's descriptor, so it can only replace the opened file's own entry, wherever its folder now
    is. Doing it takes a process of the same user, which could write the file directly anyway.
  • LOW: Portal wording. A request that comes through Portal but also carries the app token or a
    paired driving device's credential counts as that credential. The rule is "never a Portal sign-in
    alone"; whoever holds those credentials drives the PC already.
  • LOW: a kept draft can't be reached once its file is gone or past 1 MB (copy or discard).
  • LOW: musl Linux (Alpine). libc.so.6 does not load there, so saves are refused with
    access_list. Errno is read in a second FFI call after the failed one.

CodeRabbit did not review this PR (its review limit was reached when the PR opened). The reviews
were omo and Codex Terra (xhigh): a design review before code, then three rounds on the PR, whose
findings and fixes are in the comments above.

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Merge note: all GitHub checks green at this head; omo CLEAR at this exact head; Codex Terra xhigh adversarial design review before code, then three omo + Codex rounds (findings and fixes in the PR comments; remaining notes unreproduced MEDIUM/LOW only); threads resolved; CodeRabbit was rate-limited, so the omo + Codex reviews stand in. First file write route: opt-in and off by default; drive role only (not watch, Portal or the open LAN listener); the pane folder's real path only (no .git, no filesystem root); UTF-8 text up to 1 MiB, single-link files the user owns; temp + rename keeping mode, group, xattrs and ACL (refused if an ACL cannot be kept); the Linux rename goes through an O_DIRECTORY|O_NOFOLLOW folder descriptor proven inside the pane folder; Windows refused in this version. Fixes #771.

@devswha
devswha merged commit 5adc250 into main Nov 5, 2026
21 checks passed
@devswha
devswha deleted the feat/text-file-save branch November 5, 2026 21:20
devswha added a commit that referenced this pull request Nov 5, 2026
) (#3073)

## What changes

Part of #1196. The first slice of the Markdown documents panel: **list,
search and open**. The safe
write route of #771 (#3064) merged while this was in progress, and a
document opened from the new
tab opens in that same file viewer: with **Settings > Edit text files**
on, it can be edited and saved
there, with #3064's revision check. Creating and deleting a document are
not in this PR (follow-ups
on #1196).

- **Settings > Chat > Documents in Files** (a device setting, **off by
default**). Off, Files is
  exactly as it is today: three tabs, no new request.
- On, Files gets a fourth tab, **Documents**: the Markdown files (`.md`,
`.markdown`) under the
pane's folder, **newest first**, each with the folder it is in and when
it changed. A **Filter
documents** field narrows them by path (Escape clears it before it
closes Files), a Refresh button
reads the list again (nothing polls), and a row opens the file in the
existing file viewer, which
renders it with its outline. It works in the dialog, in Files beside the
pane, and on a phone's
  sheet.
- **Server:** new read-only route `GET /api/pane/docs?pane_id=` ->
`PaneDocuments`
(`shared/files.ts`). In a repository it asks git afresh (`git ls-files
-co --exclude-standard`,
the same safe invocation as @-mentions), so ignored build output and
`node_modules` stay out and
Refresh sees a file made a moment ago. Outside a repository, it walks
with the same caps but
counts only Markdown files, so a folder of other files cannot crowd a
note out. Every file found
is statted and the newest 500 kept. The reads run on a file thread (a
new `documents` task),
stop after **2 s** (a fixed bound), and count as a **scan** in the file
pool, so at most two
threads run them and file routes keep the rest. The request's abort
reaches the task.
`truncated` says when a cap or the time limit left files out; the tab
then says "Some Markdown
  files here are not listed." and never claims the newest are shown.
- **Access:** the route is gated like `/api/pane/files`. It is refused
(403 `read_only`) to a
watching device, on this PC and through another PC's route. It is
forwarded to other PCs
(`MACHINE_PROXY_PATH`). On a herdr-core machine's own server, whose
files are on another
  machine, it answers 501 `unsupported`.
- **Phone layout:** four tabs do not fit beside the title and Close at
390px (66px over), or in
the narrowest docked panel (320px). There, and only while the fourth tab
is shown, the tabs take
the row under the title. With three tabs (the setting off), the header
is unchanged.
- An older PC that does not know the route gets its own message ("This
PC's bridge cannot list
  documents yet.").

No new control is visible by default: the tab exists only behind its
setting, and the composer is
not touched.

Design reference: Paseo's `paseo-markdown-viewer` plugin (a read-only
Markdown list beside the
agent that opens each file in a reader) and the Codex app's side panel,
which opens a `.md` file
next to Review. This slice follows Paseo's read-only list, inside Files,
and leaves editing to
#771. @vibewriter-hand offered to build the editing slice on #771's
write API: this PR adds no
editor and no write route, so create, save-a-new-note and delete remain
open for that slice.

## Screenshots

Demo fixtures, setting on
(`scripts/files-documents-demo-regression.ts`).

| | Light | Dark |
|---|---|---|
| 1280, dialog | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-1280-light.png"
width="320"> | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-1280-dark.png"
width="320"> |
| 1280, beside the pane at its narrowest (320px) | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-panel-1280-light.png"
width="320"> | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-panel-1280-dark.png"
width="320"> |
| 390, sheet (tabs on their own row) | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-390-light.png"
width="320"> | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-390-dark.png"
width="320"> |
| 1280, a row opened beside the pane | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-file-panel-1280-light.png"
width="320"> | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-file-panel-1280-dark.png"
width="320"> |
| 390, the setting (off by default) | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-setting-390-light.png"
width="320"> | <img
src="https://raw.githubusercontent.com/devswha/ovis/595c88a192163359a025cfb50f1e31d3a3b8e1da/files-documents-setting-390-dark.png"
width="320"> |

## Validation

- Failing first: the new rows in `server/device-access.contract.test.ts`
(a watching device refused
on this PC, its alias and another PC's route; a driving one answered),
`server/machine-proxy.test.ts`
(never forwarded for a watching device), `server/hc-machines.test.ts`
(501 on a herdr-core
machine's own server) and the `paneDocuments` cases in
`server/files.test.ts` (newest first with
sizes and times, skipped folders and git-ignored files left out, the cap
and `truncated`) failed
on the base (2d8581e) and pass now. New
`src/components/MarkdownDocuments.test.tsx` (server order,
open by absolute path, filter, an older PC's 404 told apart from a
missing pane) and the
`filesDocuments` rows in `src/lib/settings.test.ts` (off by default,
kept in its own key).
- New `scripts/files-documents-demo-regression.ts` (added to
`scripts/ci-browser.sh`): off by
default no Documents tab; on, the list in order, the folder named, no
sideways scroll, the filter
and Escape, a row opening the viewer; in the dialog, beside the pane at
320px and on a 390px sheet,
  where it checks that the four tabs and Close fit; light and dark.
- Review round (7ff0796): `server/files.test.ts` gained a repository of
2,000 old notes plus a newer
`zz-newest.md` (listed first), a non-git folder of 1,000 `.txt` files
plus `z-note.md` (listed, not
cut) and the 2 s budget (a zero budget answers cut);
`server/file-pool.test.ts` gained "a Markdown
listing counts as a scan". The three behaviour tests failed on 9456ed6
and pass now.
- Second round (c94745f): `server/file-pool.test.ts` "runs one Markdown
listing at a time, and a stuck one
leaves commands scans both their threads" failed on b8d4fa7 and passes
now.
- `bun run check fast`: passed at c94745f (origin/main merged):
workflow syntax, generated files,
  typecheck, build, unit tests 12420 pass, 0 fail.
- Reviews: omo CLEAR at c94745f; Codex Terra (xhigh) CLEAR at c94745f
after two fix rounds (findings
  and resolutions in the PR comments).
- Browser regressions for the touched shared components (Files dialog
and panel, file viewer,
  Settings) in the self-hosted CI runner image (`herdr-ci-runner:local`,
`scripts/ci-browser-parallel.ts`, after `bun run build`, inside `bun run
check run`), 12/12
passed at 9456ed6 (files-documents again at 7ff0796, 4/4 locally):
files-documents, files-panel, files-prompt, file-viewer,
file-viewer-mobile,
deep-link, diff-find, diff-line-notes, palette-local-page,
settings-pages, file-text-size,
  text-file-edit.
- Skipped locally: `bun run check integration` and the full browser lane
(CI runs them).
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.

Add safe editing for text files opened from chat output

1 participant