diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx index 152655a0b0..f7ed1e319e 100644 --- a/docs/docs/config.mdx +++ b/docs/docs/config.mdx @@ -87,6 +87,7 @@ wsh editconfig | autoupdate:installonquit | bool | whether to automatically install updates on quit (requires app restart) | | autoupdate:channel | string | the auto update channel "latest" (stable builds), or "beta" (updated more frequently) (requires app restart) | | tab:preset | string | a "bg@" preset to automatically apply to new tabs. e.g. `bg@green`. should match the preset key | +| tab:confirmclose | bool | if set to true, a confirmation dialog will be shown before closing a tab (defaults to false) | | widget:showhelp | bool | whether to show help/tips widgets in right sidebar | | window:transparent | bool | set to true to enable window transparency (cannot be combined with `window:blur`) (macOS and Windows only, requires app restart, see [note on Windows compatibility](https://www.electronjs.org/docs/latest/tutorial/custom-window-styles#limitations)) | | window:blur | bool | set to enable window background blurring (cannot be combined with `window:transparent`) (macOS and Windows only, requires app restart, see [note on Windows compatibility](https://www.electronjs.org/docs/latest/tutorial/custom-window-styles#limitations)) | diff --git a/emain/emain-window.ts b/emain/emain-window.ts index d3b7f4849e..75dd1de379 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -728,15 +728,27 @@ ipcMain.on("set-waveai-open", (event, isOpen: boolean) => { } }); -ipcMain.on("close-tab", async (event, workspaceId, tabId) => { +ipcMain.handle("close-tab", async (event, workspaceId: string, tabId: string, confirmClose: boolean) => { const ww = getWaveWindowByWorkspaceId(workspaceId); if (ww == null) { console.log(`close-tab: no window found for workspace ws=${workspaceId} tab=${tabId}`); - return; + return false; + } + if (confirmClose) { + const choice = dialog.showMessageBoxSync(ww, { + type: "question", + defaultId: 1, // Enter activates "Close Tab" + cancelId: 0, // Esc activates "Cancel" + buttons: ["Cancel", "Close Tab"], + title: "Confirm", + message: "Are you sure you want to close this tab?", + }); + if (choice === 0) { + return false; + } } await ww.queueCloseTab(tabId); - event.returnValue = true; - return null; + return true; }); ipcMain.on("switch-workspace", (event, workspaceId) => { diff --git a/emain/preload.ts b/emain/preload.ts index 3ad8d25828..3b86b86b1a 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -51,7 +51,7 @@ contextBridge.exposeInMainWorld("api", { deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId), setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId), createTab: () => ipcRenderer.send("create-tab"), - closeTab: (workspaceId, tabId) => ipcRenderer.send("close-tab", workspaceId, tabId), + closeTab: (workspaceId, tabId, confirmClose) => ipcRenderer.invoke("close-tab", workspaceId, tabId, confirmClose), setWindowInitStatus: (status) => ipcRenderer.send("set-window-init-status", status), onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)), onBuilderInit: (callback) => ipcRenderer.on("builder-init", (_event, initOpts) => callback(initOpts)), diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index c83edbb779..d8c58ea435 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -130,8 +130,17 @@ function getStaticTabBlockCount(): number { function simpleCloseStaticTab() { const ws = globalStore.get(atoms.workspace); const tabId = globalStore.get(atoms.staticTabId); - getApi().closeTab(ws.oid, tabId); - deleteLayoutModelForTab(tabId); + const confirmClose = globalStore.get(getSettingsKeyAtom("tab:confirmclose")) ?? false; + getApi() + .closeTab(ws.oid, tabId, confirmClose) + .then((didClose) => { + if (didClose) { + deleteLayoutModelForTab(tabId); + } + }) + .catch((e) => { + console.log("error closing tab", e); + }); } function uxCloseBlock(blockId: string) { @@ -151,6 +160,13 @@ function uxCloseBlock(blockId: string) { const blockData = globalStore.get(blockAtom); const isAIFileDiff = blockData?.meta?.view === "aifilediff"; + // If this is the last block, closing it will close the tab — route through simpleCloseStaticTab + // so the tab:confirmclose setting is respected. + if (getStaticTabBlockCount() === 1) { + simpleCloseStaticTab(); + return; + } + const layoutModel = getLayoutModelForStaticTab(); const node = layoutModel.getNodeByBlockId(blockId); if (node) { @@ -190,6 +206,13 @@ function genericClose() { return; } + // If this is the last block, closing it will close the tab — route through simpleCloseStaticTab + // so the tab:confirmclose setting is respected. + if (blockCount === 1) { + simpleCloseStaticTab(); + return; + } + const layoutModel = getLayoutModelForStaticTab(); const focusedNode = globalStore.get(layoutModel.focusedNode); const blockId = focusedNode?.data?.blockId; diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 3f3e662a88..bd18ca197a 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -592,9 +592,18 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const handleCloseTab = (event: React.MouseEvent | null, tabId: string) => { event?.stopPropagation(); const ws = globalStore.get(atoms.workspace); - getApi().closeTab(ws.oid, tabId); - tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); - deleteLayoutModelForTab(tabId); + const confirmClose = globalStore.get(getSettingsKeyAtom("tab:confirmclose")) ?? false; + getApi() + .closeTab(ws.oid, tabId, confirmClose) + .then((didClose) => { + if (didClose) { + tabsWrapperRef.current?.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); + deleteLayoutModelForTab(tabId); + } + }) + .catch((e) => { + console.log("error closing tab", e); + }); }; const handleTabLoaded = useCallback((tabId: string) => { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index d6d2d98f01..cd92c6336e 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -118,7 +118,7 @@ declare global { deleteWorkspace: (workspaceId: string) => void; // delete-workspace setActiveTab: (tabId: string) => void; // set-active-tab createTab: () => void; // create-tab - closeTab: (workspaceId: string, tabId: string) => void; // close-tab + closeTab: (workspaceId: string, tabId: string, confirmClose: boolean) => Promise; // close-tab setWindowInitStatus: (status: "ready" | "wave-ready") => void; // set-window-init-status onWaveInit: (callback: (initOpts: WaveInitOpts) => void) => void; // wave-init onBuilderInit: (callback: (initOpts: BuilderInitOpts) => void) => void; // builder-init diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index dada3a248b..9d49696a85 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1306,6 +1306,7 @@ declare global { "markdown:fixedfontsize"?: number; "preview:showhiddenfiles"?: boolean; "tab:preset"?: string; + "tab:confirmclose"?: boolean; "widget:*"?: boolean; "widget:showhelp"?: boolean; "window:*"?: boolean; diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 179c7927fd..598ee5ca02 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -78,6 +78,7 @@ const ( ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles" ConfigKey_TabPreset = "tab:preset" + ConfigKey_TabConfirmClose = "tab:confirmclose" ConfigKey_WidgetClear = "widget:*" ConfigKey_WidgetShowHelp = "widget:showhelp" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index c744f77aa3..0a81f9ba45 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -124,7 +124,8 @@ type SettingsType struct { PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"` - TabPreset string `json:"tab:preset,omitempty"` + TabPreset string `json:"tab:preset,omitempty"` + TabConfirmClose bool `json:"tab:confirmclose,omitempty"` WidgetClear bool `json:"widget:*,omitempty"` WidgetShowHelp *bool `json:"widget:showhelp,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index 0f6365d711..7058295a37 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -194,6 +194,9 @@ "tab:preset": { "type": "string" }, + "tab:confirmclose": { + "type": "boolean" + }, "widget:*": { "type": "boolean" },