From 29a242492ff8760a0315f3c731dda3d906ec116a Mon Sep 17 00:00:00 2001
From: Sir <777x777@protonmail.com>
Date: Sat, 7 Feb 2026 03:40:26 +0400
Subject: [PATCH 1/2] feat: add tab background glow on process exit with
running indicator
When term:exitindicator is enabled, tabs show a colored background
glow reflecting process state:
- Amber spinning indicator while a cmd block is running
- Green glow when process exits successfully (exit 0)
- Red glow when process errors (non-zero exit or signal kill)
Uses the existing tab indicator system with a priority hierarchy:
bell (1) < running (1.5) < exit (2). Running indicator has
ClearOnFocus=false so it persists while the process is active.
Exit indicators auto-clear on focus via ClearOnFocus.
Skips the indicator when cmd:closeonexit would auto-delete the
block. Clears the running indicator before setting exit indicator
to prevent PersistentIndicator from resurrecting the amber glow.
The glow effect also applies to existing bell indicators, giving
them a colored background tint for better visibility.
Closes #2834
---
docs/docs/config.mdx | 2 +
frontend/app/tab/tab.scss | 15 ++++
frontend/app/tab/tab.tsx | 6 ++
pkg/blockcontroller/shellcontroller.go | 99 +++++++++++++++++++++++++
pkg/waveobj/metaconsts.go | 1 +
pkg/waveobj/wtypemeta.go | 1 +
pkg/wconfig/defaultconfig/settings.json | 1 +
pkg/wconfig/metaconsts.go | 1 +
pkg/wconfig/settingsconfig.go | 1 +
schema/settings.json | 3 +
10 files changed, 130 insertions(+)
diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx
index aad9e7d8c6..ff1a87db80 100644
--- a/docs/docs/config.mdx
+++ b/docs/docs/config.mdx
@@ -67,6 +67,7 @@ wsh editconfig
| term:macoptionismeta | bool | on macOS, treat the Option key as Meta key for terminal keybindings (default false) |
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
+| term:exitindicator | bool | when enabled, shows a colored tab indicator when a process exits — green for success, red for error (default false) |
| term:durable | bool | makes remote terminal sessions durable across network disconnects (defaults to true) |
| editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
@@ -134,6 +135,7 @@ For reference, this is the current default configuration (v0.11.5):
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
+ "term:exitindicator": false,
"term:copyonselect": true,
"term:durable": true,
"waveai:showcloudmodes": true,
diff --git a/frontend/app/tab/tab.scss b/frontend/app/tab/tab.scss
index 3739752eee..89559219e0 100644
--- a/frontend/app/tab/tab.scss
+++ b/frontend/app/tab/tab.scss
@@ -96,6 +96,15 @@
transition: none !important;
}
+ &.has-indicator {
+ .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ }
+ &.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ }
+ }
+
.wave-button {
position: absolute;
top: 50%;
@@ -129,6 +138,12 @@ body:not(.nohover) .tab.dragging {
border-color: transparent;
background: rgb(from var(--main-text-color) r g b / 0.1);
}
+ &.has-indicator .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ }
+ &.has-indicator.active .tab-inner {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ }
.close {
visibility: visible;
&:hover {
diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx
index 9a3d0f9925..8033762b15 100644
--- a/frontend/app/tab/tab.tsx
+++ b/frontend/app/tab/tab.tsx
@@ -224,7 +224,13 @@ const Tab = memo(
dragging: isDragging,
"before-active": isBeforeActive,
"new-tab": isNew,
+ "has-indicator": indicator != null,
})}
+ style={
+ indicator != null
+ ? ({ "--tab-indicator-color": indicator.color || "#f59e0b" } as React.CSSProperties)
+ : undefined
+ }
onMouseDown={onDragStart}
onClick={handleTabClick}
onContextMenu={handleContextMenu}
diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go
index b0c7081efc..57e93c6925 100644
--- a/pkg/blockcontroller/shellcontroller.go
+++ b/pkg/blockcontroller/shellcontroller.go
@@ -526,6 +526,34 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
shellInputCh := make(chan *BlockInputUnion, 32)
bc.ShellInputCh = shellInputCh
+ // Fire amber "running" indicator for cmd blocks
+ if bc.ControllerType == BlockController_Cmd {
+ exitIndicatorEnabled := blockMeta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
+ if !blockMeta.HasKey(waveobj.MetaKey_TermExitIndicator) {
+ if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
+ exitIndicatorEnabled = *globalVal
+ }
+ }
+ if exitIndicatorEnabled {
+ indicator := wshrpc.TabIndicator{
+ Icon: "spinner+spin",
+ Color: "#f59e0b",
+ Priority: 1.5,
+ ClearOnFocus: false,
+ }
+ eventData := wshrpc.TabIndicatorEventData{
+ TabId: bc.TabId,
+ Indicator: &indicator,
+ }
+ event := wps.WaveEvent{
+ Event: wps.Event_TabIndicator,
+ Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
+ Data: eventData,
+ }
+ wps.Broker.Publish(event)
+ }
+ }
+
go func() {
// handles regular output from the pty (goes to the blockfile and xterm)
defer func() {
@@ -616,6 +644,77 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
msg = fmt.Sprintf("%s (exit code %d)", baseMsg, exitCode)
}
bc.writeMutedMessageToTerminal("[" + msg + "]")
+ go func(exitCode int, exitSignal string) {
+ defer func() {
+ panichandler.PanicHandler("blockcontroller:exit-indicator", recover())
+ }()
+ ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
+ defer cancelFn()
+ blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, bc.BlockId)
+ if err != nil {
+ log.Printf("error getting block data for exit indicator: %v\n", err)
+ return
+ }
+ exitIndicatorEnabled := blockData.Meta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
+ if !blockData.Meta.HasKey(waveobj.MetaKey_TermExitIndicator) {
+ if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
+ exitIndicatorEnabled = *globalVal
+ }
+ }
+ if !exitIndicatorEnabled {
+ return
+ }
+ closeOnExit := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExit, false)
+ closeOnExitForce := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExitForce, false)
+ if closeOnExitForce || (closeOnExit && exitCode == 0) {
+ // Clear running indicator before block gets deleted
+ if bc.ControllerType == BlockController_Cmd {
+ clearEvent := wps.WaveEvent{
+ Event: wps.Event_TabIndicator,
+ Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
+ Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
+ }
+ wps.Broker.Publish(clearEvent)
+ }
+ return
+ }
+ // Clear running indicator before exit indicator to prevent
+ // PersistentIndicator from resurrecting the amber glow
+ if bc.ControllerType == BlockController_Cmd {
+ clearEvent := wps.WaveEvent{
+ Event: wps.Event_TabIndicator,
+ Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
+ Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
+ }
+ wps.Broker.Publish(clearEvent)
+ }
+ var indicator wshrpc.TabIndicator
+ if exitCode == 0 && exitSignal == "" {
+ indicator = wshrpc.TabIndicator{
+ Icon: "check",
+ Color: "#4ade80",
+ Priority: 2,
+ ClearOnFocus: true,
+ }
+ } else {
+ indicator = wshrpc.TabIndicator{
+ Icon: "xmark-large",
+ Color: "#f87171",
+ Priority: 2,
+ ClearOnFocus: true,
+ }
+ }
+ eventData := wshrpc.TabIndicatorEventData{
+ TabId: bc.TabId,
+ Indicator: &indicator,
+ }
+ event := wps.WaveEvent{
+ Event: wps.Event_TabIndicator,
+ Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
+ Data: eventData,
+ }
+ wps.Broker.Publish(event)
+ }(exitCode, exitSignal)
go checkCloseOnExit(bc.BlockId, exitCode)
}()
return nil
diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go
index c1383ee32c..c9c2a02462 100644
--- a/pkg/waveobj/metaconsts.go
+++ b/pkg/waveobj/metaconsts.go
@@ -119,6 +119,7 @@ const (
MetaKey_TermConnDebug = "term:conndebug"
MetaKey_TermBellSound = "term:bellsound"
MetaKey_TermBellIndicator = "term:bellindicator"
+ MetaKey_TermExitIndicator = "term:exitindicator"
MetaKey_TermDurable = "term:durable"
MetaKey_WebZoom = "web:zoom"
diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go
index 73fcf52fd7..bfb80a0923 100644
--- a/pkg/waveobj/wtypemeta.go
+++ b/pkg/waveobj/wtypemeta.go
@@ -123,6 +123,7 @@ type MetaTSType struct {
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
+ TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
TermDurable *bool `json:"term:durable,omitempty"`
WebZoom float64 `json:"web:zoom,omitempty"`
diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json
index f3869e7007..947901f705 100644
--- a/pkg/wconfig/defaultconfig/settings.json
+++ b/pkg/wconfig/defaultconfig/settings.json
@@ -26,6 +26,7 @@
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
+ "term:exitindicator": false,
"term:copyonselect": true,
"term:durable": false,
"waveai:showcloudmodes": true,
diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go
index b681627b6d..e0f90645c3 100644
--- a/pkg/wconfig/metaconsts.go
+++ b/pkg/wconfig/metaconsts.go
@@ -50,6 +50,7 @@ const (
ConfigKey_TermMacOptionIsMeta = "term:macoptionismeta"
ConfigKey_TermBellSound = "term:bellsound"
ConfigKey_TermBellIndicator = "term:bellindicator"
+ ConfigKey_TermExitIndicator = "term:exitindicator"
ConfigKey_TermDurable = "term:durable"
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go
index f636bb1008..81183b1f40 100644
--- a/pkg/wconfig/settingsconfig.go
+++ b/pkg/wconfig/settingsconfig.go
@@ -97,6 +97,7 @@ type SettingsType struct {
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
+ TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
TermDurable *bool `json:"term:durable,omitempty"`
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
diff --git a/schema/settings.json b/schema/settings.json
index e78511e33c..de7582153e 100644
--- a/schema/settings.json
+++ b/schema/settings.json
@@ -128,6 +128,9 @@
"term:bellindicator": {
"type": "boolean"
},
+ "term:exitindicator": {
+ "type": "boolean"
+ },
"term:durable": {
"type": "boolean"
},
From 2cf3c49314238978cfcb625458f57fbac5181b47 Mon Sep 17 00:00:00 2001
From: Sir <777x777@protonmail.com>
Date: Sun, 8 Feb 2026 02:18:47 +0400
Subject: [PATCH 2/2] feat: add subtle breathing glow to indicator tabs +
include generated types
Tab backgrounds with indicators now breathe with a very slow 5s
ease-in-out cycle (opacity 0.12-0.18, 0.16-0.22 when active) instead
of static tints. Barely perceptible but signals activity without the
distraction of spinning icons.
Also includes generated gotypes.d.ts for term:exitindicator that was
missing from the initial commit.
Co-Authored-By: Claude Opus 4.6
---
frontend/app/tab/tab.scss | 28 ++++++++++++++++++++++++----
frontend/types/gotypes.d.ts | 2 ++
2 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/frontend/app/tab/tab.scss b/frontend/app/tab/tab.scss
index 89559219e0..58076120da 100644
--- a/frontend/app/tab/tab.scss
+++ b/frontend/app/tab/tab.scss
@@ -98,10 +98,10 @@
&.has-indicator {
.tab-inner {
- background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ animation: indicatorBreathe 5s ease-in-out infinite;
}
&.active .tab-inner {
- background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ animation: indicatorBreatheActive 5s ease-in-out infinite;
}
}
@@ -139,10 +139,10 @@ body:not(.nohover) .tab.dragging {
background: rgb(from var(--main-text-color) r g b / 0.1);
}
&.has-indicator .tab-inner {
- background: rgb(from var(--tab-indicator-color) r g b / 0.15);
+ animation: indicatorBreathe 5s ease-in-out infinite;
}
&.has-indicator.active .tab-inner {
- background: rgb(from var(--tab-indicator-color) r g b / 0.2);
+ animation: indicatorBreatheActive 5s ease-in-out infinite;
}
.close {
visibility: visible;
@@ -157,6 +157,26 @@ body.nohover .tab.active .close {
visibility: visible;
}
+@keyframes indicatorBreathe {
+ 0%,
+ 100% {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.12);
+ }
+ 50% {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.18);
+ }
+}
+
+@keyframes indicatorBreatheActive {
+ 0%,
+ 100% {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.16);
+ }
+ 50% {
+ background: rgb(from var(--tab-indicator-color) r g b / 0.22);
+ }
+}
+
@keyframes expandWidthAndFadeIn {
from {
width: var(--initial-tab-width);
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 8a30f5e9be..c9dd6c0baf 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -1081,6 +1081,7 @@ declare global {
"term:conndebug"?: string;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
+ "term:exitindicator"?: boolean;
"term:durable"?: boolean;
"web:zoom"?: number;
"web:hidenav"?: boolean;
@@ -1267,6 +1268,7 @@ declare global {
"term:macoptionismeta"?: boolean;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
+ "term:exitindicator"?: boolean;
"term:durable"?: boolean;
"editor:minimapenabled"?: boolean;
"editor:stickyscrollenabled"?: boolean;