Skip to content

feat: add file save confirmation on tab close#1982

Merged
Davidknp merged 1 commit into
mainfrom
commands-improvements
May 12, 2026
Merged

feat: add file save confirmation on tab close#1982
Davidknp merged 1 commit into
mainfrom
commands-improvements

Conversation

@Davidknp
Copy link
Copy Markdown
Collaborator

No description provided.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR adds an unsaved-changes confirmation dialog that appears when a user closes a dirty file tab, wiring the new closeTabWithGuard path through a registered async close handler in FileModelLifecycleStore.

  • New UnsavedChangesDialog offers "Save" and "Discard" actions but has no "Cancel" button and hides the close-button (showCloseButton={false}), so users have no explicit UI path to abort the close and keep the tab open.
  • Save-then-close flow in _confirmClose calls saveFile but doesn't check its outcome — saveFile swallows all errors internally, meaning a failed save still resolves to 'proceed' and the tab closes with changes silently lost.
  • registerCloseHandler stores only one handler; a second call silently replaces the first, which could cause missed guards as the codebase grows.

Confidence Score: 3/5

Two real defects on the new code path — no Cancel button in the dialog and silent data loss on save failure — need fixes before this ships to users.

The dialog's missing Cancel button means a user who accidentally clicks the tab-close X has no way to abort; they must either save or discard. More critically, when the user chooses Save but the disk write fails, saveFile swallows the error and _confirmClose still resolves to 'proceed', so closeTab fires and the unsaved changes are lost with no notification. Both issues are on the primary new code path introduced by this PR.

unsaved-changes-dialog.tsx (missing Cancel action) and the _confirmClose method in file-model-lifecycle-store.ts (save-error handling) need the most attention before merging.

Important Files Changed

Filename Overview
src/renderer/lib/components/unsaved-changes-dialog.tsx New dialog component for unsaved-changes confirmation; missing a Cancel button, leaving users no explicit way to abort a tab close.
src/renderer/features/tasks/editor/stores/file-model-lifecycle-store.ts Adds close-guard logic and _confirmClose; save errors are swallowed by saveFile, so a failed save still resolves to 'proceed' and closes the tab with data loss.
src/renderer/features/tasks/tabs/tab-manager-store.ts Adds closeTabWithGuard and registerCloseHandler; handler slot is a single field so a second registration silently replaces the first.
src/renderer/features/tasks/view/unified-main-tab-bar.tsx One-line change wiring onClose to closeTabWithGuard instead of closeTab; correct and safe.
src/renderer/app/modal-registry.ts Registers UnsavedChangesDialog in the modal registry at xs size; straightforward addition.

Sequence Diagram

sequenceDiagram
    participant User
    participant TabBar as UnifiedMainTabBar
    participant TM as TabManagerStore
    participant FMLS as FileModelLifecycleStore
    participant Dialog as UnsavedChangesDialog
    participant Registry as modelRegistry

    User->>TabBar: click x on tab
    TabBar->>TM: closeTabWithGuard(tabId)
    TM->>FMLS: _closeHandler(tabId)
    FMLS->>Registry: isDirty(uri)?
    alt file is dirty
        FMLS->>Dialog: showModal (fileName)
        alt User clicks Save
            Dialog->>FMLS: onSuccess('save')
            FMLS->>FMLS: saveFile(path) [errors swallowed]
            FMLS->>TM: closeTab(tabId) [always, even if save failed]
        else User clicks Discard
            Dialog->>FMLS: onSuccess('discard')
            FMLS->>TM: closeTab(tabId)
        else User dismisses overlay
            Dialog->>FMLS: onClose()
            Note over FMLS,TM: resolve('cancel') - tab stays open
        end
    else file is clean or non-file tab
        FMLS->>TM: closeTab(tabId)
    end
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/renderer/lib/components/unsaved-changes-dialog.tsx:28-34
**Missing "Cancel" button leaves no explicit escape path**

The dialog has `showCloseButton={false}` and only "Discard" and "Save" actions. "Discard" closes the tab without saving — that is not the same as cancelling the close gesture. If a user clicks the X tab button by mistake, there is no button to abort the operation and keep the tab open. The `onClose` callback exists (and correctly resolves to `'cancel'`), so all that is missing is a Cancel button wired to `onClose`.

### Issue 2 of 3
src/renderer/features/tasks/editor/stores/file-model-lifecycle-store.ts:229-232
**Save failure silently proceeds to tab close**

`saveFile` swallows all errors internally and always resolves, so `savePromise.then(() => resolve('proceed'))` always fires even when the disk write failed. If saving fails (permission error, disk full, network drive gone, etc.), the tab closes anyway and the user loses their changes without any indication. The save result or error should be checked before resolving to `'proceed'`.

### Issue 3 of 3
src/renderer/features/tasks/tabs/tab-manager-store.ts:538-540
**Second `registerCloseHandler` call silently replaces the first**

Only a single `_closeHandler` slot exists; a second call overwrites the first without warning. While only `FileModelLifecycleStore` registers a handler today, any future lifecycle store or plugin that needs to participate in close coordination will silently clobber the previous handler, leading to missed save-guards or missed cleanup. Consider composing handlers (e.g. an array) or at least asserting that no handler is already registered.

Reviews (1): Last reviewed commit: "feat: add file save confirmation on tab ..." | Re-trigger Greptile

Comment on lines +28 to +34
</DialogContentArea>
<DialogFooter>
<Button variant="outline" onClick={() => onSuccess('discard')}>
Discard
</Button>
<Button onClick={() => onSuccess('save')}>Save</Button>
</DialogFooter>
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.

P1 Missing "Cancel" button leaves no explicit escape path

The dialog has showCloseButton={false} and only "Discard" and "Save" actions. "Discard" closes the tab without saving — that is not the same as cancelling the close gesture. If a user clicks the X tab button by mistake, there is no button to abort the operation and keep the tab open. The onClose callback exists (and correctly resolves to 'cancel'), so all that is missing is a Cancel button wired to onClose.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/lib/components/unsaved-changes-dialog.tsx
Line: 28-34

Comment:
**Missing "Cancel" button leaves no explicit escape path**

The dialog has `showCloseButton={false}` and only "Discard" and "Save" actions. "Discard" closes the tab without saving — that is not the same as cancelling the close gesture. If a user clicks the X tab button by mistake, there is no button to abort the operation and keep the tab open. The `onClose` callback exists (and correctly resolves to `'cancel'`), so all that is missing is a Cancel button wired to `onClose`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +229 to +232
onSuccess: (result) => {
const savePromise = result === 'save' ? this.saveFile(path) : Promise.resolve();
void savePromise.then(() => resolve('proceed'));
},
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.

P1 Save failure silently proceeds to tab close

saveFile swallows all errors internally and always resolves, so savePromise.then(() => resolve('proceed')) always fires even when the disk write failed. If saving fails (permission error, disk full, network drive gone, etc.), the tab closes anyway and the user loses their changes without any indication. The save result or error should be checked before resolving to 'proceed'.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/features/tasks/editor/stores/file-model-lifecycle-store.ts
Line: 229-232

Comment:
**Save failure silently proceeds to tab close**

`saveFile` swallows all errors internally and always resolves, so `savePromise.then(() => resolve('proceed'))` always fires even when the disk write failed. If saving fails (permission error, disk full, network drive gone, etc.), the tab closes anyway and the user loses their changes without any indication. The save result or error should be checked before resolving to `'proceed'`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +538 to +540
registerCloseHandler(handler: (tabId: string) => Promise<void>): void {
this._closeHandler = handler;
}
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.

P2 Second registerCloseHandler call silently replaces the first

Only a single _closeHandler slot exists; a second call overwrites the first without warning. While only FileModelLifecycleStore registers a handler today, any future lifecycle store or plugin that needs to participate in close coordination will silently clobber the previous handler, leading to missed save-guards or missed cleanup. Consider composing handlers (e.g. an array) or at least asserting that no handler is already registered.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/renderer/features/tasks/tabs/tab-manager-store.ts
Line: 538-540

Comment:
**Second `registerCloseHandler` call silently replaces the first**

Only a single `_closeHandler` slot exists; a second call overwrites the first without warning. While only `FileModelLifecycleStore` registers a handler today, any future lifecycle store or plugin that needs to participate in close coordination will silently clobber the previous handler, leading to missed save-guards or missed cleanup. Consider composing handlers (e.g. an array) or at least asserting that no handler is already registered.

How can I resolve this? If you propose a fix, please make it concise.

@Davidknp Davidknp merged commit 40f957d into main May 12, 2026
1 check passed
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.

1 participant