From e9750beaf5fe601d6b83d5bd5e3af76bb781de7c Mon Sep 17 00:00:00 2001 From: ashvin Date: Fri, 13 Mar 2026 21:29:24 +0100 Subject: [PATCH 1/2] feat: close window when last terminal exits (term:closeonlasttermclose) Adds a new global setting `term:closeonlasttermclose` that automatically closes the window when the last shell/terminal block exits. When enabled, typing `exit` in the last remaining terminal closes the window immediately (no delay), while non-last terminals are unaffected. Closes #3026 Co-Authored-By: Claude Sonnet 4.6 --- frontend/types/gotypes.d.ts | 1 + pkg/blockcontroller/shellcontroller.go | 46 ++++++++++++++++++++++++-- pkg/wconfig/metaconsts.go | 1 + pkg/wconfig/settingsconfig.go | 1 + schema/settings.json | 3 ++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index a53527e346..cadc477e54 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1342,6 +1342,7 @@ declare global { "term:bellindicator"?: boolean; "term:osc52"?: string; "term:durable"?: boolean; + "term:closeonlasttermclose"?: boolean; "editor:minimapenabled"?: boolean; "editor:stickyscrollenabled"?: boolean; "editor:wordwrap"?: boolean; diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index a410225394..b139fe605c 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -647,6 +647,39 @@ func (union *ConnUnion) getRemoteInfoAndShellType(blockMeta waveobj.MetaMapType) return nil } +func isLastShellBlockInWorkspace(ctx context.Context, blockId string) bool { + tabId, err := wstore.DBFindTabForBlockId(ctx, blockId) + if err != nil || tabId == "" { + return false + } + workspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, tabId) + if err != nil || workspaceId == "" { + return false + } + workspace, err := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if err != nil || workspace == nil { + return false + } + shellBlockCount := 0 + for _, wsTabId := range workspace.TabIds { + tab, err := wstore.DBGet[*waveobj.Tab](ctx, wsTabId) + if err != nil || tab == nil { + continue + } + for _, wsBlockId := range tab.BlockIds { + block, err := wstore.DBGet[*waveobj.Block](ctx, wsBlockId) + if err != nil || block == nil { + continue + } + controller := block.Meta.GetString(waveobj.MetaKey_Controller, "") + if controller == BlockController_Shell || controller == BlockController_Cmd { + shellBlockCount++ + } + } + } + return shellBlockCount == 1 +} + func checkCloseOnExit(blockId string, exitCode int) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() @@ -657,10 +690,19 @@ func checkCloseOnExit(blockId string, exitCode int) { } closeOnExit := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExit, false) closeOnExitForce := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExitForce, false) + defaultDelayMs := 2000.0 if !closeOnExitForce && !(closeOnExit && exitCode == 0) { - return + // Check global setting: close when last terminal exits + settings := wconfig.GetWatcher().GetFullConfig().Settings + if !settings.TermCloseOnLastTermClose { + return + } + if !isLastShellBlockInWorkspace(ctx, blockId) { + return + } + defaultDelayMs = 0 } - delayMs := blockData.Meta.GetFloat(waveobj.MetaKey_CmdCloseOnExitDelay, 2000) + delayMs := blockData.Meta.GetFloat(waveobj.MetaKey_CmdCloseOnExitDelay, defaultDelayMs) if delayMs < 0 { delayMs = 0 } diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index e031a493ea..1b60d386e2 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -58,6 +58,7 @@ const ( ConfigKey_TermBellIndicator = "term:bellindicator" ConfigKey_TermOsc52 = "term:osc52" ConfigKey_TermDurable = "term:durable" + ConfigKey_TermCloseOnLastTermClose = "term:closeonlasttermclose" ConfigKey_EditorMinimapEnabled = "editor:minimapenabled" ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 69c531eb77..c926d5e92f 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -109,6 +109,7 @@ type SettingsType struct { TermBellIndicator *bool `json:"term:bellindicator,omitempty"` TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"` TermDurable *bool `json:"term:durable,omitempty"` + TermCloseOnLastTermClose bool `json:"term:closeonlasttermclose,omitempty"` EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"` EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index d60367bea3..1f4617f837 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -161,6 +161,9 @@ "term:durable": { "type": "boolean" }, + "term:closeonlasttermclose": { + "type": "boolean" + }, "editor:minimapenabled": { "type": "boolean" }, From 96dbd53fb3e6cdcb6035567ba76e96d5da65bc01 Mon Sep 17 00:00:00 2001 From: ashvin Date: Fri, 13 Mar 2026 21:40:26 +0100 Subject: [PATCH 2/2] fix: return false on DB read errors in isLastShellBlockInWorkspace Fail closed on DB errors instead of skipping blocks with continue, which could undercount shell blocks and incorrectly trigger window close. Co-Authored-By: Claude Sonnet 4.6 --- pkg/blockcontroller/shellcontroller.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index b139fe605c..5e23e3a893 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -663,12 +663,18 @@ func isLastShellBlockInWorkspace(ctx context.Context, blockId string) bool { shellBlockCount := 0 for _, wsTabId := range workspace.TabIds { tab, err := wstore.DBGet[*waveobj.Tab](ctx, wsTabId) - if err != nil || tab == nil { + if err != nil { + return false + } + if tab == nil { continue } for _, wsBlockId := range tab.BlockIds { block, err := wstore.DBGet[*waveobj.Block](ctx, wsBlockId) - if err != nil || block == nil { + if err != nil { + return false + } + if block == nil { continue } controller := block.Meta.GetString(waveobj.MetaKey_Controller, "")