Skip to content

feat(files): add a remote-safe workspace file tree and editor - #130

Merged
wingleeio merged 105 commits into
zeronsh:mainfrom
jsgrrchg:feature/file-editor
Sep 8, 2026
Merged

wingleeio merged 105 commits into
zeronsh:mainfrom
jsgrrchg:feature/file-editor

Conversation

@jsgrrchg

@jsgrrchg jsgrrchg commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR adds a complete workspace file browsing and editing workflow to Zeron's right pane.

Users can open a new Files surface, navigate or search the checkout associated with the current chat, preview and edit supported text files, keep multiple files open as surface tabs, and attach workspace files or folders to the composer by dragging them from the tree, search results, or file tabs.

The implementation works through the same RPC boundary for local and remote workspaces. The UI never reads or writes workspace files directly, and every operation is resolved and authorized by the engine on the device that owns the checkout.

Editing is powered by a pinned Apache-2.0 gpui-base fork that is compatible with Comet's GPUI revision. Zeron continues to own the surrounding product behavior: file tabs, layout, theme integration, syntax highlighting, autosave, optimistic concurrency, external-change reconciliation, inline review comments, settings, and remote routing.

The work was developed in three stacked phases and is included end to end in this branch:

  1. Workspace file protocol and engine backend.
  2. Files surface and virtualized file tree.
  3. Editable file documents and editor integration.

Important external dependency: this PR intentionally depends on the contributor-owned fork jsgrrchg/gpui-component, pinned at immutable commit 8194877275c5d7d3a8eacf3aed7c2cf8c9be8098. That fork contains two required custom patches: GPUI dependency/API alignment with Comet and stable physical-pixel caret rendering. The exact fork delta is documented below.

Motivation

Zeron already had read-only diff rendering and file mentions, but it did not have a general-purpose way to browse or edit arbitrary files in the active workspace.

Reusing the diff viewer as an editor would have required rebuilding cursor movement, selection, IME, clipboard, undo/redo, and text-buffer behavior inside a renderer designed for immutable hunks. Pulling in Zed's complete editor would also have introduced GPL code and a large dependency graph coupled to Zed's project, language, LSP, and multibuffer architecture.

This PR instead introduces a small product-owned filesystem layer and uses the focused editing primitives from gpui-base. That gives Zeron native GPUI performance and editor fundamentals while keeping workspace semantics, transport, safety, and UI under Zeron's control.

User-facing experience

Files surface

  • Adds Files to the right-pane surface picker and the + surface menu.
  • Keeps one Files browser per chat panel and follows the checkout associated with that chat.
  • Shows an explicit empty/error state when the chat has no resolvable workspace.
  • Reuses the initial Files tab for the first opened document and changes its title to the file name.
  • Opens additional documents as independent right-pane tabs.
  • Restores editor focus when switching back to an already-open file tab.
  • Preserves the file tree owned by each file surface instead of replacing it with a global browser.
  • Supports right-pane tab reordering without interfering with file drag-and-drop.

Responsive layout

  • Uses a compact browser/editor presentation when the right pane is narrow.
  • Shows the editor and file tree together once the surface reaches the wide breakpoint.
  • Places the tree on the right to match the established T3 Code-inspired interaction model.
  • Adds a dedicated sidebar toggle in the file header so users can show or hide the tree without losing the current document.
  • Remembers an explicit sidebar dismissal while the surface crosses responsive breakpoints, so a tree hidden by the user does not immediately reopen itself.
  • Keeps the sidebar toggle available while the tree is visible, including in wide layouts.
  • Makes the editor/tree split resizable.

File tree

  • Lists the workspace root and lazily loads expanded directories.
  • Uses a flat, virtualized row projection instead of recursively rendering the tree every frame.
  • Keeps stable selection and expansion state while directory pages arrive.
  • Sorts directories before files with deterministic case-insensitive ordering.
  • Supports keyboard navigation through the visible tree.
  • Represents loading, empty, error, retry, and pagination rows in the tree model.
  • Avoids flashing a transient loading indicator during fast initial loads.
  • Uses compact 27 px rows and the existing Zeron surface, wash, border, hover, and selection tokens.
  • Uses Geist for application chrome and Geist Mono for file content.
  • Removes redundant read-only labels from every tree row; unsupported files instead explain their state when opened.

Search

  • Adds an in-surface Search files input with a 200 ms debounce.
  • Uses the dedicated workspace search RPC rather than changing the existing composer mention search behavior.
  • Projects fuzzy search matches into a virtualized, automatically expanded directory tree containing only matching entries and their real ancestors.
  • Ranks each directory branch by its strongest descendant match, deduplicates shared ancestors, and lets users collapse or expand result groups without loading unrelated children.
  • Rejects stale async responses when the query or workspace generation changes.
  • Preserves keyboard navigation, file opening, directory reveal, and composer drag-and-drop from the hierarchical results.

Hidden and ignored files

  • Adds an eye toggle to show or hide hidden and ignored files.
  • Uses distinct open-eye and closed-eye states with accessible labels.
  • Persists the preference globally and applies it to existing and future file trees.
  • Dims ignored entries and all descendants of ignored directories instead of only dimming the ignored parent.
  • Keeps .git unavailable even when Show all files is enabled.
  • Adds the same preference to Settings → Files.

File previews and tabs

  • Opens supported workspace files directly from the tree or search results.
  • Shows a breadcrumb and compact file actions above the document.
  • Supports multiple simultaneously open documents with stable titles and identities.
  • Keeps read-only previews for files that cannot safely be edited.
  • Supports vertical scrolling and true horizontal trackpad scrolling when wrapping is disabled.
  • Prevents ordinary vertical trackpad movement from being interpreted as horizontal scrolling.
  • Adds a line-style word-wrap control to the file header.
  • Persists word wrap globally and applies changes immediately to every open editor and to editors opened later.
  • Keeps wrapping disabled by default.

Workspace file protocol

The protocol adds five explicit workspace file operations:

RPC Shape Responsibility
ListWorkspaceDirectory unary Lazily list a workspace directory with pagination metadata.
SearchWorkspaceFiles unary Fuzzy-search workspace-relative paths for the Files surface.
ReadWorkspaceFile unary Read and classify an arbitrary workspace file safely.
WriteWorkspaceFile unary Atomically write a file only when its expected content hash still matches.
WatchWorkspaceFiles stream Publish sequenced filesystem changes for live tree and document reconciliation.

The shared contracts model:

  • A workspace target identified by exactly one of chatId or spaceId.
  • Optional registered checkout selection for spaces.
  • Workspace-relative paths normalized with / separators.
  • Explicit file, directory, and symlink entry kinds.
  • Entry size, modification time, ignored state, and read-only state.
  • UTF-8, UTF-8 BOM, binary, and unsupported encoding classifications.
  • LF, CRLF, mixed, and no-line-ending classifications.
  • Typed read-only reasons for binary files, unsupported encodings, mixed line endings, symlinks, oversized files, permission failures, and non-regular files.
  • Content hashes for optimistic concurrency.
  • Typed write outcomes for successful writes and changed, deleted, replaced, or non-regular-file conflicts.
  • Sequenced create, modify, remove, and rename events with explicit resync signaling.

All five methods are relay-forwardable, while WatchWorkspaceFiles is registered as a streaming RPC.

Engine backend

Shared workspace resolution

  • Introduces a dedicated WorkspaceFiles service owned by the engine lifecycle.
  • Centralizes chat/space checkout resolution instead of duplicating authorization logic across handlers.
  • Preserves support for both Git checkouts and registered plain folders.
  • Gives Git checkouts a canonical repository identity and plain folders a stable device-scoped identity.
  • Verifies that the resolved chat or space belongs to the engine's local device after relay routing.
  • Reuses the existing relay infrastructure rather than implementing a separate remote-files transport.
  • Shuts down watchers and background work with the engine.

Path security

  • Accepts only workspace-relative protocol paths.
  • Rejects absolute paths, Windows prefixes, UNC-like paths, backslashes, NUL bytes, . components, and .. traversal.
  • Limits paths to 4,096 bytes and 256 components before filesystem access.
  • Rejects .git components regardless of listing preferences.
  • Uses symlink_metadata and canonical containment checks to prevent traversal through file or parent symlinks.
  • Revalidates the target immediately before writes.
  • Normalizes emitted paths back to the /-separated wire representation.
  • Validates removed watcher paths lexically when canonicalization is no longer possible.

Directory listing and search

  • Runs blocking filesystem traversal on worker threads instead of the async event loop.
  • Supports ignored-file filtering through the existing ignore semantics.
  • Paginates deterministic directory results with cursors tied to a directory fingerprint.
  • Rejects stale cursors when the directory contents change between pages.
  • Caps a directory traversal at 50,000 entries.
  • Caps search queries at 256 characters and result sets at 200 entries.
  • Ranks direct filename matches ahead of weaker nested-path matches.
  • Preserves Unicode workspace paths.
  • Keeps .git filtered even when ignored entries are requested.
  • Makes long-running worker work cooperatively cancellable when an RPC is dropped.

Safe reads

  • Reads regular files without following symlinks.
  • Hashes the original bytes rather than a normalized text representation.
  • Detects UTF-8, UTF-8 BOM, binary data, and unsupported/lossy text.
  • Detects LF, CRLF, mixed, and absent line endings.
  • Allows editing up to 1 MiB.
  • Allows read-only previewing up to 8 MiB.
  • Returns a typed read-only result instead of treating every unsupported file as a generic RPC failure.
  • Prevents a file changed during the read from being accepted as a stable snapshot.

Conflict-aware writes

  • Requires every write to provide the content hash returned by the last accepted read or write.
  • Serializes writes per checkout and path with shared per-file locks.
  • Rechecks the file revision and expected hash before replacing content.
  • Returns a structured conflict instead of silently overwriting external changes.
  • Never recreates a file that was deleted after it was opened.
  • Preserves the requested UTF-8 or UTF-8 BOM encoding.
  • Preserves LF or CRLF line endings.
  • Rejects mixed-line-ending, binary, oversized, or otherwise non-writable sources.
  • Preserves existing file permissions.
  • Writes through a same-directory temporary file and atomically replaces the destination.
  • Cleans up temporary files if the operation fails or is cancelled.
  • Syncs the containing directory where supported after replacement.

Shared filesystem watcher

  • Shares one watcher per checkout across every subscriber instead of creating one watcher per tab or file.
  • Debounces native filesystem events for 100 ms.
  • Normalizes and deduplicates create, modify, remove, and rename sequences.
  • Filters .git and the backend's own temporary write files.
  • Preserves the final state of remove/create bursts.
  • Publishes monotonic sequence numbers so clients can detect dropped frames.
  • Starts subscriptions with a resync frame.
  • Falls back to repair/resync behavior when native watching exceeds the 8,000-directory budget.
  • Tears down a checkout watcher when its final subscriber disappears.

Files UI architecture

The new UI is split into focused modules:

Module Responsibility
files/client.rs Typed RPC calls, remote targeting, retries, and response decoding.
files/model.rs Pure file-tree state, pagination, ordering, selection, expansion, invalidation, and flat visible rows.
files/tree.rs Virtualized tree rendering, keyboard/pointer interaction, and drag sources.
files/search.rs Debounced search state, stale-response protection, result rendering, and drag sources.
files/watch.rs Watch subscription lifecycle, sequence-gap recovery, tree invalidation, and document reconciliation.
files/document.rs Document identity, revisions, dirty state, save snapshots, conflicts, and external-change phases.
files/editor.rs Narrow boundary around gpui-base::EditorState.
files/editor_adapter.rs Theme, caret, and zeron-syntax adaptation for gpui-base.
files/preview.rs Tabs, breadcrumbs, editor/preview rendering, autosave, comments, sidebar behavior, and recovery UI.
files/mod.rs Files surface ownership, target lifecycle, loading, layout, and surface events.

Every asynchronous request captures the effective workspace context plus a generation or document identity. Late reads, searches, highlights, writes, and watcher events are rejected when the active chat, checkout, path, or revision no longer matches.

Editor integration

gpui-base boundary

  • Pins gpui-base from the contributor-owned fork jsgrrchg/gpui-component at commit 8194877275c5d7d3a8eacf3aed7c2cf8c9be8098.
  • Does not use longbridge/gpui-component directly: the fork is required to make gpui-base use the exact GPUI source and revision already used by Comet.
  • Uses an immutable Git SHA rather than a floating branch, so Cargo resolves a reproducible editor implementation.
  • Keeps the dependency isolated to the file editor adapter/document boundary.
  • Does not import Zed's complete editor, project model, language server stack, multibuffer implementation, or GPL editor crates.
  • Reuses editor primitives for Rope-backed text, native keyboard input, selection, IME, clipboard, undo/redo, line numbers, soft wrapping, and scrolling.
  • Keeps the dependency Apache-2.0 and records gpui-base in third-party notices.

Why a fork is required

Rust treats types from different Git sources or revisions as different crate identities even when their source code and names look equivalent.

Upstream gpui-component was resolving GPUI from zed-industries/zed, while Comet resolves GPUI from the Zed fork used by this repository. Depending on both unchanged would produce incompatible gpui::Window, gpui::App, gpui::Entity, element, event, and context types across the application/editor boundary, along with duplicate GPUI packages in Cargo's dependency graph.

The fork intentionally solves that dependency-identity problem at the source. It is not a parallel Zeron product implementation: the fork contains generic GPUI compatibility and caret rendering fixes, while all Zeron-specific tabs, themes, syntax, autosave, RPC, comments, settings, and filesystem behavior remain in Comet.

Exact dependency chain

Comet pins the fork directly:

# Comet/Cargo.toml
gpui-base = {
    git = "https://github.com/jsgrrchg/gpui-component",
    rev = "8194877275c5d7d3a8eacf3aed7c2cf8c9be8098"
}

The fork then pins every relevant GPUI dependency to the same Zed fork and immutable revision used by Comet:

# jsgrrchg/gpui-component/Cargo.toml
gpui = {
    git = "https://github.com/wingleeio/zed",
    rev = "5d1f83d9f27a19bec1fb241dc33b42238af9cf8d"
}

The same wingleeio/zed revision is used for gpui_platform, gpui_web, gpui_macros, and reqwest_client inside the fork. Cargo.lock records the complete gpui-base source as jsgrrchg/gpui-component @ 81948772, preventing Cargo from silently following future branch changes.

The fork history consumed by Comet is:

da4f93696dc2b2b4d91bcc42412b9053a3d24de8  fork main base
└── 1fc58bb1c0b62c1578afd282e771e2d17ded43a3  GPUI/Comet compatibility
    └── 8194877275c5d7d3a8eacf3aed7c2cf8c9be8098  stable physical-pixel caret

Fork modification 1 — align gpui-base with Comet's GPUI

Commit 1fc58bb1c0b62c1578afd282e771e2d17ded43a3, chore(gpui): align gpui-base with Comet GPUI revision, makes the compatibility layer compile and behave against Comet's GPUI source.

This commit:

  • Redirects gpui, gpui_platform, gpui_web, gpui_macros, and reqwest_client from zed-industries/zed to wingleeio/zed @ 5d1f83d9f27a19bec1fb241dc33b42238af9cf8d.
  • Regenerates the fork's lockfile so transitive Zed/GPUI crates such as collections, gpui_util, http_client, media, perf, refineable, scheduler, sum_tree, and related platform crates resolve from that same immutable source.
  • Adapts axis-locked scrolling to the API exposed by Comet's GPUI fork by setting restrict_scroll_to_axis through element style state.
  • Preserves the browser/WASM no-op behavior for axis locking where the native gesture timing implementation is unavailable.
  • Removes the now-inapplicable OngoingScrollExt compatibility surface.
  • Updates GPUI visual tests for the Window::draw(...).clear() signature available in Comet's GPUI revision.
  • Adds a focused Comet compatibility editor fixture that renders the public Editor, edits ASCII/Unicode/emoji and multiline text, exercises selection and cursor movement, verifies undo/redo, applies scroll state, and confirms visible-row layout.

The compatibility commit changes 32 files with 205 insertions and 146 deletions. Most touched component files contain mechanical test-harness API updates; the meaningful runtime compatibility change is concentrated in the GPUI dependency pins and scroll-axis adapter.

Fork modification 2 — stable caret pixel snapping

Commit 8194877275c5d7d3a8eacf3aed7c2cf8c9be8098, fix(editor): snap caret dimensions to physical pixels, fixes a rendering defect discovered while testing Zeron's file editor.

On macOS, gpui-base used a logical caret width of 1.5 px. At a 1× display scale, GPUI could independently round the left and right caret edges after the caret moved to fractional X positions. The resulting physical width alternated between one and two device pixels, which made the caret appear to grow and shrink while typing.

This commit:

  • Introduces a shared cursor_size calculation that snaps caret width and height through Window::pixel_snap before bounds are constructed.
  • Stores the snapped caret size in LastLayout so paint, scrolling, alignment, and clipping use one consistent geometry value.
  • Replaces remaining raw CURSOR_WIDTH assumptions in centered/right-aligned safety margins with the snapped width.
  • Clamps right-aligned caret geometry using its final snapped width so the caret remains fully inside the input bounds.
  • Applies the same geometry path to Input, Textarea, and the code Editor, rather than adding a Zeron-only rendering exception.
  • Updates display-map fixtures with explicit caret geometry.
  • Adds deterministic tests across 1×, 1.5×, and 2× scale factors and multiple fractional X/Y positions.
  • Verifies that the caret's physical width and height remain stable as its logical position changes.
  • Adds visual integration coverage proving that the caret remains non-zero, visible, and within bounds in Input, Textarea, and Editor modes.

The caret commit changes 5 files with 162 insertions and 9 deletions. It is a generic editor rendering correction and does not contain Comet/Zeron product logic.

Combined fork delta

Relative to the fork's main base at da4f93696dc2b2b4d91bcc42412b9053a3d24de8, the exact revision consumed by Comet changes 36 files with 367 insertions and 155 deletions.

The combined delta consists only of:

  1. Immutable dependency alignment with Comet's GPUI fork.
  2. Small API adaptations required by that GPUI revision.
  3. Compatibility and regression tests.
  4. A generic physical-pixel caret fix.

There are no Zeron RPCs, themes, settings, workspace models, file operations, or application-specific UI components in the fork.

Fork maintenance and reproducibility

  • Comet must continue pinning an immutable commit and must not depend on zeron-gpui-compact, fix/caret-pixel-snapping, or any other floating branch.
  • Any future gpui-component update must first rebase or port these two commits onto the desired upstream base.
  • The GPUI revision inside the fork must remain identical to Comet's GPUI revision; changing only one repository can reintroduce duplicate and incompatible GPUI types.
  • Generic fixes such as caret snapping should be upstreamed when practical, but Comet must keep the known-good SHA until an upstream release contains equivalent behavior and is compatible with Comet's GPUI source.
  • The public fork commit must remain reachable for clean Cargo builds and CI.

Dependency documentation follow-up before merge

Cargo.toml and Cargo.lock already point to 8194877275c5d7d3a8eacf3aed7c2cf8c9be8098, but THIRD_PARTY_NOTICES.md still labels gpui-base with the earlier abbreviated compatibility SHA 1fc58bb.

Before opening or merging this PR, update that notice to identify the actual consumed revision 81948772 so the dependency manifest, lockfile, and legal notice agree.

Editing behavior

  • Creates one stable EditorState per open editable document.
  • Uses Geist Mono for file content and Zeron's existing syntax palette.
  • Uses Zeron's shared purple caret token instead of the editor's default black/white caret.
  • Includes a fork-side physical-pixel snapping fix so the caret keeps a stable apparent width while moving across fractional logical positions.
  • Preserves editor keybindings, including Backspace behavior, without consuming unrelated global shortcuts.
  • Restores focus when an existing file tab becomes active.
  • Keeps unsupported files in the existing read-only preview path instead of creating an editable buffer.
  • Keeps horizontal scrolling available whenever soft wrapping is disabled.

Document model

Each open file is keyed by chat, checkout identity, and workspace-relative path rather than by path alone.

The document tracks:

  • The accepted file metadata and on-disk content hash.
  • A monotonic user-edit revision and last-saved revision.
  • Writable encoding and line-ending metadata.
  • The active editor entity and editor subscriptions.
  • Read, highlight, autosave, save, and reconciliation tasks.
  • The exact revision captured by an in-flight save.
  • Pending clean external reloads.
  • Inline review-comment synchronization state.
  • Explicit phases for loading, ready, saving, save failure, external modification, conflict, deletion, read-only content, and errors.

Dirty state is derived from revision numbers instead of comparing full text buffers every frame.

Programmatic loads and clean external replacements establish a new baseline without being treated as user edits.

Autosave and lifecycle safety

  • Uses delayed autosave instead of Cmd-S or Ctrl-S as the primary save mechanism.
  • Defaults to 900 ms of idle time after the latest edit.
  • Offers 300 ms, 600 ms, 900 ms, 1.5 s, and 3 s choices in Settings.
  • Clamps persisted values to the supported 100–10,000 ms safety range.
  • Rearms pending timers when the setting changes.
  • Keeps at most one write in flight per document.
  • Coalesces edits made while a write is active into a later save.
  • Marks a document clean only if the successful response corresponds to the exact revision captured by that write.
  • Preserves newer edits when an older write finishes.
  • Stops automatic retries after a persistent save failure.
  • Exposes stable Retry, Keep Open, and Discard Changes recovery actions instead of hammering the RPC in a tight loop.
  • Attempts to flush dirty documents before closing a file surface, closing the window, or switching workspace targets.
  • Blocks the destructive lifecycle transition if changes cannot be saved safely.
  • Removes transient Saving/Unsaved labels and tab indicators that flashed during normal fast autosave operation.
  • Retains visible status only for actionable failures, conflicts, and deleted-on-disk documents.

Reload and external-change handling

  • Prevents Reload file from silently discarding a dirty buffer.
  • Shows an explicit Discard unsaved changes? confirmation with Cancel and Discard & Reload actions.
  • Pauses autosave while destructive reload confirmation is active and resumes it if the user cancels.
  • Automatically accepts clean external updates after verifying their content hash.
  • Preserves the current selection and scroll offset when applying a clean external replacement.
  • Preserves dirty editor content when the disk changes and offers Keep Editing or Reload from Disk.
  • Converts Keep Editing into an explicit conflict state instead of overwriting disk content later.
  • Defers watcher reconciliation while a save is in flight and checks again after the save resolves.
  • Treats watcher events matching the last saved hash as self-generated or duplicate events.
  • Preserves dirty buffers when files are removed from disk.
  • Reconciles an open DeletedOnDisk document when the same file or a descendant of a recreated directory appears again.
  • Restores normal autosave when a recreated file matches the last known disk hash.
  • Updates open document paths, tab titles, breadcrumbs, comments, and maps when a file or containing directory is renamed.
  • Refreshes the tree and every open document after watcher sequence gaps or explicit resync frames.
  • Removes always-visible tree/file refresh controls because healthy watcher updates reconcile both surfaces automatically; a contextual Refresh now action remains available when the watcher is degraded, while Reload from Disk remains available for external-change conflicts.

Syntax highlighting

  • Reuses zeron-syntax as the single language registry and highlight source.
  • Adapts line-relative neutral highlight spans into the absolute UTF-8 ranges expected by gpui-base.
  • Resolves highlight kinds through Zeron's current light/dark syntax palette.
  • Keeps style runs ordered, non-overlapping, and complete for the requested editor range.
  • Runs highlighting outside the render path.
  • Debounces editor reparsing by 120 ms after edits.
  • Rejects stale highlight results when the document identity, generation, or revision changes.
  • Keeps editing functional as plain text when highlighting is unsupported or exceeds syntax limits.
  • Extends Markdown highlighting with block and inline semantics for headings, raw/code content, links, references, emphasis, and strong text.
  • Preserves fenced-code highlighting through the bundled child grammars.

Inline review comments in editable files

  • Extends the existing diff review-comment infrastructure to ordinary editable files.
  • Replaces the hovered gutter line number with the centered + affordance used to begin a comment.
  • Renders the draft as a floating frosted popover instead of inserting a full-width block between source lines.
  • Uses a compact 12 px editor-only comment input while leaving diff comment typography unchanged.
  • Keeps submitted comments as small gutter bubbles.
  • Expands a submitted bubble into a frosted comment card when clicked.
  • Allows comments to be removed from the expanded card.
  • Anchors comments to editor text ranges so their line locations follow edits where possible.
  • Handles normal lines and trailing empty lines.
  • Keeps comment overlays inside the visible editor viewport and adapts their width in narrow layouts.
  • Generalizes staged diff comments into shared review comments that can represent either a diff side or a current workspace file.
  • Sends file comments to the composer as path:line references without synthetic left/right diff tags.
  • Supports mixed diff and file comments in one review block.
  • Projects submitted comments into the existing composer/transcript badge UI.
  • Prevents sending while editor comment locations are still being synchronized.

Drag files into the composer

  • Makes file-tree rows draggable.
  • Makes folder rows draggable without recursively enumerating their contents.
  • Makes file search results draggable.
  • Makes file tabs draggable as attachments while preserving their existing tab-reorder behavior.
  • Accepts internal file, folder, and file-tab drops across the conversation column.
  • Inserts the reference at the composer's current selection and restores composer focus.
  • Uses the existing strict zeron-file: Markdown transport and the existing projected @file/@folder chip appearance.
  • Keeps references workspace-relative, which makes the same payload valid for local and remote chats.
  • Rejects absolute paths and parent traversal before insertion.
  • Adds safe separators around a dropped chip and records the insertion as one undoable composer edit.
  • Uses a compact raised-surface drag ghost consistent with right-pane tabs.
  • Generalizes the overlay copy from Drop images to attach to Drop to attach.
  • Preserves the existing external image attachment pipeline unchanged.

Settings

Adds a dedicated Settings → Files page with:

Setting Default Behavior
Autosave delay 900 ms Applies to every open and future editable workspace file.
Word wrap Off Applies immediately to every open and future file editor/preview.
Show all files Off Applies to every existing and future file tree.

The settings use the existing UiSettings store, backward-compatible serde defaults, atomic persistence, and Shell's existing debounced settings save. No second configuration file or per-editor settings store is introduced.

Visual and interaction polish

  • Follows the existing Zeron glass/transparency language instead of introducing a separate editor theme.
  • Uses theme colors and shared wash/ink helpers rather than hard-coded reference colors.
  • Keeps application UI in Geist and source content in Geist Mono.
  • Keeps the file search header and eye toggle aligned at the same compact height without persistent refresh chrome.
  • Gives every shared composer input a stable accessibility node with an ID, a placeholder-derived label, and the correct search, single-line, or multiline text-input role.
  • Uses accessible labels for contextual recovery, show/hide files, wrapping, sidebar visibility, comment actions, and retry actions.
  • Keeps action buttons available without persistent noisy labels.
  • Removes transient loading and save status flashes because local operations normally complete faster than those states can be meaningfully read.
  • Keeps error and recovery states visible when user action is required.

Performance characteristics

  • The tree renders a flat virtualized list with uniform row heights.
  • Search results are virtualized independently.
  • Directories load lazily rather than scanning the entire workspace before first paint.
  • Blocking filesystem work runs on worker threads and supports cancellation.
  • Watchers are shared per checkout, not multiplied per file or tab.
  • Search, syntax, autosave, and filesystem events use bounded debounce windows.
  • Syntax highlighting remains outside the render thread and stale parse results are discarded.
  • Dirty state uses revisions instead of full-buffer comparisons.
  • Folder drag-and-drop is O(1) because it attaches a workspace-relative reference rather than walking the folder.
  • Global wrap and hidden-file preference changes update existing surfaces in place without recreating the underlying workspace model unnecessarily.

Supported file matrix

Input Preview Edit Save behavior
UTF-8, up to 1 MiB Yes Yes Preserves LF/CRLF and uses optimistic concurrency.
UTF-8 BOM, up to 1 MiB Yes Yes Preserves BOM and line endings.
UTF-8, 1–8 MiB Yes No Read-only TooLarge preview.
Larger than 8 MiB Metadata state No No content is materialized into the UI.
Mixed line endings Yes No Read-only to avoid destructive normalization.
Binary Classified state No No lossy decoding.
Unsupported encoding Classified state No No lossy conversion.
Symlink Classified state No Never followed by workspace file operations.
Non-regular file Classified state No No read/write attempt.

Test coverage

The implementation adds unit and integration coverage for:

  • Protocol serialization and typed write outcomes.
  • Safe relative path parsing, Unicode paths, and traversal rejection.
  • Directory ordering, pagination, stale cursors, ignored files, and .git filtering.
  • Search ranking and result bounds.
  • Encoding, BOM, line-ending, binary, size-limit, and symlink classification.
  • Atomic writes, permission preservation, expected-hash conflicts, deletion conflicts, and invalid sources.
  • Watcher normalization, deduplication, rename handling, shared lifecycle, cancellation, and repair-only mode.
  • Local and relay-forwarded end-to-end file RPC behavior.
  • Pure tree model expansion, selection, pagination, invalidation, stale generations, and large monorepo projections.
  • Typed UI client routing and conflict decoding.
  • Document revisions, stale results, dirty state, save snapshots, conflicts, external reloads, deletion, recreation, and explicit discard.
  • Search stale-response protection and watch sequence-gap detection.
  • File tab identity and Files surface single-instance behavior.
  • Editor highlight range conversion and caret theme integration.
  • Markdown block/inline highlighting and fenced child languages.
  • Reload confirmation, failed-save lifecycle recovery, responsive sidebar preference, directory renames, recreated documents, and comment overlays.
  • Review comment serialization, mixed diff/file comments, composer badges, and dropped workspace mentions.

Commands verified on this branch

cargo test -p zeron-engine --test workspace_files
# 2 passed

cargo test -p zeron-engine --test device_routing workspace_file_surface_proxies_over_the_relay
# 1 passed

cargo test -p zeron-syntax
# 16 unit tests passed; 3 quality tests passed; 1 diagnostic benchmark ignored

cargo test -p zeron-ui --lib
# 497 passed

cargo check -p zeron-ui --locked
# passed

git diff --check
# passed

cargo check still emits the existing unexpected cfg cargo-clippy warnings from Objective-C macros; the affected notification and appearance code predates this branch.

Cargo also reports existing future-incompatibility warnings for block v0.1.6 and proc-macro-error2 v2.0.1; both exact versions are already present on upstream/main and are not introduced by this PR.

Suggested manual QA

Local workspace

  • Open Files from the right-pane picker.
  • Expand and collapse nested directories with mouse and keyboard.
  • Search for files by base name and nested path.
  • Create, modify, rename, and remove files externally and confirm the tree reconciles automatically through the workspace watcher.
  • Interrupt the watcher and confirm the contextual Refresh now recovery action reconciles the tree and open documents.
  • Toggle Show all files and verify hidden/ignored descendants dim consistently.
  • Confirm .git is never exposed.
  • Open the first file and confirm the Files tab is reused and renamed.
  • Open additional files and switch among their tabs.
  • Confirm returning to a file tab immediately restores typing focus.
  • Reorder file tabs.

Editor

  • Type ASCII, Unicode, emoji, and composed/IME text.
  • Verify Backspace, selection, copy/paste, undo, and redo.
  • Confirm the purple caret keeps a stable width while moving.
  • Toggle word wrap and verify every open editor updates.
  • Open another editor and verify it inherits the current wrapping preference.
  • With wrapping disabled, verify horizontal trackpad scrolling.
  • Verify vertical scrolling does not drift horizontally.
  • Open Markdown and confirm headings, links, emphasis, strong text, inline code, and fenced code are highlighted.

Autosave and conflicts

  • Edit a file and confirm it saves after the configured idle delay.
  • Change the delay in Settings and confirm open editors adopt it.
  • Edit continuously and confirm saves are coalesced rather than sent per keystroke.
  • Change a clean open file externally and confirm the editor updates without losing cursor/scroll position.
  • Change a dirty open file externally and verify Keep Editing / Reload from Disk recovery.
  • Choose Reload from Disk for a dirty externally modified file and verify explicit discard confirmation.
  • Delete an open file and confirm the buffer is preserved with a stable recovery state.
  • Recreate the deleted file and confirm the document reconciles.
  • Force a save failure and confirm there is no immediate infinite retry loop.
  • Close the tab/window with dirty content and verify flush/retry/keep/discard behavior.

Layout and settings

  • Resize through the narrow/wide breakpoint.
  • Hide the tree in wide mode and verify it stays hidden after resize cycles.
  • Show it again from the editor header.
  • Resize the editor/tree split.
  • Restart the app and verify autosave delay, wrapping, and hidden-file preferences persist.
  • Check dark, light, and glass appearances.

Review comments

  • Hover a line number and confirm the centered + replaces the number.
  • Open a draft and verify the floating frosted card does not move source lines.
  • Submit a comment and confirm it collapses to a gutter bubble.
  • Reopen and remove the submitted comment.
  • Edit above a comment and verify its anchored line follows the text.
  • Stage both diff and editable-file comments and send them together.
  • Verify the composer/transcript badge shows correct file and line locations.

Drag-and-drop

  • Drag a file-tree file to the composer and verify an @file chip appears at the selection.
  • Drag a folder and verify an @folder chip appears without a recursive scan.
  • Drag a search result to the composer.
  • Drag a file tab to the composer.
  • Confirm ordinary file-tab reordering still works.
  • Confirm external image drops still use the image attachment pipeline.

Remote workspace

  • Open Files for a chat whose checkout belongs to another connected device.
  • List, search, preview, edit, and autosave through relay routing.
  • Receive external filesystem updates from the owning device.
  • Drag a remote workspace reference into the composer and verify it remains workspace-relative.
  • Disconnect the owning device and verify stable retry/error states.

Scope deliberately excluded

  • Creating, renaming, moving, or deleting files and directories from the UI.
  • Git staging or source-control actions from the file tree.
  • LSP completions, diagnostics, hover, go-to-definition, rename symbol, and code actions.
  • Multi-cursor editing, minimap, semantic outline, and semantic breadcrumbs.
  • Binary editing or automatic conversion of unsupported encodings.
  • Automatic normalization of mixed line endings.
  • Persisting dirty buffers across a complete application restart.
  • Loading Zed's complete editor or GPL editor crates.
  • Dynamic grammar/plugin downloads.
  • Treating Cmd-S/Ctrl-S as the primary save workflow.
  • Attaching arbitrary non-image files from Finder; this PR's new reference drag flow originates from Zeron's internal workspace tree, search results, and file tabs.

Risks and mitigations

Filesystem safety

The feature expands Zeron from read-only arbitrary-file access to write access. The backend mitigates this with target authorization, strict relative-path validation, symlink rejection, checkout containment checks, expected-content hashes, typed conflicts, per-file serialization, and atomic replacement.

Remote consistency

Relay delays can reorder user intent and filesystem notifications. Requests carry workspace/document generations and revisions, writes are conditional on hashes, watch frames are sequenced, and sequence gaps trigger full reconciliation.

Autosave data loss

Autosave is intentionally conservative: conflicts never overwrite disk content, dirty reloads require confirmation, close/workspace transitions wait for safe resolution, failed writes stop automatic retries, and deleted files preserve their in-memory buffers.

Large repositories

The tree and search are virtualized, directories load lazily, search/list results are bounded, filesystem work runs off the event loop, and watchers enter repair mode when the native directory budget is exceeded.

External editor dependency

Only the Apache-2.0 gpui-base editing primitives are consumed from a pinned immutable fork commit. Product behavior remains in Comet, the fork is aligned with Comet's GPUI source, and the dependency does not bring in Zed's full editor architecture.

Reviewer guide

For a structured review, the implementation can be read in this order:

  1. crates/proto/src/entities.rs and crates/rpc/src/lib.rs for the wire contract.
  2. crates/engine/src/workspace_files.rs for target resolution, path safety, reads, writes, and watcher behavior.
  3. crates/engine/src/rpc.rs, crates/engine/tests/workspace_files.rs, and crates/engine/tests/device_routing.rs for wiring and end-to-end coverage.
  4. crates/ui/src/files/client.rs and crates/ui/src/files/model.rs for the typed UI boundary and pure tree model.
  5. crates/ui/src/files/tree.rs, search.rs, watch.rs, and mod.rs for browsing and lifecycle behavior.
  6. crates/ui/src/files/document.rs, editor.rs, editor_adapter.rs, and preview.rs for editor state, autosave, conflicts, syntax, comments, and responsive layout.
  7. crates/ui/src/settings/files.rs, crates/ui/src/settings.rs, and crates/ui/src/shell.rs for product integration and persistence.
  8. crates/syntax/src/lib.rs for Markdown highlighting extensions.
  9. crates/ui/src/comments.rs, composer.rs, changes.rs, and state.rs for shared review comments and workspace-reference drag-and-drop.

Change size

  • 35 tracked files changed relative to upstream/main.
  • Approximately 11,391 lines added and 225 removed.
  • New backend service, protocol contracts, integration tests, Files UI module tree, settings page, icons, editor adapter, and document lifecycle model.

Screenshots / recordings

Captura de pantalla 2026-08-17 a las 05 19 31 Captura de pantalla 2026-08-17 a las 05 19 26 Captura de pantalla 2026-08-17 a las 05 19 06

@jsgrrchg

jsgrrchg commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@wingleeio I added two intentionally failing regression tests that reproduce the stale syntax-highlighting race. One shows that a highlight result for revision 0 is still accepted after the editor advances to revision 1, and the other demonstrates how the resulting stale byte ranges split the UTF-8 boundaries of updated Unicode text. No production fix is included yet, I'll work on this tomorrow, currently this is causing sporadic panic crashes.

@jsgrrchg
jsgrrchg requested a review from wingleeio September 6, 2026 17:06
# Conflicts:
#	Cargo.toml
#	THIRD_PARTY_NOTICES.md
#	crates/ui/src/composer.rs
#	crates/ui/src/settings.rs
#	crates/ui/src/shell.rs

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on 131e891f5380384d74582338571c8213453f2b16 for a reproduced write-target bug.

Blocking finding

[P1] Pending edits can be saved into a different checkout after the chat's cwd changes.

I reproduced this in the unmodified headed Linux app with an isolated repository and a registered linked worktree containing the same initial README.md:

  1. Open README.md from the original checkout and edit it with autosave disabled. Confirm both on-disk copies are still unchanged.
  2. Change the same chat's cwd to the linked worktree through Mutate { op: "setChatCwd", ... }.
  3. The Files surface automatically flushes its pending buffer and clears the dirty indicator. Inspecting disk shows the original checkout is unchanged, while the new worktree received the edit made against the original checkout.

sync_target notices the changed context and calls save_document before applying it. However, the write request only identifies the chat and relative file path; the engine resolves that chat's already-updated cwd. Matching initial content hashes allow the write to succeed against the wrong file. The response-generation checks happen after the write and cannot protect it.

Please bind pending writes to the original resolved checkout identity, verify that identity on the owning engine, and preserve/block the dirty buffer when the target changes rather than flushing through a mutable chat target. Add a regression covering two registered checkouts with identical starting file contents.

Validation

  • cargo test -q --locked -p zeron-engine --lib workspace_files::tests — 25 passed.
  • cargo test -q --locked -p zeron-ui --lib files:: — 60 passed.
  • cargo test -q --locked -p zeron-engine --test workspace_files --test device_routing — 8 passed.
  • cargo build -q --locked -p zeron — passed.
  • git diff --check origin/main...HEAD — passed.
  • Headed Linux smoke test: browsing, opening multiple file tabs, syntax rendering, explicit save, and preservation of a dirty buffer on external modification worked. The checkout-change scenario above failed.
  • Existing UI and macOS frame-recovery CI checks are green. Native macOS interaction was not exercised locally.

Screenshots

These are captures from the actual PR build, uploaded as GitHub user attachments.

Before the checkout change: the dirty dot is visible on README.md, and the composer footer still says Local checkout.

Pending edit in the original checkout

After the checkout change: the footer says Worktree and the dirty dot is gone. Disk verification confirmed the pending edit landed in this new worktree while the original file remained unchanged.

Pending edit saved into the new worktree

Normal explicit save: the source document was saved successfully and its exact contents verified on disk.

Source editor after an explicit save

External modification: the warning appears and the unsaved editor buffer is retained.

External-change warning preserves the editor buffer

Comment thread crates/ui/src/files/mod.rs Outdated
@wingleeio

Copy link
Copy Markdown
Contributor

Pushed the Files UI polish in 83b8ef8.

  • Moved the resize hit area over the divider so the editor and tree borders meet without a gap.
  • Unified header heights, padding, icon/button sizing, and hover styling with the surrounding pane controls.
  • Added toolbar tooltips and preserved editor focus when using toolbar actions.
  • Let warning messages and actions wrap in narrow panes.

Validation: all 60 Files UI tests passed, the application build passed, and the live Linux app was checked at wide and narrow widths, including divider dragging and toolbar focus. These changes address presentation; the previously reported checkout write-target blocker remains open.

Joined borders and resized sidebar

Files UI with continuous header borders and resized sidebar

Narrow pane with wrapping warning actions

Narrow Files pane with visible warning actions

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed 15f3a55d: Files, Diffs, and History now share the 38px header height, 24px controls, 6px corners, and 4px action spacing. Git file/summary subheaders follow the same height. History search text is centered, and the expanded field shrinks at the minimum sidebar width so refresh and view controls stay accessible. Files warning text wraps within its pane.

The earlier P1 checkout-switch finding is fixed. Reads return their checkout identity; writes must carry that identity and the engine rejects a changed target before touching disk. Switching workspaces preserves dirty buffers and pauses saves. Returning to the original workspace permits saving. The write RPC now requires expectedCheckoutId from the read response, so requests without it fail closed.

Validation: native Linux build; all 726 UI tests, 25 workspace-file engine tests, and 9 workspace-file/device-routing integration tests passed. The regression uses two registered worktrees with identical file contents and verifies neither is modified by a stale save. Native checks covered search, save, workspace switching, warning wrapping, diff folding/split view, History search at wide/minimum widths, and terminal input. GitHub CI is running; macOS was not tested locally.

Screenshots below are native captures uploaded as GitHub user attachments.

Files

Files

Diffs and Git subheaders

Diffs and Git subheaders

History search

History search

Terminal

Terminal

Files: preserved edits and wrapped warning in a narrow pane

Files: preserved edits and wrapped warning in a narrow pane

History: all toolbar actions remain visible at minimum width

History: all toolbar actions remain visible at minimum width

@wingleeio

Copy link
Copy Markdown
Contributor

Adjusted the History graph gutter: the first lane center now aligns with the 16px header text inset. Reduced the extra trailing graph space by the same amount, preserving commit-column alignment and responsive graph width.

Validation: native Linux build and screenshot; all 36 History tests passed.

History graph with balanced left spacing

@wingleeio

Copy link
Copy Markdown
Contributor

Updated the default shortcuts: Cmd+S saves the active workspace file, Cmd+B toggles the left sidebar, and Cmd+R toggles the right sidebar (Ctrl on Linux/Windows). All three are configurable in Settings → Shortcuts. Existing default panel bindings migrate; custom bindings remain intact. Closing the focused file pane now restores keyboard focus so the right-sidebar shortcut can reopen it immediately.

Centered the History author placeholder in the avatar’s full inner width with an explicit font and line height.

Validation: native Linux build; all 727 UI tests passed, including shortcut migration, reset, platform spelling, and conflict checks. Native checks verified saving with both the default and a custom recorded binding, persistence, both sidebar toggles, and repeated close/reopen from a focused editor. An unrelated transcript-selection test failed on the first full run and passed on rerun. macOS was not tested locally.

Centered author initial

Centered author initial

Configurable shortcuts (Linux uses Ctrl)

Configurable shortcuts (Linux uses Ctrl)

File saved using the keyboard shortcut

File saved using the keyboard shortcut

@wingleeio

wingleeio commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Removed the save icon. Files now use a neutral dirty dot on the tab; it disappears after a successful save. Pending edits remain marked with autosave enabled. The toolbar shows messages only for save failures, conflicts, or external disk changes; failed saves remain retryable. Cmd+S / the configured Save file shortcut handles saving.

Validation: native build, all 726 UI tests passed, and native edit → dirty dot → shortcut save → clean tab checked against the file on disk.

Unsaved file

Unsaved file

After saving

After saving

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes after auditing 8bd809302140ad200051098c26880d966d10ca3a. I cannot recommend merging or releasing this revision because the normal Quit action can discard unsaved file edits.

Three findings are attached inline: a data-loss path through application lifecycle actions, a strong reference cycle retaining closed editors, and shortcut collisions introduced when migrating a valid customized keymap.

Validation:

  • Ran cargo test --locked --no-fail-fast -p zeron-engine -p zeron-proto -p zeron-rpc -p zeron-syntax -p zeron-ui: 1,083 passed, 1 failed, 5 ignored. The failure, rpc_surface_over_in_memory_transport, reproduces unchanged on current main (e7ae37ea): unexpected contextUsage: null. It is not attributed to this PR.
  • Both required CI jobs are green on the audited head.
  • Reproduced Quit data loss in the native Linux UI with autosave disabled. A temporary Linux binding invoked the existing app_menus::Quit action (normally bound to Cmd+Q on macOS); the process exited and the edited marker was absent from disk. Cmd+W's bypass was established by source tracing, not a macOS runtime test.
  • A focused audit-only test through UiSettings::load reproduced [SaveFile, NewSession] conflicts from a previously valid custom keymap.
  • Reviewed checkout identity enforcement, file path handling, atomic writes/conflicts, RPC routing, editor lifecycle, shortcuts, and release workflow. The previously reported wrong-checkout save is addressed, but that does not cover the new lifecycle finding.

Temporary audit instrumentation was removed; no source changes are included in this review. No merge, tag, or release was performed.

Native UI immediately before the reproduced Quit (dirty tab dot visible; screenshot text names the original Close scenario):

Unsaved editor buffer before Quit

Comment thread crates/ui/src/lib.rs
Comment thread crates/ui/src/files/editor.rs Outdated
Comment thread crates/ui/src/settings.rs

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed all three audit findings in 4259e08c.

  • Quit and Close now defer out of the current window dispatch, then wait for file saves before destroying windows. Runtime changes and update installation use the same guard. Failed/conflicting saves keep the app open and reveal the affected file; Keep Open cancels the pending exit. A macOS application delegate guard also routes Dock/system Quit through this decision, before AppKit commits to termination.
  • The editor context-menu callback captures a weak editor reference. A GPUI regression verifies the actual editor entity is released after its final owner drops.
  • Legacy shortcut migration reserves customized bindings first. New bindings use an available fallback, or remain unbound if all candidates are occupied; they never silently take an existing custom chord. Tests cover conflicts and persisted round trips.

Validation: 729 UI tests passed with --test-threads=1, including actual Quit/Close action dispatch with pending and failed saves, runtime/update guards, cancellation, editor lifetime, and shortcut migration. The parallel run hit the previously observed unrelated transcript-selection flake; the full serial suite passed. The production binary builds successfully. Native Linux checks used temporary bindings to the existing Quit action; those bindings were removed and the production binary rebuilt. New Linux/macOS CI jobs are running.

With autosave disabled, native Quit wrote the exact edited content to disk and then exited:

Dirty editor before successful save-on-quit

A conflicting disk edit blocked Quit and preserved both the unsaved buffer and the external file:

Quit blocked by a disk conflict

Keep Open cancelled the exit and retained the editable buffer:

Editor retained after cancelling Quit

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported audit findings are addressed in 4259e08 and the Apple Silicon BOOL correction in 24bcc41. Approved after rechecking the final head: Linux UI CI and macOS application/native frame-recovery CI are green; 729 local UI tests pass, and 736 UI tests pass on the clean merge with current main. Native Linux checks confirm Quit saves the exact edited content before exit, blocks on a disk conflict without overwriting external changes, and preserves the buffer when exit is cancelled. Screenshots are attached in the preceding review. No remaining blocking findings from this audit.

@wingleeio
wingleeio merged commit 03250d0 into zeronsh:main Sep 8, 2026
3 checks passed
@wingleeio

Copy link
Copy Markdown
Contributor

Merged and released in v0.2.47.

The release workflow and final main CI are green. All four public updater downloads match the published SHA-256 manifest; latest.txt and the manifest both report 0.2.47. The packaged Linux x86_64 binary passed a CLI smoke test. Apple accepted and stapled notarization for both the macOS app and DMG. Fix screenshots remain attached to the preceding review.

hoangvu12 added a commit to hoangvu12/roboco that referenced this pull request Oct 13, 2026
…wing (ui-parity zeronsh#130)

TranscriptState.apply now captures a ScrollAnchor (the row under the
header's bottom edge, probed at offset + topInset clamped at 0) before
the frame swap while not following, and restores it after 68/69's
onFrameApplied runway height when needsAnchor holds: the target is the
anchored row's new y plus the old delta, single row, no fallback, 0.5 dp
dead band, clamped to [minOffset, maxOffset] and written through
viewport.setOffset so an in-flight fling keeps its momentum. Pure math
lives in ScrollAnchor.kt over the AnchorRows seam (LayoutFrame in
production, fakes in tests).

Tests: ScrollAnchorTest (plain JUnit, the ticket's worked-example
table, probes, clamp, dead band, runway precedence, resting finger),
ScrollAnchorFlingTest (Robolectric, fling continuity across a shift),
ScrollAnchorLabTest (instrumented, real core, streaming/prepend/rewrap).
Not run locally (VPS/CI).
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.

4 participants