# Protect bulk tab-close paths against losing unsaved form data

## Context

`hasUnsavedChanges(tabId)` in `src/chrome/chromeActions.ts` exists and is already wired into the per-row close button (`src/main/flexView/pagelists/pagerow/PageRow.tsx:255,329`) and the Dedup button (`src/store/windowsSlice.ts:660` in `dedupTabsAsync`). Several bulk-close paths still call `closeTab` unconditionally.

## Call sites to fix

Gate these with `hasUnsavedChanges` before closing:

- `src/main/components/WindowGroupHeader.tsx:103` (`handleStashAll`) — Stash All on a window/group
- `src/main/components/WindowGroupHeader.tsx:109` (`handleSnoozeAll`) — Snooze All
- `src/main/inspector/MultiSelectActions.tsx:119` — multi-select Snooze Tabs
- `src/main/inspector/MultiSelectActions.tsx:103` — multi-select Forget tabs forever (also calls `deletePage` — only the close should be gated; deletion of DB records should still proceed for clean tabs)
- `src/main/flexView/pagelists/pagerow/PageTags.tsx:126` — per-row Stash icon
- `src/db/pages.ts:438` (`stashTabs`) — the helper used by stash flows; either gate inside or have callers gate first

## UX pattern

Count tabs with unsaved changes first; if any are dirty, show a single confirmation ("N tabs have unsaved changes — close anyway?") using the existing notification/dialog primitives. On cancel, close only the clean tabs and keep the dirty ones open. On confirm, close everything.

For the single-tab Stash icon in `PageTags.tsx`, mirror the existing PageRow behavior (skip the close and dispatch `setNotification('Tab not closed — unsaved changes')`). The DB-side stash tag should still be written regardless of whether the tab was closed — being stashed in the DB but still open in the browser is a fine state.

## Notes

- `hasUnsavedChanges` returns `false` when `chrome.scripting.executeScript` throws (chrome://, file://, no host permission). That fail-open is acceptable; do not change it.
- `hasUnsavedChanges` only catches DOM-default-vs-current diffs on form controls. It does NOT catch contenteditable editors (Notion, Google Docs comment boxes, Slack drafts). Out of scope for this task — feel free to note the limitation in a code comment near one of the call sites, but do not attempt to detect contenteditable dirtiness.
- `dedupAll` (the synchronous reducer at `src/store/windowsSlice.ts:457`) is exported but unused in the UI — leave it alone or delete it; do not spend effort gating it.
- The queue-processor's `closeTab` calls in `src/background/queueProcessor.ts:261,284` close temp tabs the processor itself opened; those are not user tabs and do not need gating.

Add or extend tests where reasonable, but the existing test suite has known pre-existing failures (see CLAUDE.md) — don't get stuck fixing those.
