Skip to content

Commit 7014aa5

Browse files
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
1 parent c199f34 commit 7014aa5

File tree

10 files changed

+124
-0
lines changed

10 files changed

+124
-0
lines changed

docs/docs/config.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ wsh editconfig
6767
| term:macoptionismeta | bool | on macOS, treat the Option key as Meta key for terminal keybindings (default false) |
6868
| term:bellsound <VersionBadge version="v0.14" /> | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
6969
| term:bellindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
70+
| term:exitindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a colored tab indicator when a process exits — green for success, red for error (default false) |
7071
| term:durable <VersionBadge version="v0.14" /> | bool | makes remote terminal sessions durable across network disconnects (defaults to true) |
7172
| editor:minimapenabled | bool | set to false to disable editor minimap |
7273
| 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):
134135
"telemetry:enabled": true,
135136
"term:bellsound": false,
136137
"term:bellindicator": false,
138+
"term:exitindicator": false,
137139
"term:copyonselect": true,
138140
"term:durable": true,
139141
"waveai:showcloudmodes": true,

frontend/app/tab/tab.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@
9696
transition: none !important;
9797
}
9898

99+
&.has-indicator {
100+
.tab-inner {
101+
background: rgb(from var(--tab-indicator-color) r g b / 0.15);
102+
}
103+
&.active .tab-inner {
104+
background: rgb(from var(--tab-indicator-color) r g b / 0.2);
105+
}
106+
}
107+
99108
.wave-button {
100109
position: absolute;
101110
top: 50%;

frontend/app/tab/tab.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,13 @@ const Tab = memo(
224224
dragging: isDragging,
225225
"before-active": isBeforeActive,
226226
"new-tab": isNew,
227+
"has-indicator": indicator != null,
227228
})}
229+
style={
230+
indicator?.color
231+
? ({ "--tab-indicator-color": indicator.color } as React.CSSProperties)
232+
: undefined
233+
}
228234
onMouseDown={onDragStart}
229235
onClick={handleTabClick}
230236
onContextMenu={handleContextMenu}

pkg/blockcontroller/shellcontroller.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,34 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
526526
shellInputCh := make(chan *BlockInputUnion, 32)
527527
bc.ShellInputCh = shellInputCh
528528

529+
// Fire amber "running" indicator for cmd blocks
530+
if bc.ControllerType == BlockController_Cmd {
531+
exitIndicatorEnabled := blockMeta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
532+
if !blockMeta.HasKey(waveobj.MetaKey_TermExitIndicator) {
533+
if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
534+
exitIndicatorEnabled = *globalVal
535+
}
536+
}
537+
if exitIndicatorEnabled {
538+
indicator := wshrpc.TabIndicator{
539+
Icon: "spinner+spin",
540+
Color: "#f59e0b",
541+
Priority: 1.5,
542+
ClearOnFocus: false,
543+
}
544+
eventData := wshrpc.TabIndicatorEventData{
545+
TabId: bc.TabId,
546+
Indicator: &indicator,
547+
}
548+
event := wps.WaveEvent{
549+
Event: wps.Event_TabIndicator,
550+
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
551+
Data: eventData,
552+
}
553+
wps.Broker.Publish(event)
554+
}
555+
}
556+
529557
go func() {
530558
// handles regular output from the pty (goes to the blockfile and xterm)
531559
defer func() {
@@ -616,6 +644,77 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
616644
msg = fmt.Sprintf("%s (exit code %d)", baseMsg, exitCode)
617645
}
618646
bc.writeMutedMessageToTerminal("[" + msg + "]")
647+
go func(exitCode int, exitSignal string) {
648+
defer func() {
649+
panichandler.PanicHandler("blockcontroller:exit-indicator", recover())
650+
}()
651+
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
652+
defer cancelFn()
653+
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, bc.BlockId)
654+
if err != nil {
655+
log.Printf("error getting block data for exit indicator: %v\n", err)
656+
return
657+
}
658+
exitIndicatorEnabled := blockData.Meta.GetBool(waveobj.MetaKey_TermExitIndicator, false)
659+
if !blockData.Meta.HasKey(waveobj.MetaKey_TermExitIndicator) {
660+
if globalVal := wconfig.GetWatcher().GetFullConfig().Settings.TermExitIndicator; globalVal != nil {
661+
exitIndicatorEnabled = *globalVal
662+
}
663+
}
664+
if !exitIndicatorEnabled {
665+
return
666+
}
667+
closeOnExit := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExit, false)
668+
closeOnExitForce := blockData.Meta.GetBool(waveobj.MetaKey_CmdCloseOnExitForce, false)
669+
if closeOnExitForce || (closeOnExit && exitCode == 0) {
670+
// Clear running indicator before block gets deleted
671+
if bc.ControllerType == BlockController_Cmd {
672+
clearEvent := wps.WaveEvent{
673+
Event: wps.Event_TabIndicator,
674+
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
675+
Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
676+
}
677+
wps.Broker.Publish(clearEvent)
678+
}
679+
return
680+
}
681+
// Clear running indicator before exit indicator to prevent
682+
// PersistentIndicator from resurrecting the amber glow
683+
if bc.ControllerType == BlockController_Cmd {
684+
clearEvent := wps.WaveEvent{
685+
Event: wps.Event_TabIndicator,
686+
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
687+
Data: wshrpc.TabIndicatorEventData{TabId: bc.TabId, Indicator: nil},
688+
}
689+
wps.Broker.Publish(clearEvent)
690+
}
691+
var indicator wshrpc.TabIndicator
692+
if exitCode == 0 && exitSignal == "" {
693+
indicator = wshrpc.TabIndicator{
694+
Icon: "check",
695+
Color: "#4ade80",
696+
Priority: 2,
697+
ClearOnFocus: true,
698+
}
699+
} else {
700+
indicator = wshrpc.TabIndicator{
701+
Icon: "xmark-large",
702+
Color: "#f87171",
703+
Priority: 2,
704+
ClearOnFocus: true,
705+
}
706+
}
707+
eventData := wshrpc.TabIndicatorEventData{
708+
TabId: bc.TabId,
709+
Indicator: &indicator,
710+
}
711+
event := wps.WaveEvent{
712+
Event: wps.Event_TabIndicator,
713+
Scopes: []string{waveobj.MakeORef(waveobj.OType_Tab, bc.TabId).String()},
714+
Data: eventData,
715+
}
716+
wps.Broker.Publish(event)
717+
}(exitCode, exitSignal)
619718
go checkCloseOnExit(bc.BlockId, exitCode)
620719
}()
621720
return nil

pkg/waveobj/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const (
119119
MetaKey_TermConnDebug = "term:conndebug"
120120
MetaKey_TermBellSound = "term:bellsound"
121121
MetaKey_TermBellIndicator = "term:bellindicator"
122+
MetaKey_TermExitIndicator = "term:exitindicator"
122123
MetaKey_TermDurable = "term:durable"
123124

124125
MetaKey_WebZoom = "web:zoom"

pkg/waveobj/wtypemeta.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type MetaTSType struct {
123123
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
124124
TermBellSound *bool `json:"term:bellsound,omitempty"`
125125
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
126+
TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
126127
TermDurable *bool `json:"term:durable,omitempty"`
127128

128129
WebZoom float64 `json:"web:zoom,omitempty"`

pkg/wconfig/defaultconfig/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"telemetry:enabled": true,
2727
"term:bellsound": false,
2828
"term:bellindicator": false,
29+
"term:exitindicator": false,
2930
"term:copyonselect": true,
3031
"term:durable": false,
3132
"waveai:showcloudmodes": true,

pkg/wconfig/metaconsts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const (
5050
ConfigKey_TermMacOptionIsMeta = "term:macoptionismeta"
5151
ConfigKey_TermBellSound = "term:bellsound"
5252
ConfigKey_TermBellIndicator = "term:bellindicator"
53+
ConfigKey_TermExitIndicator = "term:exitindicator"
5354
ConfigKey_TermDurable = "term:durable"
5455

5556
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"

pkg/wconfig/settingsconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ type SettingsType struct {
9797
TermMacOptionIsMeta *bool `json:"term:macoptionismeta,omitempty"`
9898
TermBellSound *bool `json:"term:bellsound,omitempty"`
9999
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
100+
TermExitIndicator *bool `json:"term:exitindicator,omitempty"`
100101
TermDurable *bool `json:"term:durable,omitempty"`
101102

102103
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`

schema/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@
128128
"term:bellindicator": {
129129
"type": "boolean"
130130
},
131+
"term:exitindicator": {
132+
"type": "boolean"
133+
},
131134
"term:durable": {
132135
"type": "boolean"
133136
},

0 commit comments

Comments
 (0)