Repository navigation
feat(history): change history and revisions - #133
Merged
Merged
Conversation
Every edit, structural change, undo, redo and note change is recorded in __history/__history_changes, keyed by row id and column name, with its origin and author. Entries are written in the same transaction as the data they describe. Column moves are now undoable instead of clearing undo, rename undo re-keys notes and settings, and restore_to/restore_value put the grid back by identity as one undoable step. Refs #132
A right-dock panel lists every change newest first: unsaved changes on top, saved ones under day headers, with bursts of edits by one person collapsed. Right-clicking a change restores the project to it, restores a single value, or names the version; the header filters by kind or to named versions and can clear the history. Saved layouts that predate a panel now adopt it into its default dock instead of losing it. Refs #132
Details gains a collapsible History section listing the selected item's changes, old value beside new, with a button to restore a saved value. The cell menu's "Show edit history" brings the History panel forward narrowed to that cell, following its column back through renames. The author setting now says it signs changes as well as notes. Refs #132
devnull03
force-pushed
the
feat/change-history
branch
from
September 14, 2026 21:14
9ce33bd to
2800df6
Compare
Every change in the History panel and the Details section now reads with its date as well as its time, unsaved ones included. Truncated summaries show their full text on hover, like the Problems panel. The panel's status-bar icon is Lucide's history clock, shipped as an app asset, so PanelMeta now names an icon by asset path. Refs #132
The status bar already carries one button per panel and is the real switcher, so a dock holding two tools drew a tab strip that said the same thing twice. Each panel now reports `visible` only while it is the one its group is showing, which is the gate the skin already uses to draw a plain title bar in place of the strip. `set_active` is driven off the group's active index rather than off visibility, so exactly one panel per dock ever answers yes and there is no feedback loop. Nothing in PanelRegistry changes: `toggle` already brought a hidden panel to the front and closed the dock on the shown one. History also drew its own ellipsis button beside the one the tab bar always draws. Its item moves onto `Panel::dropdown_menu`, which the library builds from whichever panel is on screen, so the menu follows the dock's contents for free.
… contents Dragging the History handle up hid the Notes header entirely, and the fields painted through the section below it. The three splits nest, but each pane declared a minimum with no reference to what it holds: the history split's first pane floored at the fields' own 80px while holding the fields *and* Notes, and the preview split's second pane took the 100px default while holding all three sections. The resizer shrank them past what their children fit in, and nothing clipped the surplus. The floors were not even the ones written. gpui-base applies a caller's style refinement and then overwrites min_h with size_range.start, and the drag clamp reads size_range too, so `min_h` on a resizable_panel binds nothing at either end. Those panes never set a range, so both ends used the 100px default. Every floor is now the sum of its contents, via size_range, and a collapsed section is boxed at its real height (header plus the rule it draws above itself) rather than 1px short. The panel clips at its own edge so a floor that is still wrong truncates a section instead of drawing two over each other. Also carries the Details panel's own set_active/visible hooks from the previous commit: the two topics share this file and splitting it would have meant rewriting it.
The change log is append-only and a working project accumulates one entry per edit, so every read path that scaled with the whole log rather than with the page being shown was a stall waiting to happen. Reading a page ran one `WHERE entry_id = ?` per entry, and the only index on `__history_changes` was `(row_id, column_name)` — so a page of 200 was 200 full scans of every change ever recorded. The changes for a page now come back in one query, and the table carries an `(entry_id, seq)` index. Existing projects pick the index up at their next write; the batching already removes the N+1 without it. The panel's unforced refresh re-read every page the reader had loaded, so writing a note cost more the further back they had paged. It now re-reads only the newest page and splices it onto the pages below, which the log being append-only makes sound. A forced reload — a rename, a clear — still re-reads the lot. The Details panel's History section built and formatted every line even while collapsed, when all it draws is a header strip; it now asks only whether anything is behind it.
The batched change fetch distributes one query's rows across the entries of a page, so an entry holding several changes is what catches a distribution that mixes two entries together or loses the sequence they were written in. Nothing covered that: every entry the existing tests write holds exactly one change, and a reordered pair reads as the wrong before-value for a restore.
Every loaded entry became an element on every frame — a hover, a scroll, a keystroke in the grid each rebuilt the whole list. One page is 200 entries and "Load older" has no ceiling, so the panel got heavier the longer a researcher worked and the further back they looked. The list is now flattened to one addressable row per line and drawn through `v_virtual_list`, which builds only the visible range. The rows hold indices into the filtered view rather than text, so formatting a description — the expensive part — happens for the rows on screen and nowhere else. The list places rows by fixed heights and gives the rows those same heights, rather than measuring: measuring means building every row to ask how tall it is, which is the cost this removes. The constants and the rows they belong to have to be changed together, which is what the note on them says. The unsaved entries' local times were already fetched in one call; they now happen once per render rather than once per row built.
The log had no retention path at all: it only ever grew, and "Clear History…" was the single lever — all of it or none. A collection worked on for months has no way to bound its `.qrate` file short of throwing the whole audit trail away. Retention is off by default, because this log is an audit trail and deleting from it is a decision the archivist makes rather than one qrate makes for them. Set a limit and the oldest changes go, except that a named version is never dropped: a name is the archivist saying this is the entry to come back to, so the rule they set to bound a file must not be what takes it away. Named entries also still count towards the number kept, so turning a limit on can never cost more than the number it names. Pruning happens when the project opens, not as entries are written. The History panel holds the page it last read, and pruning underneath it would leave the list pointing at entries that are no longer there.
Converting a unix timestamp to the archivist's local day and time needs timezone rules, and this crate had no date library — so it borrowed SQLite's calendar. Saved entries got theirs as three extra `localtime` columns on the page query; entries still in memory had no query to ride on, so `local_times` opened a brand-new empty in-memory database purely to do the arithmetic, once per timestamp, on every render. chrono is already in the tree through calamine with `clock` enabled, so one function converts both now and nothing new is compiled. The clock is read once per batch rather than once per row, the page query loses three columns, and a saved entry and an unsaved one agree about what day a change was made on because they are converted by the same code. An hour a clock change repeated resolves to its first reading rather than refusing to be a time; one a clock change skipped is shown as the UTC it was recorded as rather than not shown at all.
The lint step has been a no-op since the remap was configured. Cargo runs a linted crate as `<wrapper> <clippy-driver> <rustc> <args>` and clippy-driver copes by dropping the rustc path, but not once the remap has rewritten it — so the crate sees that path as a second input file and rustc stops with "multiple input filenames provided" on the first crate it reaches. A plain crate lints fine; every qrate worktree sets the remap, which is why this only shows up here. `[env]` in `.cargo/config.toml` does not force, so an empty value in the environment wins while zccache stays the wrapper and keeps caching. Only this step loses the cross-worktree sharing the remap buys; the test step below it, and every ordinary build, keep it.
One tracked path was holding a different plan on every branch, so each planning branch collided with main on it as a matter of course — three open PRs conflict on this file and nothing else. Nothing references it, and git history keeps every version that was ever in it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #132 (Notion ID: 3da21d32-b13b-81f8-bc41-e0ad8b446aa8)
Summary
settings::history): every data and note change — cell edits, row/column add/delete/rename/move, notes, undo/redo — recorded in__history/__history_changesinside the.qratefile, keyed by row id and column name, with origin (typed, paste, fix label, …) and author. Written in the same transaction as the data, so unsaved changes that are discarded leave no entries.table::restore_toreverses everything after an entry by identity as one undoable step and logs it as a new entry (append-only);restore_valueputs one cell back.__columns.Verification
./scripts/ci.sh(fmt, clippy-D warnings,cargo test --workspace) passes locally. New tests cover log round-trips, restore across a mixed edit sequence, undo logging, paging, naming, clearing, rename-following and cell narrowing. Not verified: the new UI has not been clicked through in the running app.🤖 Generated with Claude Code