Skip to content

feat(workspace): sort selector in file-tree kebab menu (#6066) - #6091

Merged
8 commits merged into
nesquena:masterfrom
rodboev:pr/workspace-sort-6066
Aug 10, 2026
Merged

8 commits merged into
nesquena:masterfrom
rodboev:pr/workspace-sort-6066

Conversation

@rodboev

@rodboev rodboev commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • The workspace tree renders whatever order list_dir returns, and list_dir sorts by name only (api/workspace.py:1344-1355). Past a screen of files, "the one I just edited" is only findable by scrolling. The panel kebab already exists as the home for workspace prefs and already persists one of them, so both the affordance and the persistence pattern were already built — only the sort concern was missing.
  • The sort had to land at one chokepoint. _visibleWorkspaceEntries (static/ui.js:18752) has exactly two consumers — the top-level render (:19042) and the expanded-children render (:19430) — and those are the only two places a level's rows are built. Sorting at either call site alone would give a sorted root and an unsorted subdirectory, so both now route through one composed _workspaceEntriesForRender, and _visibleWorkspaceEntries stays a pure filter so the feat(workspace): hide (or dim) .DS_Store / Thumbs.db / .git/ / __pycache__ etc. in the file tree by default, with a 'Show hidden files' toggle #1793 hidden-files behavior and its test are untouched.
  • Grouping is preserved by carrying the server's authoritative partition rank across the API boundary. _sort_key_de returns (not is_link, is_file, name.lower()), which orders symlinks, then non-regular non-links, then regular files. api/workspace.py emits workspace_sort_rank as 0, 1, or 2 from the same link-local classification, and _workspaceEntryRank consumes only those exact numeric values. Special entries serialized as type: 'file' therefore stay in rank 1. Name (A → Z) returns the filtered list untouched rather than re-sorting to an equivalent order, so the default path is a literal no-op and the server's ordering is never re-litigated in JS.
  • Date created could not be renderer-only. Entries carry mtime_ns and nothing else time-related (api/workspace.py:1297, :1309, :1335), so list_dir now emits birthtime_ns from the follow_symlinks=False stat it already performs (:1374), with no extra syscall. Linux returns None rather than ctime, which is inode-change time there, and the UI disables the option with a reason instead of showing a field that would silently lie. The final patch stringifies nanosecond fields at the route layer and compares signed decimal keys exactly, so newest-first sorting neither collapses distinct >2^53 timestamps nor classifies negative integer values as missing; equivalent zero encodings such as 0, -0, +0, 000, and -00 now normalize to one exact zero key instead of drifting into a separate signed bucket.
  • One sibling site is deliberately out of scope: dir_signature (api/workspace.py:1433) also reads these entry dicts, and it does not gain birthtime_ns. Birthtime is immutable per inode, so it adds no change-detection signal beyond the name/type/size/mtime/target already hashed, and adding it would perturb every stored signature once and force a spurious refresh for every open workspace on upgrade.

What Changed

  • static/ui.js: added the Sort by radio group to _buildWorkspacePrefsMenu above the existing checkbox; added the exact signed timestamp-key comparator and _workspaceEntriesForRender; routed both render call sites (:19181, :19569) through the new chokepoint; consumes the transported workspace_sort_rank without type inference; clears created-sort availability on the no-workspace render path; extracted the kebab dot into _syncWorkspacePrefsIndicators; tracked created-sort availability per workspace; and reconciled the disabled created row's input, ARIA, class, and .workspace-prefs-meta while an open menu observes support changes.
  • api/workspace.py: emits workspace_sort_rank on every entry shape, including special files and display-only escape symlinks, from the same classification used by the server ordering key. dir_signature remains unchanged.
  • static/workspace.js: reset workspace birthtime availability on root and refresh loads so switching profiles or workspaces clears stale created-sort support before the next render.
  • api/workspace.py: added _birthtime_ns() and emitted birthtime_ns on all three list_dir entry shapes, always from the link-local stat. dir_signature unchanged.
  • api/routes.py: route-serialized mtime_ns / birthtime_ns as exact decimal strings for browser consumers, leaving the Python-side list_dir and dir_signature contracts on integer nanoseconds.
  • static/style.css: styles for the radio group, its label, the separator, and the disabled row — existing tokens only, no media queries.
  • static/i18n.js: six new keys in all 15 locale blocks, English baseline in non-en.
  • tests/test_issue6066_workspace_sort.py: backend birthtime and partition-rank behavior across platform stat shapes, signature-unchanged checks, exact signed timestamp ordering, strict malformed-rank fallback, workspace-switch and no-workspace availability resets, open-menu metadata reconciliation, and a Node harness over the extracted sort helpers.
  • tests/test_issue6066_workspace_sort_layout.py: Playwright checks that the grown menu lays out at 1280 / 1024x600 / 480x320, stays inside the viewport when the unavailable-created explanation appears or disappears on an already-open menu, and still covers DE/RU.

Why It Matters

Finding a recently-edited file in a workspace no longer means scrolling an alphabetical list; the preference persists across reloads and the kebab dot shows at a glance that a non-default order is active. Directories and symlinks keep their existing grouping at every level, so the default view is byte-for-byte what it is today.

Verification

The focused workspace-sort, payload, cruft-filter, and directory-signature tests pass, with the platform-specific FIFO case skipped on this Windows host because FIFO creation is unavailable. The Node harness covers all four keys, rank grouping, special-file rank 1 despite type: 'file', strict malformed-rank fallback, null and missing timestamps, signed values, exact decimal-string ordering, effective menu state, and created-sort availability resets when the workspace changes or disappears. Backend coverage uses real temporary directories and symlinks, checks the link-local timestamp on the display-only escape row, preserves dir_signature, and verifies exact browser serialization without mutating the source entries. CI runs the full matrix on Python 3.11, 3.12, and 3.13.

Risks / Follow-ups

  • Happy to split this if you'd prefer. The suggestion in the issue thread was to ship name+modified first and defer created. The two concerns are separate commits on this branch, so the birthtime_ns commit can be dropped without touching the rest — say the word and I'll re-push without it. It's included because the thread already settled the fallback question (None on Linux, UI disables the option), which was the open decision.
  • st_ctime_ns on Windows only. The thread's rule is "never fall back to ctime", which is right on POSIX where ctime is inode-change time. On Windows st_ctime is creation time (as the issue body notes), and on Python 3.11 — the CI floor — neither st_birthtime_ns nor st_birthtime exists there, so the suggested getattr(lst, 'st_birthtime_ns', None) alone would leave "Date created" permanently disabled for Windows users on 3.11. _birthtime_ns therefore reads st_ctime_ns under sys.platform == 'win32' and only there. If you'd rather take the dead option on 3.11 than the platform branch, that's a one-line removal.
  • Directories over 200 entries. list_dir caps at 200 entries (api/workspace.py:1385, :1428) and the cap is applied after the name sort, so past 200 the client receives the alphabetically-first 200. Sorting client-side over that window is therefore accurate only up to 200 entries — beyond it, "newest first" ranks a name-biased sample, and the payload carries no truncation signal for the UI to warn from. This PR sorts the window it's given rather than changing a shared endpoint's cap or contract under a sort-selector change. Happy to file a follow-up for a truncation flag or a server-side sort parameter if that's the direction you want.
  • Symlink grouping left as-is. Symlinks currently sort above real directories at every level (that's what (not is_link, is_file, name.lower()) does). The new ranking mirrors that exactly rather than quietly normalizing it, so nothing reorders today — but if the intent was dirs-first-including-symlinked-dirs, that's a separate change worth its own issue.
  • Security-sensitive surface. This adds workspace_sort_rank and birthtime_ns to the display-only escape-symlink row from feat(workspace-tree): surface escape-target symlinks as display-only rows #4581. Both are derived from the link's own follow_symlinks=False stat, so they disclose nothing about the target; the resolved path, target-derived is_dir, and target size are still withheld and navigation is still blocked by safe_resolve_ws/open_anchored_fd. Covered by a test that plants a real escaping symlink and asserts the link-local timestamp, partition rank, and continued absence of target fields.
  • Non-en locales carry the English baseline for the six new keys, per the existing convention; native translations welcome as a follow-up.
  • Release note: workspace file tree can now be sorted by name (A→Z / Z→A), date created, or date modified from the panel's ⋮ menu; the choice persists across reloads.

Contract Routing

Task type: user-visible workspace preference feature plus product-semantics regression coverage.

Touched areas: workspace file-tree ordering, the workspace prefs kebab menu, and the /api/list metadata payload consumed by that menu.

Relevant public docs:

  • AGENTS.md
  • CONTRIBUTING.md
  • docs/CONTRACTS.md
  • docs/UIUX-GUIDE.md

Scope boundaries: keep the existing symlink/dir/file grouping, keep dir_signature unchanged, keep the hidden-files contract intact, and add no new inline workspace chrome outside the existing kebab menu.

Evidence needed before claiming done: focused backend and UI regression coverage, exact metadata payload proof, layout proof with before/after screenshots, and CI confirmation on the upstream matrix.

Screenshots

Before, the workspace panel kebab only exposes the hidden-files preference.

Before: workspace panel kebab with only the hidden-files toggle

After, the same menu adds the Sort by group and shows a non-default sort active from the kebab itself.

After: workspace panel kebab with Sort by radios and an active non-default sort

Upstream

Closes #6066.

Thanks to @nesquena-hermes for tracing both the list payload and the kebab wiring in the issue thread — the birthtime_ns-from-the-existing-stat route, the None-not-ctime decision, the global-pref call, and the sort-within-the-server's-partitions note all came from there and shaped this patch.

Model Used

GPT 5 via Codex CLI

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Sort by preference to the workspace panel's kebab menu, allowing users to order the file tree by name (A→Z / Z→A), date modified, or date created (newest first). It introduces birthtime_ns from a zero-extra-syscall lstat call, serializes both nanosecond timestamps as exact decimal strings at the route layer to avoid JS precision loss on large values, and sorts client-side within the server's existing symlink/dir/file partitions.

  • api/workspace.py + api/routes.py: _birthtime_ns() extracts creation time across platform/Python-version stat shapes; serialize_workspace_entries_for_browser() stringifies mtime_ns/birthtime_ns so the browser receives exact decimal strings; dir_signature is intentionally left unchanged.
  • static/ui.js: A single _workspaceEntriesForRender chokepoint composes visible-entry filtering with rank-preserving sort; a custom big-integer string comparator handles arbitrary-precision nanosecond values; _effectiveWorkspaceSortKey falls back to name-asc when created-desc is stored but the server hasn't reported any birthtime, keeping the stored preference for when it later becomes available.
  • Tests: Backend covers platform stat shapes, symlink provenance, and signature immutability; a Node harness exercises all sort keys, rank grouping, null/zero/negative/large timestamp ordering, and open-menu state reconciliation; Playwright layout proofs cover three viewports and two non-English locales.

Confidence Score: 5/5

Safe to merge — the change is well-contained to the workspace panel, adds no new security surface beyond what the existing mtime_ns field already exposes, and all edge cases are covered by tests.

The timestamp serialization, sort comparator, birthtime availability tracking, and fallback logic are each thoroughly exercised by the Node harness and backend tests. The dir_signature contract is unchanged, the hidden-files filter path is untouched, and the new sort preference degrades gracefully on Linux or when the server hasn't reported birthtime yet.

No files require special attention.

Important Files Changed

Filename Overview
static/ui.js Adds sort key constants, big-integer string comparator, _workspaceEntriesForRender chokepoint, birthtime availability tracking, and _buildWorkspacePrefsMenu sort radio group; both render call sites updated; aria-checked and indicator dot both use _effectiveWorkspaceSortKey (not the raw stored key).
api/workspace.py Adds _birthtime_ns(), _browser_timestamp_ns(), and serialize_workspace_entries_for_browser(); emits birthtime_ns on all three list_dir entry shapes from the existing lstat result; dir_signature unchanged; platform/Python-version matrix handled correctly.
api/routes.py Routes serialize entries through serialize_workspace_entries_for_browser before JSON serialization; dir_signature still receives the original integer entries; escape-list-dir endpoint also covered; no mutation of source dicts.
static/workspace.js One-line addition: calls _syncWorkspaceBirthtimeSupportScope on root/refresh loads so stale created-sort availability is cleared when the workspace or profile changes.
static/i18n.js Six new keys added to all 15 locale blocks; English baseline in non-en per existing convention; count equality with workspace_show_hidden_files verified by test.
static/style.css Seven-line addition for radio group container, group label, radio item, disabled state, and separator; uses only existing design tokens.
tests/test_issue6066_workspace_sort.py Comprehensive backend + Node harness coverage: platform stat shapes, escape-symlink birthtime provenance, dir_signature immutability, all sort keys, rank grouping, null/zero/negative/large timestamp ordering, signed-zero normalization, workspace-switch availability reset, and open-menu state reconciliation.
tests/test_issue6066_workspace_sort_api_payload.py Verifies serialize_workspace_entries_for_browser converts large integer nanosecond values to exact decimal strings and does not mutate original entry dicts.
tests/test_issue6066_workspace_sort_layout.py Playwright layout coverage at 1280/1024x600/480x320; DE and RU locale proofs; menu repositioning after created-sort availability flip on an already-open menu near the viewport edge.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant FS as Filesystem
    participant WS as workspace.py (list_dir)
    participant RT as routes.py
    participant UI as ui.js (renderFileTree)
    participant LS as localStorage

    FS->>WS: "os.lstat(follow_symlinks=False)"
    WS->>WS: _birthtime_ns(lstat_result)
    WS-->>RT: "entries [{mtime_ns: int, birthtime_ns: int|None}]"
    RT->>RT: serialize_workspace_entries_for_browser(entries)
    Note over RT: converts int ns to exact decimal strings
    RT-->>UI: "JSON {entries: [{mtime_ns: str, birthtime_ns: str|null}]}"
    UI->>UI: _noteWorkspaceBirthtimeSupport(S.entries)
    UI->>LS: getItem('hermes-workspace-sort-key')
    LS-->>UI: stored sort key
    UI->>UI: _effectiveWorkspaceSortKey()
    UI->>UI: _workspaceEntriesForRender(entries)
    UI->>UI: _renderTreeItems(container, sorted, depth)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant FS as Filesystem
    participant WS as workspace.py (list_dir)
    participant RT as routes.py
    participant UI as ui.js (renderFileTree)
    participant LS as localStorage

    FS->>WS: "os.lstat(follow_symlinks=False)"
    WS->>WS: _birthtime_ns(lstat_result)
    WS-->>RT: "entries [{mtime_ns: int, birthtime_ns: int|None}]"
    RT->>RT: serialize_workspace_entries_for_browser(entries)
    Note over RT: converts int ns to exact decimal strings
    RT-->>UI: "JSON {entries: [{mtime_ns: str, birthtime_ns: str|null}]}"
    UI->>UI: _noteWorkspaceBirthtimeSupport(S.entries)
    UI->>LS: getItem('hermes-workspace-sort-key')
    LS-->>UI: stored sort key
    UI->>UI: _effectiveWorkspaceSortKey()
    UI->>UI: _workspaceEntriesForRender(entries)
    UI->>UI: _renderTreeItems(container, sorted, depth)
Loading

Reviews (2): Last reviewed commit: "fix(workspace): align sort menu state wi..." | Re-trigger Greptile

Comment thread static/ui.js
@nesquena-hermes nesquena-hermes added the size:L Large PR (>10 files or >250 LOC) label Jul 15, 2026
@cutter-sh

cutter-sh Bot commented Jul 15, 2026

Copy link
Copy Markdown

🎬 Cutter preview — PR #6091

Open Workspace Preferences menu
Open Workspace Preferences menu — Workspace preferences menu adds a Sort by section with name, created, and modified options.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Reading the diff at origin/master...HEAD for static/ui.js — the sort chokepoint (_workspaceEntriesForRender, 18803-18811), the effective-key resolver (18799-18801), and both menu-state sites (18844, 18938) — the routing through one _workspaceEntriesForRender is clean, and keeping _visibleWorkspaceEntries a pure filter so the #1793 hidden-files test stays untouched is the right call. One follow-up on the greptile P2 about the disabled created-desc radio, because I think there's a tighter fix than the one suggested.

The inconsistency

Three consumers derive the sort key, but from two different sources:

// 18799 — resolver: falls back to name-asc when birthtime unavailable
function _effectiveWorkspaceSortKey(){
  const key=_normalizeWorkspaceSortKey(S.workspaceSortKey);
  return key==='created-desc'&&!_workspaceCreatedSortAvailable()?WORKSPACE_SORT_DEFAULT:key;
}
  • Render ordering (_workspaceEntriesForRender, 18804) uses _effectiveWorkspaceSortKey().
  • The kebab dot (_syncWorkspacePrefsIndicators, 18837) uses _effectiveWorkspaceSortKey().
  • But both places that set the radio's checked/aria-checked state use the stored preference:
// 18844 (_syncWorkspaceSortMenuState) and 18938 (_buildWorkspacePrefsMenu)
const active=_normalizeWorkspaceSortKey(S.workspaceSortKey);

So when created-desc is stored but birthtime is unavailable, the menu paints created-desc as checked while files actually render name-asc. That's the mismatch greptile called out.

Why not greptile's suggestion

Greptile proposed marking created-desc unchecked+disabled. But active is still created-desc, so name-asc wouldn't get checked either — the menuitemradio group ends up with zero checked rows, which is its own ARIA violation and looks like "no sort selected" when the tree is clearly name-sorted.

Suggested fix

Drive active off the effective key at both sites:

const active=_effectiveWorkspaceSortKey();

Now name-asc gets checked (matching the real ordering and the dot), created-desc still renders disabled via the independent disabled=input.value==='created-desc'&&!createdOk check, and its aria-checked correctly reads false. The nice property: your _noteWorkspaceBirthtimeSupport latch (18823) already calls _syncWorkspaceSortMenuState() when the first non-null birthtime_ns arrives, so the moment support latches, _effectiveWorkspaceSortKey() flips back to created-desc and the radio re-checks itself — no extra bookkeeping. The stored preference in localStorage is untouched, so the user's real choice survives for when it becomes available again.

Everything else here looks solid — the string-decimal comparator handling >2^53 nanosecond values, dir_signature left on integer inputs, and the escape-symlink row reading birthtime_ns from the link-local stat are all the right boundaries. This is the only spot where visible state and effective state can disagree.

@rodboev

rodboev commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, you're right about the last mismatch.

  1. The tree ordering and the kebab dot already route through _effectiveWorkspaceSortKey(), but _syncWorkspaceSortMenuState() and _buildWorkspacePrefsMenu() were still keying active off S.workspaceSortKey.
  2. I'm switching both menu-state sites to _effectiveWorkspaceSortKey() so the checked radio matches the actual rendered ordering when created-desc is stored but unavailable.
  3. I also added a focused regression around the unavailable-created case so the menu now proves name-asc is the checked row while created-desc stays disabled and unselected.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — GREEN ✅ (workspace file-tree sort selector — engineering-clean; parks for @nesquena visual sign-off, crown-jewel workspace pane)

Certified head: 573ac3318298ba31d2d19629513a3609e87531fa (@rodboev, T1)
Gated on a clean rebase: head was 147 commits behind master; I applied its merge-base three-dot diff onto current origin/master@deac1384 as gate-rebase/6091-573ac33. Feature patch-id byte-identical before/after rebase (bfacc9afe4e86ea7aeaa6633577b738d5bf5ff54), zero conflicts.
Verdict: Codex SAFE TO SHIP, Fable SHIP-UX, full suite clean, and my own live drive confirms the feature works cleanly across viewports. Engineering-GREEN. This is a visible new control on the workspace pane (crown jewel), so it PARKS for @nesquena's visual sign-off — 4 screenshots sent to Telegram.

The feature

A "Sort by" selector added to the workspace file-tree kebab (⋮) menu: Name (A→Z) / Name (Z→A) / Date created (newest first) / Date modified (newest first), plus the existing Show-hidden toggle. Scope: api/routes.py +4/-1, api/workspace.py +38, static/i18n.js +90, static/style.css +7, static/ui.js +171/-15, static/workspace.js +1, +3 test files. +775/-16.

Security / correctness — verified

  • No server-side sort on user input. Sort is 100% CLIENT-SIDE with an explicit allowlist WORKSPACE_SORT_KEYS = ['name-asc','name-desc','created-desc','modified-desc']; _normalizeWorkspaceSortKey() clamps any stored/URL value to the default; the value is esc()-escaped. api/workspace.py only ADDS metadata (_birthtime_ns() platform-safe getattr; serialize_workspace_entries_for_browser() stringifies mtime_ns/birthtime_ns to dodge JS 2^53 loss). No sort key reaches the server, no getattr/sorted(key=…) on attacker input, no path traversal. Codex verified the timestamp metadata does not alter directory signatures or path authorization.
  • Ordering correctness. Folders-first grouping preserved (frontend _workspaceEntryRank symlink/dir/file matches backend _sort_key exactly); big-int-safe timestamp comparator; missing-metadata sorts last; created-desc gracefully disabled with an honest note ("Creation time is not reported by this server or platform.") until birthtime is observed, falling back to name-asc. The head commit ("align sort menu state with effective ordering") ensures the checked radio always reflects the ACTUAL applied ordering — no menu/tree desync.
  • Locale parity. All 6 new keys (workspace_sort_by, workspace_sort_name_asc, workspace_sort_name_desc, workspace_sort_created_desc, workspace_sort_created_unavailable, workspace_sort_modified_desc) present in every one of the 15 locale blocks (verified 15/15 each) — no en-only, no raw-key render.

What I ran

  • Codex (reproduce leg): SAFE TO SHIP — all 9 files + callers reviewed; no sort key reaches server; allowlisted/escaped/synchronized; no classic-script collision or undef-global; 46 focused tests incl. rendered desktop/mobile layouts; JS/Python runtime lint clean. (Notes the pre-existing 200-entry cap as an unchanged known limitation.)
  • Fable (UX leg): SHIP-UX — placement correct, honest-state UX (the disabled Date-created states WHY rather than silently using ctime), all 6 keys × 15 locales, cross-device sound (menu position:fixed, max-width:min(280px, calc(100vw−16px)), flip-above, tested to 480×320). Two non-blocking observations only.
  • My own live drive on a seeded gate server (8783) with a realistic file tree (folders assets/docs/src/tests + files .gitignore/LICENSE/package.json/README.md, varied timestamps), 1440×900 + 480×760:
    • Default: folders-first, Name A→Z. ✅
    • Sort kebab open: all 5 rows render; Date created correctly disabled with the "not reported by this server or platform" note; radio group + Show-hidden checkbox clean. ✅
    • Date-modified sort active → kebab dot shown. ✅
    • 480px narrow: menu viewport-clamps in-frame, no off-screen clipping. ✅
  • Focused: test_issue6066_workspace_sort.py + _api_payload.py = 25 pass; ruff-forward + node --check (ui.js/i18n.js/workspace.js) clean.
  • Full serial suite: 13460 passed / 7 failed — all 7 are established clean-master no-network/env failures (Nous catalog/picker ×5, model-cache-fingerprint, default-model); the PR touches none of those files. Zero failures from this diff.

Screenshots (sent to Telegram)

  1. Workspace explorer with the seeded tree, default Name A→Z.
  2. Sort kebab OPEN — all options + the gracefully-disabled Date-created row.
  3. Date-modified sort active.
  4. 480px narrow viewport — menu clamps in-frame.

Disposition

GREEN — engineering-ship-ready at sha:573ac3318298. gate-pass; added to Priority Queue Tier 1. This is a new visible control on the workspace pane (crown-jewel surface), so per the screenshot-evidence-standards crown-jewel gate it does NOT ship unattended — it PARKS for @nesquena's visual sign-off (screenshots delivered). Preserve @rodboev author credit. The non-blocking observations (Fable's kebab-dot asymmetry note; no ascending date variants) need no action.

@nesquena-hermes nesquena-hermes added gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent ux User experience / visual polish labels Jul 19, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Thanks @rodboev — the UX side is approved (independent design review passed: placement in the panel kebab is the right home, matches VS Code/JetBrains view-menu convention, disabled "Date created" with inline reason is the correct pattern, ARIA menuitemradio semantics are clean). Full suite is green (13944 passed) and default sort is a verified no-op pass-through.

One real correctness regression to fix before merge, though — reproduced with a live FIFO:

Special-file (FIFO / socket / device node) partition divergence on non-default sorts.

The rank-preservation claim holds for symlink / dir / regular-file, but misses the special-file case:

  • Server _sort_key_de (api/workspace.py:1424) ranks by (not is_link, is_file, name). A FIFO/socket/device node has is_file=False, so the server groups it in the directory partition.
  • But the server serializes that same entry as type: 'file' (api/workspace.py:1412, the non-symlink branch — only S_ISDIR becomes 'dir', everything else is 'file').
  • _workspaceEntryRank (static/ui.js:19558) then maps type:'file' → rank 2 (regular-file partition).

Net: on any non-default sort, a FIFO/socket/device node is moved out of the dir partition (where the server put it) into the regular-file partition. Reproduced:

entries = [a-pipe (FIFO), z-dir (dir), b-file (regular)]
server order (name-desc): pipe, directory, regular file
PR's name-desc helper:    directory, regular file, pipe   ← pipe jumped partitions

Default sort is unaffected (it's a pass-through), and the full suite stays green because no fixture creates a special file — the defect lives on a path the tests can't currently reach.

Fix (mirrors your own "mirror the server's key rather than reinvent it" approach):

  1. Emit an authoritative partition rank from the backend, derived from the same classification the server ordering uses (not is_link, is_file), on every entry in api/workspace.py — not just dirs/symlinks.
  2. Consume that field in _workspaceEntryRank instead of re-deriving rank from type.
  3. Keep the new field out of dir_signature so it doesn't perturb change-detection / the default order.
  4. Add a regression that builds a real FIFO (os.mkfifo) + a dir + a regular file and asserts every non-default sort preserves the server's symlink→dir→file partition boundaries.

Design review (UX) is already a pass, so this is the only blocker. Happy to take the follow-up myself if you'd prefer — just say the word.

rodboev added 4 commits August 1, 2026 18:46
…esquena#6066)

The frontend re-derived partition rank from the serialized type, so FIFO,
socket, and device entries moved into the regular-file partition on non-default
sorts. Emit workspace_sort_rank from list_dir, consume it in the rank helper,
and keep it out of dir_signature.
@rodboev

rodboev commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 1c64e52.

api/workspace.py now emits workspace_sort_rank from the same link and regular-file classification used by both server sort paths, including special entries and display-only escape symlinks. The UI consumes only exact ranks 0, 1, and 2, so a special entry serialized as type: "file" stays in the non-regular partition. The rank is excluded from dir_signature, and created-sort availability now fails closed when the workspace session is gone.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Gate certification: PASS

Certified contributor head: 1c64e52f6e256cfa334ba70d79ccca59ddc56884

Integration tested: rebased gate head 04674b866372b0cc9e336133f3eea717d1c2d3cf on frozen origin/master a4bd54ef71b9716ab7c723a24347df2755a46946.

This was a semantic rebase rather than a byte-identical one. git range-diff shows commits 3–8 patch-equivalent; commits 1–2 only preserve two newer master facts the contributor branch would otherwise displace: the Japanese workspace-panel translations and the existing workspace field in the list-dir response. Master advanced again during the gate to 218d58643274dd103c470d6a8e2b39c3e5e21eb6, but changed none of the ten PR paths; Codex also verified that compatibility merge.

Review

  • Threat scan: CLEAN on both the contributor and rebased diffs.
  • Special-file repair: correct. Server classification emits authoritative rank 0 for symlinks, 1 for directories/non-regular/unstatable entries, and 2 for regular files; the client accepts only exact numeric 0|1|2. FIFO/socket rows serialized as type:file now remain in the server's non-regular partition.
  • workspace_sort_rank stays outside dir_signature, browser serialization copies rather than mutates source entries, escape-link non-disclosure remains intact, and created-sort availability now fails closed after workspace/session teardown.
  • Codex exact-rebase gate: SAFE TO SHIP, no regression risk.
  • Independent Opus 4.8 review: APPROVE, no blocker.
  • Hosted exact-head rollup: 22/22 successful.

Verification

  • Full suite, five isolated no-network shards using standalone exact-tree clones and a sandbox-private TMPDIR: 13,921 passed, 104 skipped, 3 xpassed, 34 subtests passed. An initial five-way launch over-saturated the host and produced unrelated timeout/setup noise; it is not counted. The authoritative rerun used at most two shards concurrently and a 180-second test timeout.
  • The canonical sandbox run had four topology-only non-green nodes: POSIX ownership preservation, native Agent discovery, and two PEP 517 wheel-build checks. The exact same 2 failed / 2 errors reproduces on frozen master under the same sandbox. All 4/4 pass under exact-node, credential-cleared env -i native execution.
  • All 104 skips were enumerated: 74 Agent-hidden nodes pass natively with a disposable HOME and code-only Agent path; the one order-polluted OpenRouter skip passes all five parameter cases in isolation; 25 are documented platform/optional-tool/dev-topology skips; four unchanged layout nodes require an explicitly launched live PR server.
  • Focused workspace-sort/API/signature set: 32 passed.
  • Reviewer-owned real-filesystem discriminator: 1 passed, covering symlink + directory + FIFO + Unix socket + regular-file ordering in both dir-fd and path fallback modes.
  • git diff --check, Python compilation, sandboxed Node syntax checks, and sandboxed ruff delta: clean. New test files have no ruff findings; the 20 existing-file findings are identical to frozen master by rule/message.

Visual gate disposition

The prior head 573ac3318298ba31d2d19629513a3609e87531fa already received Fable SHIP-UX, a populated desktop/mobile live drive, and Nathan's recorded UX approval. This follow-up changes no selector markup, CSS, locale strings, static/workspace.js, or layout test fixture; it changes special-file ordering metadata and teardown behavior only. The four live-server layout nodes therefore remain covered by the prior visual disposition, while current hosted browser smoke is green. I did not launch the contributor PR in a browser because the untrusted-PR execution boundary forbids direct PR server/browser runtime.

Execution note

One initial Node parse attempt used the wrong sandbox binary path. I then mistakenly ran a parse-only host check, and separately ran the ruff-forward helper outside the wrapper. Neither host result is counted. I reran the identical Node and ruff checks through the CLEAN no-network sandbox with reviewer-staged trusted binaries; those sandboxed reruns are the evidence above.

Result

PASS. This exact contributor head is technically gate-clean and should move to Priority Queue T1. GitHub reports the contributor head as BEHIND; the release agent should perform a fresh mechanical current-master rebase before staging. Existing UX approval carries. This certificate does not merge, tag, deploy, close the PR, or push the contributor branch.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in bc6e01a Aug 10, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in exp-v0.52.194 — thanks @rodboev! 🎉

The workspace file-tree "Sort by" menu (Name A→Z / Z→A, Date created, Date modified) + the relocated "Show hidden files" toggle are live on the experimental channel. Gate: Codex/warmup gate-pass, Fable UX SHIP, ESLint + ruff + browser-smoke clean, full 5-shard suite green (only the 2 known box-baseline env flakes, unrelated), maintainer visual approval.

One fast-follow tracked: translating the 6 new i18n keys (workspace_sort_by + the 4 option labels + workspace_sort_created_unavailable) into the 14 non-English locales — they currently fall back to English. Happy to take a follow-up PR for that, or we'll fold it into the next locale pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent size:L Large PR (>10 files or >250 LOC) ux User experience / visual polish

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace file-tree: add sort selector (name / created / modified) to the kebab menu

2 participants