From 48913bf005b2d974dc42c946d30b1b2ea1550c76 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:17:25 +0000 Subject: [PATCH 1/6] feat: add tab:confirmclose setting to prompt before closing tabs - Add tab:confirmclose boolean config option to SettingsType (Go), schema/settings.json, and gotypes.d.ts - Update close-tab IPC handler to use ipcMain.handle (async) and accept confirmClose param - Show a native confirmation dialog via dialog.showMessageBoxSync when confirmClose is true - Update preload.ts to use ipcRenderer.invoke for close-tab, returning Promise - Update closeTab type signature in custom.d.ts to return Promise - Update tabbar.tsx and keymodel.ts to await closeTab result and only delete layout model on confirmed close - Document tab:confirmclose in docs/docs/config.mdx --- docs/docs/config.mdx | 1 + emain/emain-window.ts | 18 ++++++++++++++---- emain/preload.ts | 2 +- frontend/app/store/keymodel.ts | 10 ++++++++-- frontend/app/tab/tabbar.tsx | 12 +++++++++--- frontend/types/custom.d.ts | 2 +- frontend/types/gotypes.d.ts | 1 + pkg/wconfig/settingsconfig.go | 3 ++- schema/settings.json | 3 +++ 9 files changed, 40 insertions(+), 12 deletions(-) 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..cfab847c91 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -728,15 +728,25 @@ ipcMain.on("set-waveai-open", (event, isOpen: boolean) => { } }); -ipcMain.on("close-tab", async (event, workspaceId, tabId) => { +ipcMain.handle("close-tab", async (event, workspaceId, tabId, confirmClose) => { 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", + 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..e8df8b375b 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -130,8 +130,14 @@ 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); + } + }); } function uxCloseBlock(blockId: string) { diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 3f3e662a88..1a9f0ff928 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -592,9 +592,15 @@ 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); + } + }); }; 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/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" }, From 72d4b65921d34c51cc9c471695e0893a4be3a262 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 10:05:19 -0800 Subject: [PATCH 2/6] from task generate --- pkg/wconfig/metaconsts.go | 1 + 1 file changed, 1 insertion(+) 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" From 1a54f773db733b0a6e4012b117ab2d4f3687167f Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 10:09:38 -0800 Subject: [PATCH 3/6] add catch blocks --- frontend/app/store/keymodel.ts | 3 +++ frontend/app/tab/tabbar.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index e8df8b375b..e20dc5b122 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -137,6 +137,9 @@ function simpleCloseStaticTab() { if (didClose) { deleteLayoutModelForTab(tabId); } + }) + .catch((e) => { + console.log("error closing tab", e); }); } diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index 1a9f0ff928..d49c274308 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -600,6 +600,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => { tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); deleteLayoutModelForTab(tabId); } + }) + .catch((e) => { + console.log("error closing tab", e); }); }; From f874888798c16e97a030452c2fc8a2bf87235a10 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 11:24:48 -0800 Subject: [PATCH 4/6] set default/cancel ids --- emain/emain-window.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index cfab847c91..701fee9ae7 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -737,6 +737,8 @@ ipcMain.handle("close-tab", async (event, workspaceId, tabId, confirmClose) => { 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?", From 0cdddc20b1f476d688836a2d4449919f9ce643e0 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 11:39:31 -0800 Subject: [PATCH 5/6] handle last block closure => call through to give the close tab dialog --- frontend/app/store/keymodel.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index e20dc5b122..d8c58ea435 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -160,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) { @@ -199,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; From 76ee8785ebf763a9e9545a0f2636d28f63b8842d Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 12:21:16 -0800 Subject: [PATCH 6/6] fix nits --- emain/emain-window.ts | 2 +- frontend/app/tab/tabbar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 701fee9ae7..75dd1de379 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -728,7 +728,7 @@ ipcMain.on("set-waveai-open", (event, isOpen: boolean) => { } }); -ipcMain.handle("close-tab", async (event, workspaceId, tabId, confirmClose) => { +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}`); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index d49c274308..bd18ca197a 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -597,7 +597,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { .closeTab(ws.oid, tabId, confirmClose) .then((didClose) => { if (didClose) { - tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); + tabsWrapperRef.current?.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); deleteLayoutModelForTab(tabId); } })