Skip to content

Commit 65fce27

Browse files
feat: add tab:confirmclose setting to prompt before closing tabs (#2893)
- 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<boolean> - Update closeTab type signature in custom.d.ts to return Promise<boolean> - 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 --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
1 parent 69435ae commit 65fce27

10 files changed

Lines changed: 63 additions & 12 deletions

File tree

docs/docs/config.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ wsh editconfig
8787
| autoupdate:installonquit | bool | whether to automatically install updates on quit (requires app restart) |
8888
| autoupdate:channel | string | the auto update channel "latest" (stable builds), or "beta" (updated more frequently) (requires app restart) |
8989
| tab:preset | string | a "bg@" preset to automatically apply to new tabs. e.g. `bg@green`. should match the preset key |
90+
| tab:confirmclose | bool | if set to true, a confirmation dialog will be shown before closing a tab (defaults to false) |
9091
| widget:showhelp | bool | whether to show help/tips widgets in right sidebar |
9192
| 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)) |
9293
| 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)) |

emain/emain-window.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -728,15 +728,27 @@ ipcMain.on("set-waveai-open", (event, isOpen: boolean) => {
728728
}
729729
});
730730

731-
ipcMain.on("close-tab", async (event, workspaceId, tabId) => {
731+
ipcMain.handle("close-tab", async (event, workspaceId: string, tabId: string, confirmClose: boolean) => {
732732
const ww = getWaveWindowByWorkspaceId(workspaceId);
733733
if (ww == null) {
734734
console.log(`close-tab: no window found for workspace ws=${workspaceId} tab=${tabId}`);
735-
return;
735+
return false;
736+
}
737+
if (confirmClose) {
738+
const choice = dialog.showMessageBoxSync(ww, {
739+
type: "question",
740+
defaultId: 1, // Enter activates "Close Tab"
741+
cancelId: 0, // Esc activates "Cancel"
742+
buttons: ["Cancel", "Close Tab"],
743+
title: "Confirm",
744+
message: "Are you sure you want to close this tab?",
745+
});
746+
if (choice === 0) {
747+
return false;
748+
}
736749
}
737750
await ww.queueCloseTab(tabId);
738-
event.returnValue = true;
739-
return null;
751+
return true;
740752
});
741753

742754
ipcMain.on("switch-workspace", (event, workspaceId) => {

emain/preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ contextBridge.exposeInMainWorld("api", {
5151
deleteWorkspace: (workspaceId) => ipcRenderer.send("delete-workspace", workspaceId),
5252
setActiveTab: (tabId) => ipcRenderer.send("set-active-tab", tabId),
5353
createTab: () => ipcRenderer.send("create-tab"),
54-
closeTab: (workspaceId, tabId) => ipcRenderer.send("close-tab", workspaceId, tabId),
54+
closeTab: (workspaceId, tabId, confirmClose) => ipcRenderer.invoke("close-tab", workspaceId, tabId, confirmClose),
5555
setWindowInitStatus: (status) => ipcRenderer.send("set-window-init-status", status),
5656
onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)),
5757
onBuilderInit: (callback) => ipcRenderer.on("builder-init", (_event, initOpts) => callback(initOpts)),

frontend/app/store/keymodel.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,17 @@ function getStaticTabBlockCount(): number {
130130
function simpleCloseStaticTab() {
131131
const ws = globalStore.get(atoms.workspace);
132132
const tabId = globalStore.get(atoms.staticTabId);
133-
getApi().closeTab(ws.oid, tabId);
134-
deleteLayoutModelForTab(tabId);
133+
const confirmClose = globalStore.get(getSettingsKeyAtom("tab:confirmclose")) ?? false;
134+
getApi()
135+
.closeTab(ws.oid, tabId, confirmClose)
136+
.then((didClose) => {
137+
if (didClose) {
138+
deleteLayoutModelForTab(tabId);
139+
}
140+
})
141+
.catch((e) => {
142+
console.log("error closing tab", e);
143+
});
135144
}
136145

137146
function uxCloseBlock(blockId: string) {
@@ -151,6 +160,13 @@ function uxCloseBlock(blockId: string) {
151160
const blockData = globalStore.get(blockAtom);
152161
const isAIFileDiff = blockData?.meta?.view === "aifilediff";
153162

163+
// If this is the last block, closing it will close the tab — route through simpleCloseStaticTab
164+
// so the tab:confirmclose setting is respected.
165+
if (getStaticTabBlockCount() === 1) {
166+
simpleCloseStaticTab();
167+
return;
168+
}
169+
154170
const layoutModel = getLayoutModelForStaticTab();
155171
const node = layoutModel.getNodeByBlockId(blockId);
156172
if (node) {
@@ -190,6 +206,13 @@ function genericClose() {
190206
return;
191207
}
192208

209+
// If this is the last block, closing it will close the tab — route through simpleCloseStaticTab
210+
// so the tab:confirmclose setting is respected.
211+
if (blockCount === 1) {
212+
simpleCloseStaticTab();
213+
return;
214+
}
215+
193216
const layoutModel = getLayoutModelForStaticTab();
194217
const focusedNode = globalStore.get(layoutModel.focusedNode);
195218
const blockId = focusedNode?.data?.blockId;

frontend/app/tab/tabbar.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,9 +592,18 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
592592
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
593593
event?.stopPropagation();
594594
const ws = globalStore.get(atoms.workspace);
595-
getApi().closeTab(ws.oid, tabId);
596-
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease");
597-
deleteLayoutModelForTab(tabId);
595+
const confirmClose = globalStore.get(getSettingsKeyAtom("tab:confirmclose")) ?? false;
596+
getApi()
597+
.closeTab(ws.oid, tabId, confirmClose)
598+
.then((didClose) => {
599+
if (didClose) {
600+
tabsWrapperRef.current?.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease");
601+
deleteLayoutModelForTab(tabId);
602+
}
603+
})
604+
.catch((e) => {
605+
console.log("error closing tab", e);
606+
});
598607
};
599608

600609
const handleTabLoaded = useCallback((tabId: string) => {

frontend/types/custom.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ declare global {
118118
deleteWorkspace: (workspaceId: string) => void; // delete-workspace
119119
setActiveTab: (tabId: string) => void; // set-active-tab
120120
createTab: () => void; // create-tab
121-
closeTab: (workspaceId: string, tabId: string) => void; // close-tab
121+
closeTab: (workspaceId: string, tabId: string, confirmClose: boolean) => Promise<boolean>; // close-tab
122122
setWindowInitStatus: (status: "ready" | "wave-ready") => void; // set-window-init-status
123123
onWaveInit: (callback: (initOpts: WaveInitOpts) => void) => void; // wave-init
124124
onBuilderInit: (callback: (initOpts: BuilderInitOpts) => void) => void; // builder-init

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,7 @@ declare global {
13081308
"markdown:fixedfontsize"?: number;
13091309
"preview:showhiddenfiles"?: boolean;
13101310
"tab:preset"?: string;
1311+
"tab:confirmclose"?: boolean;
13111312
"widget:*"?: boolean;
13121313
"widget:showhelp"?: boolean;
13131314
"window:*"?: boolean;

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const (
7878
ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles"
7979

8080
ConfigKey_TabPreset = "tab:preset"
81+
ConfigKey_TabConfirmClose = "tab:confirmclose"
8182

8283
ConfigKey_WidgetClear = "widget:*"
8384
ConfigKey_WidgetShowHelp = "widget:showhelp"

pkg/wconfig/settingsconfig.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ type SettingsType struct {
124124

125125
PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"`
126126

127-
TabPreset string `json:"tab:preset,omitempty"`
127+
TabPreset string `json:"tab:preset,omitempty"`
128+
TabConfirmClose bool `json:"tab:confirmclose,omitempty"`
128129

129130
WidgetClear bool `json:"widget:*,omitempty"`
130131
WidgetShowHelp *bool `json:"widget:showhelp,omitempty"`

schema/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@
194194
"tab:preset": {
195195
"type": "string"
196196
},
197+
"tab:confirmclose": {
198+
"type": "boolean"
199+
},
197200
"widget:*": {
198201
"type": "boolean"
199202
},

0 commit comments

Comments
 (0)