Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <VersionBadge version="v0.14" /> | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator <VersionBadge version="v0.14" /> | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
| 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) |
| term:durable <VersionBadge version="v0.14" /> | 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 |
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions frontend/app/tab/tab.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@
transition: none !important;
}

&.has-indicator {
.tab-inner {
animation: indicatorBreathe 5s ease-in-out infinite;
}
&.active .tab-inner {
animation: indicatorBreatheActive 5s ease-in-out infinite;
}
}

.wave-button {
position: absolute;
top: 50%;
Expand Down Expand Up @@ -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 {
animation: indicatorBreathe 5s ease-in-out infinite;
}
&.has-indicator.active .tab-inner {
animation: indicatorBreatheActive 5s ease-in-out infinite;
}
.close {
visibility: visible;
&:hover {
Expand All @@ -142,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);
Expand Down
6 changes: 6 additions & 0 deletions frontend/app/tab/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
99 changes: 99 additions & 0 deletions pkg/blockcontroller/shellcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Comment on lines +529 to +555
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Running indicator logic looks good. Global settings fallback is correctly implemented, and the indicator is appropriately scoped to BlockController_Cmd blocks only.

One edge case: if the user disables term:exitindicator while a command is still running, the exit goroutine (line 664) returns early without clearing this amber spinner—leaving it stuck on the tab. Consider unconditionally clearing the running indicator for Cmd blocks before the exitIndicatorEnabled early-return:

Proposed fix
+			// Always clear the running indicator for cmd blocks
+			if blockData.Meta.GetString(waveobj.MetaKey_Controller, "") == 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)
+			}
 			if !exitIndicatorEnabled {
 				return
 			}


go func() {
// handles regular output from the pty (goes to the blockfile and xterm)
defer func() {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/waveobj/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pkg/waveobj/wtypemeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/defaultconfig/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/metaconsts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pkg/wconfig/settingsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
3 changes: 3 additions & 0 deletions schema/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@
"term:bellindicator": {
"type": "boolean"
},
"term:exitindicator": {
"type": "boolean"
},
"term:durable": {
"type": "boolean"
},
Expand Down