Skip to content

Implement the 2026-10 audit and usability OpenSpec proposals - #55

Merged
kkaminsk merged 63 commits into
mainfrom
build/open-proposals-2026-10
Oct 15, 2026
Merged

kkaminsk merged 63 commits into
mainfrom
build/open-proposals-2026-10

Conversation

@kkaminsk

@kkaminsk kkaminsk commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements every open OpenSpec proposal: the 21 proposals from the 2026-10-12 code audit (codeaudit/2026-10-12.md) plus the nine usability proposals from PR #54 and ui-animations from PR #53. Each change is implemented, verified, and archived under openspec/changes/archive/ with its delta specs synced into openspec/specs/. No open change remains in openspec/changes/.

Critical / High (reliability)

  • copy-onto-itself-guard (BIG-465): a same-file copy no longer truncates the file to 0 bytes.
  • move-reparse-point-safety (BIG-464): cross-volume Move never deletes through junctions or symlinks; Delete of a junction works on Windows. Also fixes the default ::$DATA stream being reported as an ADS.
  • editor-encoding-safety (BIG-466): non-UTF-8, UTF-16 and binary files go to the viewer instead of being corrupted on save; saves are atomic via a shared durable::replace_file (ReplaceFileW on Windows) and keep attributes, creation time, ADS and DACL.
  • resize-preserves-session-state (BIG-467): shrinking below 60x16 is a flag, not a phase; editor buffers, viewers, running jobs and overlays survive.
  • listing-request-guard (BIG-469): listings carry request ids; superseded listings, find-file walks and viewer searches are cancelled and discarded.
  • link-aware-listing (BIG-470): junctions and directory symlinks list as enterable directories; unreadable entries are skipped and counted.
  • relative-destination-resolution (BIG-471): F5/F6 destinations and cd/del/rmdir targets resolve against the panel directory.
  • durable-file-copy (BIG-468): copies land via a .~fc-partial file renamed over the target, moves flush and verify before deleting the source, CopyFileExW carries full metadata, and a RealFs integration suite runs against real NTFS.
  • offload-blocking-io (BIG-472): path probes, viewer/editor opens, saves, open-with and config writes run on workers; the reducer does no filesystem I/O.
  • process-lifecycle-hardening (BIG-473): Ctrl+C reaches only the child, quit waits for job cleanup, panics stay in their worker, the conflict dialog gains Cancel.
  • streaming-listing-performance (BIG-474): streaming enumeration, per-batch sorted merge, cached quick-filter index, budgeted event drain. 100k shuffled entries merge in 70–76 ms vs a 50–53 ms bulk sort.

Medium / Low

  • idle-aware-event-loop (BIG-475), git-info-timeout-memory (BIG-476), history-persistence-hardening (BIG-477), config-parsing-robustness (BIG-478), filter-selection-safety (BIG-479), viewer-mapping-safety (BIG-480), error-diagnostics (BIG-481), installer-scope-coexistence (BIG-484).
  • ci-and-build-tooling (BIG-482): GitHub Actions workflow (fmt, clippy -D warnings, test, rustdoc -D warnings, MSRV 1.88 check, best-effort Linux), committed rustfmt.toml, release profile, lean git2 features (no libssh2/OpenSSL), all hand-declared Windows FFI moved to windows-sys, every unsafe block documented. The one-time cargo fmt --all is commit 58a7792, listed in .git-blame-ignore-revs.
  • reducer-overlay-structure (BIG-483): one overlay: Option<Overlay> slot, one InputLayer precedence function shared by reducer, key map, mouse map and renderer, update.rs split into update/*.rs, dialog hit targets derived from render geometry. Snapshots byte-identical.

Usability batch (already on this branch from the previous session)

config-profile-directory, command-line-clear-key, reread-preserves-cursor, line-editing-in-text-fields, full-mode-column-widths, hidden-entry-styling, open-with-associated-app, pulldown-menu-completion, ui-animations, and the archive of delete-key-triggers-delete (shipped in #44).

Verification

All four CI gates pass locally on the final commit: cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace, RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps --locked. Also cargo +1.88 check and a release build.

Suite Tests
filecommand-core 1281
filecommand-tui 541
snapshot_views 111
RealFs integration (Windows, temp dirs) 14

Every proposal has exactly one unticked task: its manual interactive check, which cannot be driven from an agent session. The behaviours are covered by automated tests, several on real NTFS (junctions, ADS, attributes, timestamps, a mid-copy quit over a 256 MB file).

Notes for review

  • Known timing-sensitive test: a_mouse_event_ends_the_burst_and_is_stashed_for_the_next_iteration (8 ms burst budget) flaked once under heavy concurrent build load; passes reliably alone.
  • Not fixed, noted on BIG-473: the conflict dialog's choice row is wider than its box and has always rendered truncated.
  • After merge, BIG-482's last two tasks are manual: record the first CI run and enable branch protection on main requiring the CI checks.
  • GitNexus tooling was unavailable in these sessions; impact analysis was done by reading the code.

🤖 Generated with Claude Code

kkaminsk and others added 30 commits October 7, 2026 20:15
#49 changed the workspace repository URL to
https://github.com/kkaminsk/FileCommand but did not update the stored
snapshot_about_dialog_over_help_window snapshot, which still held the
placeholder example/filecommand URL, so the test failed on main.

One-line snapshot refresh; no behavior change. Split out from the
ui-animations work so it can be reviewed (or dropped) on its own.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Decorative ASCII animations for the startup splash, the F9 menu bar and
every pop-up menu, and every modal dialog, with an off switch
(`animations = false` in config.toml, or `--noanimations`; the flag wins).

Every animation is render-only interpolation of State::clock_ms minus a
stamp the reducer recorded when the surface opened, so update stays pure,
input is never blocked, and snapshots are deterministic. The stamps are
recorded by one post-condition in update (new core::stamps) rather than at
each open site. States built via State::empty keep animations off, so the
existing snapshot suite is unchanged.

- core::anim: per-mille progress / reveal_count / sweep_pos / iris_rect
  helpers and the named durations; a compile-time check that the splash
  entrance finishes inside the existing 800 ms hold.
- State::animations, dialog_opened_at_ms, quit_opened_at_ms,
  panels_revealed_at_ms; opened_at_ms / selected_at_ms stamps on
  MenuState, UserMenuState and FileActionMenuState.
- views: a shared scratch-buffer reveal helper (iris, unfold, bar sweep,
  panel wipe) so no dialog view needed per-view changes; splash frame
  trace, centre-out identity reveal and title sweep; pull-down, user menu
  and action menu unfold plus highlight settle; panel wipe after the
  splash. Hit-maps keep using final geometry.
- Config `animations` key (default on) and `--noanimations` flag; usage
  text and README updated.
- No new theme roles: animation colours reuse splash.frame,
  splash.version, menu.hotkey and screen.backdrop.

Tests: unit tests for the anim helpers, reducer stamp tests, config and
flag tests, and insta snapshots at pinned clock offsets for the splash,
pull-down, bar sweep, dialog iris and panel wipe, plus checks that every
surface converges byte-for-byte to the non-animated render.

All 19 tasks in openspec/changes/ui-animations/tasks.md are complete.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sync its delta specs into the main specs and move the change to the
archive:

- openspec/specs/ui-animations: new capability (6 requirements)
- openspec/specs/startup-splash: "Frame-1 splash rendering" modified;
  "Splash entrance animation" added
- openspec/specs/pulldown-menus: "Menu bar entrance sweep" added

The change moves to openspec/changes/archive/2026-10-07-ui-animations.
`openspec validate --specs --strict` passes for all 34 specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	openspec/changes/archive/2026-10-07-ui-animations/tasks.md
Brings the remaining unapplied OpenSpec proposals (with their latest
Spec-side wording refinements) onto build/open-proposals-2026-10 so the
batch can continue. Also marks delete-key-triggers-delete 3.2 done after
a clean openspec validate --strict run.
kkaminsk and others added 27 commits October 14, 2026 15:44
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reads config.toml and usermenu.toml through one shared line tokenizer (BOM strip, inline comments outside quotes, table headers with comments, verbatim vs TOML-escaped quoting) and collects ConfigWarning entries with file and line for malformed lines, unknown keys and tables, keys under the wrong table, bad booleans and key bindings, and non-UTF-8 files; parse/load return ParsedConfig and the TUI shows a first-plus-count summary in the startup-warning dialog at the lowest precedence, with user-menu reload warnings surfaced the same way. load_user_menu no longer overwrites a non-UTF-8 or unreadable usermenu.toml (new UserMenuProblem reason drives the not-UTF-8 message). Theme and panel-split rewrites go through table-aware set_top_level_key (keeps comments and BOM, inserts before the first table) and every config.toml/usermenu.toml write uses the new crate::durable::replace_file helper (unique temp file, sync_all, rename, cleanup on failure). The history.json reader strips a BOM, decodes JSON strings losslessly (surrogate pairs, U+FFFD for bad escapes) and finds keys only at the top level. README config.toml example and docs updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Frecency visits are stamped and ranked with wall-clock epoch milliseconds delivered alongside the monotonic tick (Command::Tick { now_ms, epoch_ms }, State::epoch_ms, WallClock::epoch_ms, the renderer and reducer rank with the same clock), so Ctrl+J recency survives restarts; history.json gains a version marker and migrate_loaded_history re-stamps legacy files, clamps future times and prunes to the new 1,000-entry MAX_DIR_HISTORY cap (record_visit prunes the lowest frecency, never the visited path). Writes leave the UI thread: Effect::PersistHistory hands the snapshot to the new history_writer thread, which coalesces bursts (2 s debounce, 10 s max wait), merges with the on-disk file through merge_history_files before each write, writes through durable::replace_file, and is flushed with a bounded wait when Runtime drops; dispatch_cd's duplicate persist is removed and the fixed-name temp_sibling helper is gone. README history.json section updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
history-persistence-hardening replaced Runtime::history_path with a HistoryWriter; the headless Runtime fixtures added by resize-preserves-session-state and process-lifecycle-hardening now construct a disabled writer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A cross-volume move no longer deletes through reparse points (audit R-01): walk_copy records a removal disposition per planned file and directory, content reached through a traversed link is kept, the link itself is removed with the shared reparse-leaf rule only once everything copied through it succeeded, and a link that cannot be removed is reported in the end-of-job summary. On Windows, RealFs derives no-follow is_dir from FILE_ATTRIBUTE_DIRECTORY so a junction is removed with remove_dir instead of failing with Access is denied; FakeFs resolves ancestor links in every mutating operation and refuses remove_file on directory objects so the regression test fails against the old worker. Also fixes stream_enum listing the unnamed default stream as an ADS named "", which made every real-file copy record a bogus stream warning. Adds FakeFs and RealFs (mklink /J) regression tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every copied file now lands through <name>.~fc-partial in the target's directory and is renamed over the target only after the copy succeeded; the temporary is removed on every error, Skip, Abort and Cancel path, so a failed overwrite keeps the old target and a failed copy never leaves a truncated file under the target's name. FileSystem::copy_file replaces copy_file_chunked with a metadata-preserving, exclusively-creating, length-checked and optionally flushed contract whose unrestored metadata is returned as CopyReport::unrestored and mapped onto error-diagnostics' summary warnings; list_streams, copy_stream_chunked and set_modified leave the trait. On Windows the copy is CopyFileExW (data, ADS, read-only/hidden/system/archive, last-write time) with an explicit creation-time restore, a post-copy stream check and a flush-with-read-only-dance for moves; the portable loop keeps a manual copy with create_new, set_modified, sync_all and a length check. A move flushes each file and removes its source through the error-recovery dialog, reporting a source that could not be removed. FakeFs models partial files and a Flush injection and is now cfg(test)-only, as is its re-export. The RealFs integration suite gains a read-only-clearing harness, scripted sink and CrossVolume ops log, and covers copy round-trip, cancel cleanup, read-only overwrite, leftover partial names, ADS/attributes/timestamps across copy and move, failed overwrite via a sharing violation, >260-character paths, case-only rename and copy-before-remove ordering (audit R-07, R-08, R-14, B-01).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
durable-file-copy writes a sibling .~fc-partial file and renames it over the target only on success, so the process-lifecycle-hardening quit test now waits for the partial file rather than the final name, and asserts both are gone after shutdown. PARTIAL_SUFFIX is exported from fs_ops for that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every directory listing is now tagged with a request id minted like git_request and recorded as PanelState::listing_request; ListingChunk/ListingComplete/ListingFailed echo it and apply_listing_event drops any reply whose id is not the panel's current listing before touching panel state, so a listing superseded by a navigation, re-read, or tab switch never adds to, completes, or fails the listing that replaced it. A new Effect::CancelRequest plus a TUI-side CancelRegistry of fs_ops CancelFlags lets the reducer stop superseded listings, find-file walks (Esc/Enter), and viewer F7 searches (close/newer search); the workers check the flag and send nothing once cancelled. A tab left mid-listing is cancelled, stamped stale, and refreshed on its next activation without clearing an interrupted re-read's anchor; Ctrl+T keeps the in-progress listing flowing into the new tab.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The panel reader now classifies a link (junction or symbolic link on Windows, symbolic link elsewhere) by following it once: a link to a directory lists, sorts and navigates as a directory, a link to a file lists as a file with the target's size, and a dangling or unfollowable link lists as a non-enterable file instead of failing the listing; name, date and hidden flag stay the link's own, and RawDirEntry/Entry carry an internal link flag. An entry whose own metadata cannot be read is skipped and counted (RawDirListing/ListingTotals, ListingComplete.unreadable) and the mini-status reports '1 entry could not be read' / 'N entries could not be read' through last_error until the panel next lists; an enumeration failure still fails the listing. Find-file never descends into a directory link, Info counts directory links as directories, and F8 on a directory link asks a single confirmation because Delete removes only the link. The pure classify_entry helper is unit-tested on every branch, with real mklink /J junction coverage on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Implemented on main via PR #44 (c6dc429) and its spec delta already synced into operation-dialogs; only the manual verification task was left unticked, so the change is archived as complete.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reducer no longer touches the filesystem: cd/del/rmdir targets not listed in the panel are checked through Effect::ProbePath on a worker (via the \?\ long-path abstraction) and completed by a request-id-matched Command::PathProbed, with a Checking <path>... note on the panel's mini-status and stale replies (newer probe, panel navigated, menu/dialog open) discarded. F3/F4 opens, the editor save and the associated-application launch (COM-initialized on Windows) move to catch_unwind-guarded worker threads: opens carry a request id gated by State::pending_open with an Opening <name>... note and a side-recorded failure, the viewer's ByteSource travels on a Runtime hand-off channel ahead of ViewerOpened, the editor shows Saving... and accepts no edits while EditorState::save_pending is set, and launch failures land on the invoking panel. PersistTheme/PersistPanelSplit go through a new ConfigWriter thread (ordered, last write wins per setting, bounded 2 s flush on every exit path) that still calls the durable save_theme_atomic/save_panel_split_atomic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The directory reader streams: FsReader::for_each_entry hands entries to the listing worker as the OS returns them, list_dir_streaming replaces list_dir_chunked and flushes batches under a BatchPolicy (count growing with what was sent, clamped to 256..16384, or ~50 ms of enumeration time) with the cancel check per entry. Each batch is sorted once and merged into the panel in one O(n + k) pass by PanelState::merge_streamed (cursor pinned at 0 until moved, then kept on its entry; the re-read anchor is resolved from the batch via a captured name->rank map). Entry carries a private lowercase sort_key set by Entry::new, which every name comparator, quick-filter match and type-ahead match now reads; all struct literals moved to Entry::new. PanelState caches the quick-filter visible-index list (visible_len/visible_at/visible_position/visible_window) and the renderer, hit-test, cursor movement and viewport clamps use it. The event loop drains worker messages under an 8 ms / 64-message budget with a one-slot held message and a backlog signal that zeroes the poll timeout. The 100k benchmark is extended (streamed merge, shuffled arrival, update() end to end, re-sort, filtered render, 1M one-off) and the spike document re-run and corrected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One-time ci-and-build-tooling format commit (rustfmt.toml: max_width = 150, use_small_heuristics = "Max"); listed in .git-blame-ignore-revs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the GitHub Actions CI workflow (required windows and msrv jobs, best-effort linux), the committed rustfmt.toml baseline recorded in .git-blame-ignore-revs, rust-version = 1.88 and a thin-LTO/stripped release profile that keeps panic = unwind, git2 without its network transports (libssh2-sys, openssl-sys and openssl-probe leave Cargo.lock), a shared windows-sys workspace dependency replacing every hand-declared Win32 prototype and struct in drives.rs, fs_ops/fs.rs and clock.rs with a SAFETY comment on every unsafe block enforced by clippy::undocumented_unsafe_blocks, fixes for every clippy and rustdoc warning so the gates start green, and README corrections for the toolchain floor, Linux build and test coverage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the reducer vocabulary (Command, Effect, ButtonId, ClickMods, DropTarget, DragState, the probe and pending-open records) into update/command.rs and the handlers into update/{listing,file_op,viewer,editor,overlays,panels}.rs per reducer-overlay-structure design D7, extracting update_impl's Panels arm as panels::handle_panels. Bodies are unchanged; moved private functions become pub(super) and the public paths are preserved through re-exports (tasks 1.1 and 1.2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the reducer tests verbatim into one file per update module (listing, file_op, viewer, editor, overlays, panels), with the shared fixtures and the helpers more than one file uses in update/tests/mod.rs (reducer-overlay-structure task 1.3). The test count is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the ten per-overlay Option fields on State with one overlay slot (State::overlay: Option<Overlay>) with typed accessors, open_overlay/close_overlay and the last-writer-wins set_startup_warning; route the reducer, the TUI key router, the mouse context, and the render/hit-map dispatch through one precedence function (input_layer: quit confirmation, undersized placeholder, file-op dialog, viewer, editor, overlay, panels or splash); hand each overlay handler its own state through a take/handle/restore step returning OverlayStep (Keep/Close/Replace/CloseThen), which removes the per-handler gates and unwraps and folds the is_*_command predicates into Overlay::claims; and give every clickable dialog and menu one layout() (DialogLayout, MenuLayout) that both its renderer and the hit map consume, with build_hitmap merged into views::render. Adds per-layer, slot-invariant, stale-reply and geometry property tests; insta snapshots are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kkaminsk kkaminsk changed the title Build batch: eight OpenSpec proposals + ui-animations Implement the 2026-10 audit and usability OpenSpec proposals Oct 15, 2026
@kkaminsk
kkaminsk merged commit cf5768c into main Oct 15, 2026
2 of 3 checks passed
@kkaminsk kkaminsk mentioned this pull request Oct 15, 2026
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