fix(0.2.1): sidecar autosave + close/open UX prompts (Phase 2 + 3)#103
Merged
Conversation
Switch the 1 s debounced autosave from `save_file(path, content)` to `save_buffer(path, content)`. The sidecar lives at `<path>.scrybe-buffer` and accumulates unsaved keystrokes; the real file is committed only by explicit save (Ctrl+S, 💾 toolbar button, `scrybe save <path>` CLI, or the MCP save tool). Why --- Phase 1 stopped programmatic loads from triggering a self-write window in the OS file watcher. That fixed the immediate "edit-while-tab-open clobber" bug but left the fundamental coupling intact: every keystroke still raced the watcher's 2 s self-write filter. With this change the autosave path never touches the real file, so external edits and our own keystrokes are physically segregated — the bug class goes away by construction, not by timing. What changes ------------ Backend (`src-tauri/src/lib.rs`): - New `save_buffer(path, content)`: writes the in-progress edits to `<path>.scrybe-buffer`. The real file is untouched. - New `clear_buffer(path)`: removes the sidecar; silent no-op when absent. Wired into `remove_backup` so tab close cleans up. - New `read_buffer_if_exists(path)`: returns `Some(buffer)` only when a non-empty sidecar exists AND differs from the real file. The caller decides whether to restore — Phase 3 will surface this as a "recover unsaved edits?" prompt. - `save_file` now removes the sidecar after a successful write (the buffer is now stale once committed to disk). - File watcher event loop now filters by `watched` membership, so sidecar writes (sibling file in the same dir) cannot accidentally emit `scrybe://file-changed`. The macOS watcher is directory-grained so this guard matters even though notify is configured non-recursive. Frontend (`src/main.ts`): - Autosave debounce calls `save_buffer`, drops `note_autosave` and `markClean` (tab stays `isDirty: true` until explicit save flushes the sidecar to disk). - Explicit save (`saveActiveTabNow`, `Ctrl+S`, CLI `scrybe save`) and MCP-driven save are unchanged: still `save_file` + `note_autosave` + `markClean`. Tests (`src-tauri/src/lib.rs::tests`): - 9 new unit tests using `tempfile::tempdir` covering: sidecar write vs real file, save_file clears sidecar, clear_buffer (present and absent), read_buffer (none / matches / differs / empty), remove_backup cleans both files. `.gitignore`: add `*.scrybe-buffer` so autosave artifacts can't be committed. Out of scope for this PR ------------------------ - Recovery-on-open prompt: `read_buffer_if_exists` is wired but no caller surfaces a prompt yet. That's Phase 3. - Dirty-on-close prompt: closing a dirty tab still discards the buffer silently. Also Phase 3. - Three-way merge UI for buffer vs disk conflicts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 made autosave write to a `<path>.scrybe-buffer` sidecar instead of the real file, but two UX gaps were left for Phase 3: 1. Closing a dirty tab silently dropped the buffer. 2. Opening a file with a leftover sidecar (e.g., from a crash or force-close) ignored it. This commit lands both prompts behind a small 3-button modal that returns "primary" | "secondary" | "cancel". UI scaffolding -------------- `index.html` adds a generic modal overlay (`#modal-overlay`) with a title, message, primary button (blue), secondary button (red/danger), and cancel button. Enter triggers primary, Escape returns cancel. Frontend (`src/main.ts`) ------------------------ - New `showModal(title, message, primaryLabel, secondaryLabel)`: returns a Promise<ModalChoice>. Reusable across all 3-button prompts. - `closeTab` is now split into a prompt wrapper + the existing tear- down (now in `doCloseTab`). When the tab is dirty, the modal asks "Save changes to <file>?" with Save / Discard / Cancel. Save on failure aborts the close so the user can retry. Cancel keeps the tab open. - `openFileByPath` calls `read_buffer_if_exists` after `read_file`. If the backend returns a non-null buffer (sidecar exists, non-empty, differs from disk), the modal asks "Restore unsaved edits?" with Restore / Discard / Cancel. Cancel aborts the open entirely. - Restored buffers flag the new tab dirty via `state.markDirty` so the user sees the dirty indicator and gets the save-on-close prompt next time around. Discard calls `clear_buffer` so the prompt doesn't fire again on subsequent opens. State (`src/state.ts`) ---------------------- - New `markDirty(id)` for the restore-then-flag-dirty flow. Lock-file hygiene ----------------- `package-lock.json` had 0.3.0 left over from the pre-Phase 1 npm install; npm refreshed it to 0.2.1-dev on the next run. Bundled here to keep the repo state clean (no behavior change). Out of scope ------------ The 3-button modal is local-only — it doesn't replace the window.confirm() prompt in the `scrybe://cli-quit` handler. That remains a separate concern (whole-app quit vs per-tab close) and can be unified later if we want consistent UX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phases 2 and 3 of the 0.2.1 tab-reload bugfix series, stacked into one
PR for review.
a5aed07): autosave writes to<path>.scrybe-buffersidecar instead of the real file. Real file changes only on explicit
save (Ctrl+S, 💾, CLI `scrybe save`, MCP `save`).
0c74538): save-on-close prompt for dirty tabs andrestore-on-open prompt when a leftover sidecar is found. Both use a
shared 3-button modal (Primary / Secondary / Cancel) added in
`index.html`.
Together these close the bug class that bit Shawn's daily-driver
workflow on 2026-05-28 (open tab clobbering external edits via
autosave self-write filter race).
Phase 1 already merged in #102 stops programmatic loads from
triggering self-write events. Phase 2 makes autosave physically
incapable of clobbering external edits. Phase 3 keeps the user
informed when buffer and disk diverge.
What changes
Phase 2 — backend (
src-tauri/src/lib.rs)save_buffer(path, content): writes `.scrybe-buffer`clear_buffer(path): cleanup; wired intoremove_backupread_buffer_if_exists(path): returnsSome(buffer)when anon-empty sidecar exists AND differs from disk
save_fileclears the sidecar after a successful writewatchedmembership (sidecar writes cannever emit
scrybe://file-changed)Phase 2 — frontend (
src/main.ts)save_buffer(no morenote_autosave,no more
markCleanon autosave — tab stays dirty until explicitsave)
Phase 3 — UI scaffolding (
index.html)#modal-overlaywith title, message, 3 buttons. Enter triggersprimary, Escape returns cancel.
Phase 3 — frontend (
src/main.ts)showModal(title, message, primary, secondary)reusable helpercloseTabsplit into a prompt wrapper +doCloseTab(the existingtear-down). Dirty tabs prompt "Save changes to ?" with
Save / Discard / Cancel; save failure aborts close; cancel keeps tab
open.
openFileByPathcallsread_buffer_if_existsafterread_file. Ifa recoverable buffer exists, the modal prompts "Restore unsaved
edits?" with Restore / Discard / Cancel. Restored buffers are
flagged dirty so the dirty indicator + save-on-close fire correctly.
Phase 3 — state (
src/state.ts)markDirty(id)for the restore-then-flag-dirty flowMisc
.gitignoreadds*.scrybe-bufferpackage-lock.jsonreflects the 0.3.0 → 0.2.1-dev bump that npminstall regenerated
Out of scope
scrybe://cli-quit(still useswindow.confirm). Can be unified later.conflict bar ("Keep mine / Take theirs") handles the dirty-on-disk-
change case at the user level.
Test plan
cargo fmt -- --check— passescargo clippy --all-targets -- -D warnings— passescargo test --package scrybe-app— 16/16 passnpx tsc --noEmit— passes<path>.scrybe-bufferappears but
<path>is unchangedCancel all behave correctly
restore-on-open modal appears. Restore / Discard / Cancel all
behave correctly
time
🤖 Generated with Claude Code