Repository navigation
Implement the 2026-10 audit and usability OpenSpec proposals - #55
Merged
Merged
Conversation
#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.
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>
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>
Merged
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.
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 andui-animationsfrom PR #53. Each change is implemented, verified, and archived underopenspec/changes/archive/with its delta specs synced intoopenspec/specs/. No open change remains inopenspec/changes/.Critical / High (reliability)
::$DATAstream being reported as an ADS.durable::replace_file(ReplaceFileWon Windows) and keep attributes, creation time, ADS and DACL.cd/del/rmdirtargets resolve against the panel directory..~fc-partialfile renamed over the target, moves flush and verify before deleting the source,CopyFileExWcarries full metadata, and aRealFsintegration suite runs against real NTFS.Medium / Low
-D warnings, test, rustdoc-D warnings, MSRV 1.88 check, best-effort Linux), committedrustfmt.toml, release profile, leangit2features (no libssh2/OpenSSL), all hand-declared Windows FFI moved towindows-sys, everyunsafeblock documented. The one-timecargo fmt --allis commit 58a7792, listed in.git-blame-ignore-revs.overlay: Option<Overlay>slot, oneInputLayerprecedence function shared by reducer, key map, mouse map and renderer,update.rssplit intoupdate/*.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. Alsocargo +1.88 checkand a release build.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
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.mainrequiring the CI checks.🤖 Generated with Claude Code