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;